Compare commits

...

8 Commits

Author SHA1 Message Date
Balázs Orbán
afb1fcdae3 fix(providers): add normalizeIdentifier to EmailProvider
* fix(providers): add `normalizeIdentifier` to EmailProvider

* docs: document `normalizeIdentifier`

* fix: allow throwing error from normalizer

* test: add e-mail tests

* chore: log provider id

* test: merge client+config jest configs and add coverage report

* test: show coverage for untested files

* fix: only allow first domain in email. Add tests

* chore: add `coverage` to tsconfig exclude list

* cleanup

* revert

Co-authored-by: Thang Vu <thvu@hey.com>
2022-08-01 13:43:19 +02:00
Gavin Fogel
a21db8950f fix(ts): fix jsdoc link to documentation (#5039) 2022-07-28 06:54:01 +02:00
Daniel González Reina
e8371ab23a docs(providers): update custom Reddit provider to v4 (#4985)
* Update custom reddit provider to v4

* Reuse RedditProvider's default options

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-07-26 10:43:45 +02:00
Visor Web Technologies
9cdeb2ce7d docs: Fixed Typo (#5025) 2022-07-26 10:39:33 +02:00
Thang Vu
89829d8a88 chore: fix unstable_getServerSession usages in dev app (#5017)
chore: fix unstable_getServerSession usages in dev app
2022-07-25 23:08:59 +02:00
Balázs Orbán
aedabc8d3f fix: avoid redirect on always public paths (#5000)
* type safe babel config

* avoid auth redirect for `_next`

* force render default error page on user miconfig

* add slash to _next path

* use `.some`

* add docs

* change from localhost

* add favicon to public path
2022-07-24 00:45:56 +02:00
Balázs Orbán
9f2cdad457 docs: add Thang as point of contact 2022-07-22 20:31:19 +02:00
Yoann Fleury
b107ca4946 docs: update path to gitlab provider (#4997) 2022-07-22 19:33:03 +02:00
26 changed files with 374 additions and 108 deletions

View File

@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
reported by contacting info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com.
All complaints will be reviewed and investigated and will result in a response
that is deemed necessary and appropriate to the circumstances. The project team
is obligated to maintain confidentiality with regard to the reporter of an

View File

@@ -13,7 +13,7 @@ If you contact us regarding a serious issue:
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
The best way to report an issue is by contacting us via email at info@balazsorban.com or me@iaincollins.com and yo@ndo.dev, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
The best way to report an issue is by contacting us via email at info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
> For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem in the future) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.

View File

@@ -1,8 +1,9 @@
// This is an example of to protect an API route
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, options)
const session = await unstable_getServerSession(req, res, authOptions)
if (session) {
res.send({

View File

@@ -1,5 +1,6 @@
// This is an example of how to access a session from an API route
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from '../auth/[...nextauth]';
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)

View File

@@ -1,5 +1,6 @@
import { unstable_getServerSession } from "next-auth/next"
import Layout from "../components/layout"
import { authOptions } from './api/auth/[...nextauth]';
export default function Page() {
// As this page uses Server Side Rendering, the `session` will be already
@@ -40,8 +41,8 @@ export async function getServerSideProps(context) {
return {
props: {
session: await unstable_getServerSession(
contex.req,
contex.res,
context.req,
context.res,
authOptions
),
},

View File

@@ -137,6 +137,10 @@ Callbacks are asynchronous functions you can use to control what happens when an
Specify URLs to be used if you want to create custom sign in, and error pages. Pages specified will override the corresponding built-in page.
:::note
This should match the `pages` configuration that's found in `[...nextauth].ts`.
:::
#### Example (default value)
```js

View File

@@ -152,7 +152,7 @@ This error occurs when there was an issue deleting the session from the database
---
### Other
### Configuration
#### MISSING_NEXTAUTH_API_ROUTE_ERROR
@@ -164,6 +164,23 @@ Make sure the file is there and the filename is written correctly.
In production, we expect you to define a `secret` property in your configuration. In development, this is shown as a warning for convenience. [Read more](/configuration/options#secret)
#### AUTH_ON_ERROR_PAGE_ERROR
You have a custom error page defined that was rendered due to an error, but the page also required authentication. To avoid an infinite redirect loop, NextAuth.js bailed out and rendered its default error page instead.
If you are using a Middleware, make sure you include the same `pages` configuration in your `middleware.ts` and `[...nextauth].ts` files. Or use the `matcher` option to only require authentication for certain sites (and exclude your custom error page).
If you do not use a Middleware, make sure you don't try redirecting the user to the sign-in page when hitting your custom error page.
Useful links:
- https://next-auth.js.org/configuration/nextjs#pages
- https://next-auth.js.org/configuration/pages
- https://nextjs.org/docs/advanced-features/middleware#matcher
### Other
#### oauth_callback_error expected 200 OK with body but no body was returned
This error might happen with some of the providers. It happens due to `openid-client`(which is peer dependency) node version mismatch. For instance, `openid-client` requires `>=14.2.0` for `lts/fermium` and has similar limits for the other versions. For the full list of the compatible node versions please see [package.json](https://github.com/panva/node-openid-client/blob/2a84e46992e1ebeaf685c3f87b65663d126e81aa/package.json#L78)

View File

@@ -329,7 +329,7 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
Avoid storing any data in a token that might be problematic if it were to be decrypted in the future.
- If you do not explicitly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret. Since v4 this only impacts development and generating a secret is required in production.
- If you do not explicitly specify a secret for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret. Since v4 this only impacts development and generating a secret is required in production.
</p>

View File

@@ -223,3 +223,31 @@ providers: [
})
],
```
## Normalizing the email address
By default, NextAuth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior:
```ts
EmailProvider({
// ...
normalizeIdentifier(identifier: string): string {
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = identifier.toLowerCase().trim().split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
return `${local}@${domain}`
// You can also throw an error, which will redirect the user
// to the error page with error=EmailSignin in the URL
// if (identifier.split("@").length > 2) {
// throw new Error("Only one email allowed")
// }
},
})
```
:::warning
Always make sure this returns a single e-mail address, even if multiple ones were passed in.
:::

View File

@@ -15,7 +15,7 @@ https://gitlab.com/-/profile/applications
The **Gitlab Provider** comes with a set of default options:
- [Gitlab Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/gitlab.js)
- [Gitlab Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/gitlab.ts)
You can override any of the options to suit your own use case.

View File

@@ -46,27 +46,15 @@ This Provider template only has a one hour access token to it and only has the "
```js
providers: [
{
id: "reddit",
name: "Reddit",
RedditProvider({
clientId: process.env.REDDIT_CLIENT_ID,
clientSecret: process.env.REDDIT_CLIENT_SECRET,
scope: "identity mysubreddits read", //Check Reddit API Documentation for more. The identity scope is required.
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: " https://www.reddit.com/api/v1/access_token",
authorizationUrl:
"https://www.reddit.com/api/v1/authorize?response_type=code&duration=permanent",
profileUrl: "https://oauth.reddit.com/api/v1/me",
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
email: null,
}
authorization: {
params: {
duration: 'permanent',
},
},
},
}),
]
```

View File

@@ -16,7 +16,7 @@ If you contact us regarding a serious issue:
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
The best way to report an issue is by contacting us via email at info@balazsorban.com or me@iaincollins.com and yo@ndo.dev, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
The best way to report an issue is by contacting us via email at info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
:::note
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem in the future) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.

View File

@@ -1,7 +1,9 @@
// @ts-check
// We aim to have the same support as Next.js
// https://nextjs.org/docs/getting-started#system-requirements
// https://nextjs.org/docs/basic-features/supported-browsers-features
/** @type {import("@babel/core").ConfigFunction} */
module.exports = (api) => {
const isTest = api.env("test")
if (isTest) {

View File

@@ -1,16 +0,0 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
transform: {
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", require("./swc.config")],
},
rootDir: "../src",
setupFilesAfterEnv: ["../config/jest-setup.js"],
testMatch: ["**/*.test.js"],
// collectCoverageFrom: ["!client/__tests__/**"],
// coverageDirectory: "../coverage",
testEnvironment: "jsdom",
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
}

View File

@@ -0,0 +1,34 @@
/** @type {import('jest').Config} */
module.exports = {
projects: [
{
displayName: "core",
testMatch: ["**/*.test.ts"],
rootDir: ".",
setupFilesAfterEnv: ["./config/jest-setup.js"],
transform: {
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", require("./swc.config")],
},
coveragePathIgnorePatterns: ["tests"],
},
{
displayName: "client",
testMatch: ["**/*.test.js"],
setupFilesAfterEnv: ["./config/jest-setup.js"],
rootDir: ".",
transform: {
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", require("./swc.config")],
},
testEnvironment: "jsdom",
coveragePathIgnorePatterns: ["__tests__"],
},
],
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
collectCoverage: true,
coverageDirectory: "../coverage",
coverageReporters: ["html", "text-summary"],
collectCoverageFrom: ["src/**/*.(js|jsx|ts|tsx)"],
}

View File

@@ -1,13 +0,0 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
transform: {
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", require("./swc.config")],
},
rootDir: "..",
testMatch: ["**/*.test.ts"],
setupFilesAfterEnv: ["./config/jest-setup.js"],
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
}

View File

@@ -42,9 +42,7 @@
"build:js": "pnpm clean && pnpm generate-providers && tsc && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir .",
"test:client": "jest --config ./config/jest.client.config.js",
"test:core": "jest --config ./config/jest.core.config.js",
"test": "pnpm test:core && pnpm test:client",
"test": "jest --config ./config/jest.config.js",
"prepublishOnly": "pnpm build",
"generate-providers": "node ./config/generate-providers.js",
"setup": "pnpm generate-providers",

View File

@@ -96,13 +96,28 @@ export async function NextAuthHandler<
// Bail out early if there's an error in the user config
const { pages, theme } = userOptions
logger.error(assertionResult.code, assertionResult)
if (pages?.error) {
return {
redirect: `${pages.error}?error=Configuration`,
const authOnErrorPage =
pages?.error &&
req.action === "signin" &&
req.query?.callbackUrl.startsWith(pages.error)
if (!pages?.error || authOnErrorPage) {
if (authOnErrorPage) {
logger.error(
"AUTH_ON_ERROR_PAGE_ERROR",
new Error(
`The error page ${pages?.error} should not require authentication`
)
)
}
const render = renderPage({ theme })
return render.error({ error: "configuration" })
}
return {
redirect: `${pages.error}?error=Configuration`,
}
const render = renderPage({ theme })
return render.error({ error: "configuration" })
}
const { action, providerId, error, method = "GET" } = req

View File

@@ -21,27 +21,28 @@ export default async function email(
Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000
)
// Save in database
// @ts-expect-error
await adapter.createVerificationToken({
identifier,
token: hashToken(token, options),
expires,
})
// Generate a link with email, unhashed token and callback url
const params = new URLSearchParams({ callbackUrl, token, email: identifier })
const _url = `${url}/callback/${provider.id}?${params}`
// Send to user
await provider.sendVerificationRequest({
identifier,
token,
expires,
url: _url,
provider,
theme,
})
await Promise.all([
// Send to user
provider.sendVerificationRequest({
identifier,
token,
expires,
url: _url,
provider,
theme,
}),
// Save in database
// @ts-expect-error // verified in `assertConfig`
adapter.createVerificationToken({
identifier,
token: hashToken(token, options),
expires,
}),
])
return `${url}/verify-request?${new URLSearchParams({
provider: provider.id,

View File

@@ -33,16 +33,26 @@ export default async function signin(params: {
return { redirect: `${url}/error?error=OAuthSignin` }
}
} else if (provider.type === "email") {
/**
* @note Technically the part of the email address local mailbox element
* (everything before the @ symbol) should be treated as 'case sensitive'
* according to RFC 2821, but in practice this causes more problems than
* it solves. We treat email addresses as all lower case. If anyone
* complains about this we can make strict RFC 2821 compliance an option.
*/
const email = body?.email?.toLowerCase()
let email: string = body?.email
if (!email) return { redirect: `${url}/error?error=EmailSignin` }
const normalizer: (identifier: string) => string =
provider.normalizeIdentifier ??
((identifier) => {
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = identifier.toLowerCase().trim().split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
return `${local}@${domain}`
})
try {
email = normalizer(body?.email)
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", { error, providerId: provider.id })
return { redirect: `${url}/error?error=EmailSignin` }
}
// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -85,10 +95,7 @@ export default async function signin(params: {
const redirect = await emailSignin(email, options)
return { redirect }
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", {
error: error as Error,
providerId: provider.id,
})
logger.error("SIGNIN_EMAIL_ERROR", { error, providerId: provider.id })
return { redirect: `${url}/error?error=EmailSignin` }
}
}

View File

@@ -101,13 +101,18 @@ async function handleMiddleware(
options: NextAuthMiddlewareOptions | undefined,
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
) {
const { pathname, search, origin } = req.nextUrl
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error"
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
// Avoid infinite redirect loop
const publicPaths = [signInPage, errorPage, "/_next", "/favicon.ico"]
// Avoid infinite redirects/invalid response
// on paths that never require authentication
if (
req.nextUrl.pathname.startsWith(basePath) ||
[signInPage, errorPage].includes(req.nextUrl.pathname)
pathname.startsWith(basePath) ||
publicPaths.some((p) => pathname.startsWith(p))
) {
return
}
@@ -119,7 +124,7 @@ async function handleMiddleware(
`\nhttps://next-auth.js.org/errors#no_secret`
)
const errorUrl = new URL(errorPage, req.nextUrl.origin)
const errorUrl = new URL(errorPage, origin)
errorUrl.searchParams.append("error", "Configuration")
return NextResponse.redirect(errorUrl)
@@ -139,11 +144,8 @@ async function handleMiddleware(
if (isAuthorized) return await onSuccess?.(token)
// the user is not logged in, redirect to the sign-in page
const signInUrl = new URL(signInPage, req.nextUrl.origin)
signInUrl.searchParams.append(
"callbackUrl",
`${req.nextUrl.pathname}${req.nextUrl.search}`
)
const signInUrl = new URL(signInPage, origin)
signInUrl.searchParams.append("callbackUrl", `${pathname}${search}`)
return NextResponse.redirect(signInUrl)
}
@@ -178,7 +180,7 @@ export type WithAuthArgs =
* ```
*
* ---
* [Documentation](https://next-auth.js.org/getting-started/middleware)
* [Documentation](https://next-auth.js.org/configuration/nextjs#middleware)
*/
export function withAuth(...args: WithAuthArgs) {
if (!args.length || args[0] instanceof NextRequest) {

View File

@@ -46,6 +46,21 @@ export interface EmailConfig extends CommonProviderOptions {
generateVerificationToken?: () => Awaitable<string>
/** If defined, it is used to hash the verification token when saving to the database . */
secret?: string
/**
* Normalizes the user input before sending the verification request.
*
* ⚠️ Always make sure this method returns a single email address.
*
* @note Technically, the part of the email address local mailbox element
* (everything before the `@` symbol) should be treated as 'case sensitive'
* according to RFC 2821, but in practice this causes more problems than
* it solves, e.g.: when looking up users by e-mail from databases.
* By default, we treat email addresses as all lower case,
* but you can override this function to change this behavior.
*
* [Documentation](https://next-auth.js.org/providers/email#normalizing-the-e-mail-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax)
*/
normalizeIdentifier?: (identifier: string) => string
options: EmailUserConfig
}
@@ -79,7 +94,7 @@ export default function Email(options: EmailUserConfig): EmailConfig {
})
const failed = result.rejected.concat(result.pending).filter(Boolean)
if (failed.length) {
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`)
throw new Error(`Email (${failed.join(", ")}) could not be sent`)
}
},
options,

View File

@@ -0,0 +1,167 @@
import { createCSRF, handler, mockAdapter } from "./lib"
import EmailProvider from "../src/providers/email"
it("Send e-mail to the only address correctly", async () => {
const { secret, csrf } = await createCSRF()
const sendVerificationRequest = jest.fn()
const signIn = jest.fn(() => true)
const email = "email@example.com"
const { res } = await handler(
{
adapter: mockAdapter(),
providers: [EmailProvider({ sendVerificationRequest })],
callbacks: { signIn },
secret,
},
{
path: "signin/email",
requestInit: {
method: "POST",
headers: { cookie: csrf.cookie },
body: JSON.stringify({ email: email, csrfToken: csrf.value }),
},
}
)
expect(res.redirect).toBe(
"http://localhost:3000/api/auth/verify-request?provider=email&type=email"
)
expect(signIn).toBeCalledTimes(1)
expect(signIn).toHaveBeenCalledWith(
expect.objectContaining({
user: expect.objectContaining({ email }),
})
)
expect(sendVerificationRequest).toHaveBeenCalledWith(
expect.objectContaining({ identifier: email })
)
})
it("Send e-mail to first address only", async () => {
const { secret, csrf } = await createCSRF()
const sendVerificationRequest = jest.fn()
const signIn = jest.fn(() => true)
const firstEmail = "email@email.com"
const email = `${firstEmail},email@email2.com`
const { res } = await handler(
{
adapter: mockAdapter(),
providers: [EmailProvider({ sendVerificationRequest })],
callbacks: { signIn },
secret,
},
{
path: "signin/email",
requestInit: {
method: "POST",
headers: { cookie: csrf.cookie },
body: JSON.stringify({ email: email, csrfToken: csrf.value }),
},
}
)
expect(res.redirect).toBe(
"http://localhost:3000/api/auth/verify-request?provider=email&type=email"
)
expect(signIn).toBeCalledTimes(1)
expect(signIn).toHaveBeenCalledWith(
expect.objectContaining({
user: expect.objectContaining({ email: firstEmail }),
})
)
expect(sendVerificationRequest).toHaveBeenCalledWith(
expect.objectContaining({ identifier: firstEmail })
)
})
it("Send e-mail to address with first domain", async () => {
const { secret, csrf } = await createCSRF()
const sendVerificationRequest = jest.fn()
const signIn = jest.fn(() => true)
const firstEmail = "email@email.com"
const email = `${firstEmail},email2.com`
const { res } = await handler(
{
adapter: mockAdapter(),
providers: [EmailProvider({ sendVerificationRequest })],
callbacks: { signIn },
secret,
},
{
path: "signin/email",
requestInit: {
method: "POST",
headers: { cookie: csrf.cookie },
body: JSON.stringify({ email: email, csrfToken: csrf.value }),
},
}
)
expect(res.redirect).toBe(
"http://localhost:3000/api/auth/verify-request?provider=email&type=email"
)
expect(signIn).toBeCalledTimes(1)
expect(signIn).toHaveBeenCalledWith(
expect.objectContaining({
user: expect.objectContaining({ email: firstEmail }),
})
)
expect(sendVerificationRequest).toHaveBeenCalledWith(
expect.objectContaining({ identifier: firstEmail })
)
})
it("Redirect to error page if multiple addresses aren't allowed", async () => {
const { secret, csrf } = await createCSRF()
const sendVerificationRequest = jest.fn()
const signIn = jest.fn()
const error = new Error("Only one email allowed")
const { res, log } = await handler(
{
adapter: mockAdapter(),
callbacks: { signIn },
providers: [
EmailProvider({
sendVerificationRequest,
normalizeIdentifier(identifier) {
if (identifier.split("@").length > 2) throw error
return identifier
},
}),
],
secret,
},
{
path: "signin/email",
requestInit: {
method: "POST",
headers: { cookie: csrf.cookie },
body: JSON.stringify({
email: "email@email.com,email@email2.com",
csrfToken: csrf.value,
}),
},
}
)
expect(signIn).toBeCalledTimes(0)
expect(sendVerificationRequest).toBeCalledTimes(0)
expect(log.error.mock.calls[0]).toEqual([
"SIGNIN_EMAIL_ERROR",
{ error, providerId: "email" },
])
expect(res.redirect).toBe(
"http://localhost:3000/api/auth/error?error=EmailSignin"
)
})

View File

@@ -1,4 +1,3 @@
import type { NextApiRequest } from "next"
import { MissingSecret } from "../src/core/errors"
import { unstable_getServerSession } from "../src/next"
import { mockLogger } from "./lib"

View File

@@ -1,6 +1,7 @@
import { createHash } from "crypto"
import type { LoggerInstance, NextAuthOptions } from "../src"
import { NextAuthHandler } from "../src/core"
import type { LoggerInstance, NextAuthOptions } from "../src"
import type { Adapter } from "../src/adapters"
export const mockLogger: () => LoggerInstance = () => ({
error: jest.fn(() => {}),
@@ -56,3 +57,10 @@ export function createCSRF() {
csrf: { value, token, cookie: `next-auth.csrf-token=${value}|${token}` },
}
}
export function mockAdapter(): Adapter {
return {
createVerificationToken: jest.fn(() => {}),
getUserByEmail: jest.fn(() => {}),
} as Adapter
}

View File

@@ -19,5 +19,12 @@
"next": ["node_modules/next"]
}
},
"exclude": ["./*.js", "./*.d.ts", "config", "**/__tests__", "tests"]
"exclude": [
"./*.js",
"./*.d.ts",
"config",
"**/__tests__",
"tests",
"coverage"
]
}