Skip to content

Commit 32d874c

Browse files
authored
fix(next-auth): repair red v4 CI (lint + pre-existing test drift) (#13447)
* test(next-auth): use Reflect.deleteProperty to satisfy no-dynamic-delete lint * test(next-auth): repair v4 test suite (dependency drift) The v4 test suite had been red since ~Oct 2025 due to dependency drift. Three independent root causes, all unrelated to test logic: - Pin headers-polyfill to 3.0.4 via pnpm override. @mswjs/interceptors@0.16.6 does `require("headers-polyfill/lib")`, but headers-polyfill >=3.1.x ships an `exports` map that blocks the deep import, breaking all 7 client suites. - Compile the core jest project's JSX with Preact's automatic runtime (swc.preact.config.js). The default pages are Preact components rendered by `preact-render-to-string`; the React runtime emitted React elements that render to an empty body, failing assert.test.ts. - Mock `../src/core` instead of `jest.spyOn(core, "AuthHandler")` in getServerSession.test.ts. SWC compiles named exports to non-configurable getters, so spyOn threw "Cannot redefine property". Also update the stale "Send e-mail to first address only" email test to assert that an address list with multiple `@` symbols is now rejected, matching the hardened normalizer.
1 parent fa85858 commit 32d874c

7 files changed

Lines changed: 48 additions & 28 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
"packageManager": "pnpm@7.13.3",
6464
"pnpm": {
6565
"overrides": {
66-
"undici": "5.11.0"
66+
"undici": "5.11.0",
67+
"headers-polyfill": "3.0.4"
6768
}
6869
}
6970
}

packages/next-auth/config/jest.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
const swcConfig = require("./swc.config")
2+
// The default server pages are Preact components rendered with
3+
// `preact-render-to-string`. Their JSX must be compiled with Preact's
4+
// automatic runtime; the React runtime produces React elements that
5+
// `preact-render-to-string` renders as an empty body.
6+
const swcPreactConfig = require("./swc.preact.config")
27

38
/** @type {import('jest').Config} */
49
module.exports = {
@@ -8,7 +13,7 @@ module.exports = {
813
testMatch: ["<rootDir>/tests/**/*.test.ts"],
914
rootDir: ".",
1015
transform: {
11-
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", swcConfig],
16+
"\\.(js|jsx|ts|tsx)$": ["@swc/jest", swcPreactConfig],
1217
},
1318
coveragePathIgnorePatterns: ["tests"],
1419
testEnvironment: "@edge-runtime/jest-environment",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/** @type {import("@swc/core").Config} */
2+
module.exports = {
3+
jsc: {
4+
parser: { syntax: "typescript", tsx: true },
5+
transform: {
6+
react: { runtime: "automatic", importSource: "preact" },
7+
},
8+
},
9+
}

packages/next-auth/tests/detect-origin.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ describe("detectOrigin", () => {
1515
beforeEach(() => {
1616
for (const key of ENV_KEYS) {
1717
saved[key] = process.env[key]
18-
delete process.env[key]
18+
Reflect.deleteProperty(process.env, key)
1919
}
2020
})
2121

2222
afterEach(() => {
2323
for (const key of ENV_KEYS) {
24-
if (saved[key] === undefined) delete process.env[key]
24+
if (saved[key] === undefined) Reflect.deleteProperty(process.env, key)
2525
else process.env[key] = saved[key]
2626
}
2727
})

packages/next-auth/tests/email.test.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ it("Send e-mail to the only address correctly", async () => {
4040
)
4141
})
4242

43-
it("Send e-mail to first address only", async () => {
43+
it("Reject an address list containing multiple `@` symbols", async () => {
4444
const { secret, csrf } = await createCSRF()
4545
const sendVerificationRequest = jest.fn()
4646
const signIn = jest.fn(() => true)
4747

48-
const firstEmail = "email@email.com"
49-
const email = `${firstEmail},email@email2.com`
48+
// Two real `@` symbols (one per address) must be rejected rather than
49+
// silently delivering the magic link to the first address, which would let
50+
// an attacker smuggle a second recipient past the single-`@` check.
51+
const email = "email@email.com,email@email2.com"
5052
const { res } = await handler(
5153
{
5254
adapter: mockAdapter(),
@@ -64,19 +66,10 @@ it("Send e-mail to first address only", async () => {
6466
}
6567
)
6668

69+
expect(signIn).toBeCalledTimes(0)
70+
expect(sendVerificationRequest).toBeCalledTimes(0)
6771
expect(res.redirect).toBe(
68-
"http://localhost:3000/api/auth/verify-request?provider=email&type=email"
69-
)
70-
71-
expect(signIn).toBeCalledTimes(1)
72-
expect(signIn).toHaveBeenCalledWith(
73-
expect.objectContaining({
74-
user: expect.objectContaining({ email: firstEmail }),
75-
})
76-
)
77-
78-
expect(sendVerificationRequest).toHaveBeenCalledWith(
79-
expect.objectContaining({ identifier: firstEmail })
72+
"http://localhost:3000/api/auth/error?error=EmailSignin"
8073
)
8174
})
8275

packages/next-auth/tests/getServerSession.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import { MissingSecret } from "../src/core/errors"
33
import { getServerSession } from "../src/next"
44
import { mockLogger } from "./lib"
55

6+
// `AuthHandler` is compiled to a non-configurable CommonJS export, so it cannot
7+
// be replaced with `jest.spyOn`. Mock the module instead, defaulting to the
8+
// real implementation so unrelated tests in this file are unaffected.
9+
jest.mock("../src/core", () => {
10+
const actual = jest.requireActual("../src/core")
11+
return {
12+
__esModule: true,
13+
...actual,
14+
AuthHandler: jest.fn(actual.AuthHandler),
15+
}
16+
})
17+
618
const originalWarn = console.warn
719
let logger = mockLogger()
820

@@ -61,13 +73,13 @@ describe("Treat secret correctly", () => {
6173

6274
describe("Return correct data", () => {
6375
afterEach(() => {
64-
jest.restoreAllMocks()
76+
const actual = jest.requireActual("../src/core")
77+
;(core.AuthHandler as jest.Mock).mockImplementation(actual.AuthHandler)
6578
})
6679

6780
it("Should return null if there is no session", async () => {
68-
const spy = jest.spyOn(core, "AuthHandler")
6981
// @ts-expect-error
70-
spy.mockReturnValue({ body: {} })
82+
;(core.AuthHandler as jest.Mock).mockReturnValue({ body: {} })
7183

7284
const session = await getServerSession(req, res, {
7385
providers: [],
@@ -91,9 +103,8 @@ describe("Return correct data", () => {
91103
},
92104
}
93105

94-
const spy = jest.spyOn(core, "AuthHandler")
95106
// @ts-expect-error
96-
spy.mockReturnValue(mockedResponse)
107+
;(core.AuthHandler as jest.Mock).mockReturnValue(mockedResponse)
97108

98109
const session = await getServerSession(req, res, {
99110
providers: [],

pnpm-lock.yaml

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)