Auto-commit 2026-04-29 16:31
This commit is contained in:
188
node_modules/next-auth/src/client/__tests__/client-provider.test.js
generated
vendored
Normal file
188
node_modules/next-auth/src/client/__tests__/client-provider.test.js
generated
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
import { rest } from "msw"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
import { printFetchCalls } from "./helpers/utils"
|
||||
import { SessionProvider, useSession, signOut, getSession } from "../../react"
|
||||
|
||||
const origDocumentVisibility = document.visibilityState
|
||||
const fetchSpy = jest.spyOn(global, "fetch")
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
changeTabVisibility(origDocumentVisibility)
|
||||
fetchSpy.mockClear()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("fetches the session once and re-uses it for different consumers", async () => {
|
||||
render(<ProviderFlow />)
|
||||
|
||||
expect(screen.getByTestId("session-1")).toHaveTextContent("loading")
|
||||
expect(screen.getByTestId("session-2")).toHaveTextContent("loading")
|
||||
|
||||
return waitFor(() => {
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"/api/auth/session",
|
||||
expect.anything()
|
||||
)
|
||||
|
||||
const session1 = screen.getByTestId("session-1").textContent
|
||||
const session2 = screen.getByTestId("session-2").textContent
|
||||
|
||||
expect(session1).toEqual(session2)
|
||||
})
|
||||
})
|
||||
|
||||
test("when there's an existing session, it won't try to fetch a new one straightaway", async () => {
|
||||
render(<ProviderFlow session={mockSession} />)
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("will refetch the session when the browser tab becomes active again", async () => {
|
||||
render(<ProviderFlow session={mockSession} />)
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Hide the current tab
|
||||
changeTabVisibility("hidden")
|
||||
|
||||
// Given the current tab got hidden, it should not attempt to re-fetch the session
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Make the tab again visible
|
||||
changeTabVisibility("visible")
|
||||
|
||||
// Given the user made the tab visible again, now attempts to sync and re-fetch the session
|
||||
return waitFor(() => {
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"/api/auth/session",
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("will refetch the session if told to do so programmatically from another window", async () => {
|
||||
render(<ProviderFlow session={mockSession} />)
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Hide the current tab
|
||||
changeTabVisibility("hidden")
|
||||
|
||||
// Given the current tab got hidden, it should not attempt to re-fetch the session
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
|
||||
// simulate sign-out triggered by another tab
|
||||
signOut({ redirect: false })
|
||||
|
||||
// Given signed out in another tab, it attempts to sync and re-fetch the session
|
||||
return waitFor(() => {
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
"/api/auth/session",
|
||||
expect.anything()
|
||||
)
|
||||
|
||||
// We should have a call to sign-out and a call to refetch the session accordingly
|
||||
expect(printFetchCalls(fetchSpy.mock.calls)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"GET /api/auth/csrf",
|
||||
"POST /api/auth/signout",
|
||||
"GET /api/auth/session",
|
||||
]
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
test("allows to customize how often the session will be re-fetched through polling", () => {
|
||||
jest.useFakeTimers()
|
||||
|
||||
render(<ProviderFlow session={mockSession} refetchInterval={1} />)
|
||||
|
||||
// we provided a mock session so it shouldn't try to fetch a new one
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
||||
expect(fetchSpy).toHaveBeenCalledWith("/api/auth/session", expect.anything())
|
||||
|
||||
jest.advanceTimersByTime(1000)
|
||||
|
||||
// it should have tried to refetch the session, hence counting 2 calls to the session endpoint
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2)
|
||||
expect(printFetchCalls(fetchSpy.mock.calls)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"GET /api/auth/session",
|
||||
"GET /api/auth/session",
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
||||
test("allows to customize the URL for session fetching", async () => {
|
||||
const myPath = "/api/v1/auth"
|
||||
|
||||
server.use(
|
||||
rest.get(`${myPath}/session`, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSession))
|
||||
)
|
||||
)
|
||||
|
||||
render(<ProviderFlow session={mockSession} basePath={myPath} />)
|
||||
|
||||
// there's an existing session so it should not try to fetch a new one
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
|
||||
// force a session refetch across all clients...
|
||||
getSession()
|
||||
|
||||
return waitFor(() => {
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
`${myPath}/session`,
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function ProviderFlow(props) {
|
||||
return (
|
||||
<SessionProvider {...props}>
|
||||
<SessionConsumer />
|
||||
<SessionConsumer testId="2" />
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionConsumer({ testId = 1, ...rest }) {
|
||||
const { data: session, status } = useSession(rest)
|
||||
|
||||
return (
|
||||
<div data-testid={`session-${testId}`}>
|
||||
{status === "loading" ? "loading" : JSON.stringify(session)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function changeTabVisibility(status) {
|
||||
const visibleStates = ["visible", "hidden"]
|
||||
|
||||
if (!visibleStates.includes(status)) return
|
||||
|
||||
Object.defineProperty(document, "visibilityState", {
|
||||
configurable: true,
|
||||
value: status,
|
||||
})
|
||||
|
||||
document.dispatchEvent(new Event("visibilitychange"))
|
||||
}
|
||||
104
node_modules/next-auth/src/client/__tests__/csrf.test.js
generated
vendored
Normal file
104
node_modules/next-auth/src/client/__tests__/csrf.test.js
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockCSRFToken } from "./helpers/mocks"
|
||||
import logger from "../../utils/logger"
|
||||
import { getCsrfToken } from "../../react"
|
||||
import { rest } from "msw"
|
||||
|
||||
jest.mock("../../utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("returns the Cross Site Request Forgery Token (CSRF Token) required to make POST requests", async () => {
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("csrf-result").textContent).toEqual(
|
||||
mockCSRFToken.csrfToken
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("when there's no CSRF token returned, it'll reflect that", async () => {
|
||||
server.use(
|
||||
rest.get("*/api/auth/csrf", (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockCSRFToken,
|
||||
csrfToken: null,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("csrf-result").textContent).toBe("null-response")
|
||||
})
|
||||
})
|
||||
|
||||
test("when the fetch fails it'll throw a client fetch error", async () => {
|
||||
server.use(
|
||||
rest.get("*/api/auth/csrf", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.text("some error happened"))
|
||||
)
|
||||
)
|
||||
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
|
||||
url: "/api/auth/csrf",
|
||||
error: new SyntaxError("Unexpected token s in JSON at position 0"),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function CSRFFlow() {
|
||||
const [response, setResponse] = useState()
|
||||
|
||||
async function handleCSRF() {
|
||||
const result = await getCsrfToken()
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="csrf-result">
|
||||
{response === null ? "null-response" : response || "no response"}
|
||||
</p>
|
||||
<button onClick={handleCSRF}>Get CSRF</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
90
node_modules/next-auth/src/client/__tests__/helpers/mocks.js
generated
vendored
Normal file
90
node_modules/next-auth/src/client/__tests__/helpers/mocks.js
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
import { setupServer } from "msw/node"
|
||||
import { rest } from "msw"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
export const mockSession = {
|
||||
ok: true,
|
||||
user: {
|
||||
image: null,
|
||||
name: "John",
|
||||
email: "john@email.com",
|
||||
},
|
||||
expires: 123213139,
|
||||
}
|
||||
|
||||
export const mockProviders = {
|
||||
ok: true,
|
||||
github: {
|
||||
id: "github",
|
||||
name: "Github",
|
||||
type: "oauth",
|
||||
signinUrl: "path/to/signin",
|
||||
callbackUrl: "path/to/callback",
|
||||
},
|
||||
credentials: {
|
||||
id: "credentials",
|
||||
name: "Credentials",
|
||||
type: "credentials",
|
||||
authorize: null,
|
||||
credentials: null,
|
||||
},
|
||||
email: {
|
||||
id: "email",
|
||||
type: "email",
|
||||
name: "Email",
|
||||
},
|
||||
}
|
||||
|
||||
export const mockCSRFToken = {
|
||||
ok: true,
|
||||
csrfToken: randomBytes(32).toString("hex"),
|
||||
}
|
||||
|
||||
export const mockGithubResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/github/url",
|
||||
}
|
||||
|
||||
export const mockCredentialsResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/credentials/url",
|
||||
}
|
||||
|
||||
export const mockEmailResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/email/url",
|
||||
}
|
||||
|
||||
export const mockSignOutResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/signout/url",
|
||||
}
|
||||
|
||||
export const server = setupServer(
|
||||
rest.post("*/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSignOutResponse))
|
||||
),
|
||||
rest.get("*/api/auth/session", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSession))
|
||||
),
|
||||
rest.get("*/api/auth/csrf", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCSRFToken))
|
||||
),
|
||||
rest.get("*/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockProviders))
|
||||
),
|
||||
rest.post("*/api/auth/signin/github", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
),
|
||||
rest.post("*/api/auth/callback/credentials", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCredentialsResponse))
|
||||
),
|
||||
rest.post("*/api/auth/signin/email", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockEmailResponse))
|
||||
),
|
||||
rest.post("*/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
|
||||
)
|
||||
14
node_modules/next-auth/src/client/__tests__/helpers/utils.js
generated
vendored
Normal file
14
node_modules/next-auth/src/client/__tests__/helpers/utils.js
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export function getBroadcastEvents() {
|
||||
return window.localStorage.setItem.mock.calls
|
||||
.filter((call) => call[0] === "nextauth.message")
|
||||
.map(([eventName, value]) => {
|
||||
const { timestamp, ...rest } = JSON.parse(value)
|
||||
return { eventName, value: rest }
|
||||
})
|
||||
}
|
||||
|
||||
export function printFetchCalls(mockCalls) {
|
||||
return mockCalls.map(([path, { method = "GET" }]) => {
|
||||
return `${method.toUpperCase()} ${path}`
|
||||
})
|
||||
}
|
||||
84
node_modules/next-auth/src/client/__tests__/providers.test.js
generated
vendored
Normal file
84
node_modules/next-auth/src/client/__tests__/providers.test.js
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockProviders } from "./helpers/mocks"
|
||||
import { getProviders } from "../../react"
|
||||
import logger from "../../utils/logger"
|
||||
import { rest } from "msw"
|
||||
|
||||
jest.mock("../../utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("when called it'll return the currently configured providers for sign in", async () => {
|
||||
render(<ProvidersFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("providers-result").textContent).toEqual(
|
||||
JSON.stringify(mockProviders)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("when failing to fetch the providers, it'll log the error", async () => {
|
||||
server.use(
|
||||
rest.get("*/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.text("some error happened"))
|
||||
)
|
||||
)
|
||||
|
||||
render(<ProvidersFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
|
||||
url: "/api/auth/providers",
|
||||
error: new SyntaxError("Unexpected token s in JSON at position 0"),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function ProvidersFlow() {
|
||||
const [response, setResponse] = useState()
|
||||
|
||||
async function handleGerProviders() {
|
||||
const result = await getProviders()
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="providers-result">
|
||||
{response === null
|
||||
? "null-response"
|
||||
: JSON.stringify(response) || "no response"}
|
||||
</p>
|
||||
<button onClick={handleGerProviders}>Get Providers</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
97
node_modules/next-auth/src/client/__tests__/session.test.js
generated
vendored
Normal file
97
node_modules/next-auth/src/client/__tests__/session.test.js
generated
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
import logger from "../../utils/logger"
|
||||
import { useState, useEffect } from "react"
|
||||
import { getSession } from "../../react"
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
jest.mock("../../utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => server.listen())
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("if it can fetch the session, it should store it in `localStorage`", async () => {
|
||||
render(<SessionFlow />)
|
||||
|
||||
// In the start, there is no session
|
||||
const noSession = await screen.findByText("No session")
|
||||
expect(noSession).toBeInTheDocument()
|
||||
|
||||
// After we fetched the session, it should have been rendered by `<SessionFlow />`
|
||||
const session = await screen.findByText(new RegExp(mockSession.user.name))
|
||||
expect(session).toBeInTheDocument()
|
||||
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "getSession",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
|
||||
test("if there's an error fetching the session, it should log it", async () => {
|
||||
server.use(
|
||||
rest.get("*/api/auth/session", (req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.body("Server error"))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SessionFlow />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
|
||||
url: "/api/auth/session",
|
||||
error: new SyntaxError("Unexpected token S in JSON at position 0"),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function SessionFlow() {
|
||||
const [session, setSession] = useState(null)
|
||||
useEffect(() => {
|
||||
async function fetchUserSession() {
|
||||
try {
|
||||
const result = await getSession()
|
||||
setSession(result)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
fetchUserSession()
|
||||
}, [])
|
||||
|
||||
if (session) return <pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
|
||||
return <p>No session</p>
|
||||
}
|
||||
290
node_modules/next-auth/src/client/__tests__/sign-in.test.js
generated
vendored
Normal file
290
node_modules/next-auth/src/client/__tests__/sign-in.test.js
generated
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import logger from "../../utils/logger"
|
||||
import {
|
||||
server,
|
||||
mockCredentialsResponse,
|
||||
mockEmailResponse,
|
||||
mockGithubResponse,
|
||||
} from "./helpers/mocks"
|
||||
import { signIn } from "../../react"
|
||||
import { rest } from "msw"
|
||||
|
||||
const { location } = window
|
||||
|
||||
jest.mock("../../utils/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
|
||||
let _href = window.location.href
|
||||
// Allows to mutate `window.location`...
|
||||
delete window.location
|
||||
|
||||
window.location = {
|
||||
reload: jest.fn(),
|
||||
}
|
||||
Object.defineProperty(window.location, "href", {
|
||||
get: () => _href,
|
||||
// whatwg-fetch or whatwg-url does not seem to work with relative URLs
|
||||
set: (href) => {
|
||||
_href = href.startsWith("/") ? `http://localhost${href}` : href
|
||||
return _href
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider, it redirects to the default sign-in page",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(
|
||||
`http://localhost/api/auth/signin?${new URLSearchParams({
|
||||
callbackUrl,
|
||||
})}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider supplied and no callback URL, redirects using the current location",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} />)
|
||||
|
||||
const callbackUrl = window.location.href
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(
|
||||
`http://localhost/api/auth/signin?${new URLSearchParams({
|
||||
callbackUrl,
|
||||
})}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | mockUrl
|
||||
${`email`} | ${mockEmailResponse.url}
|
||||
${`credentials`} | ${mockCredentialsResponse.url}
|
||||
`(
|
||||
"$provider provider redirects if `redirect` is `true`",
|
||||
async ({ provider, mockUrl }) => {
|
||||
render(<SignInFlow providerId={provider} redirect={true} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(mockUrl)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test("redirection can't be stopped using an oauth provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="github"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(mockGithubResponse.url)
|
||||
})
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'credentials' provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="credentials"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).not.toBe(mockCredentialsResponse.url)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn`
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/credentials/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'email' provider", async () => {
|
||||
render(
|
||||
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).not.toBe(mockEmailResponse.url)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn` oauth
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/email/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("*/api/auth/signin/email", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockEmailResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(mockUrlWithHash)
|
||||
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
test("params are propagated to the signin URL when supplied", async () => {
|
||||
let matchedParams = ""
|
||||
const authParams = "foo=bar&bar=foo"
|
||||
|
||||
server.use(
|
||||
rest.post("*/auth/signin/github", (req, res, ctx) => {
|
||||
matchedParams = req.url.search
|
||||
return res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(matchedParams).toEqual(`?${authParams}`)
|
||||
})
|
||||
})
|
||||
|
||||
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
|
||||
const errorMsg = "Error when retrieving providers"
|
||||
|
||||
server.use(
|
||||
rest.get("*/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json(errorMsg))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(`http://localhost/api/auth/error`)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
|
||||
error: "Error when retrieving providers",
|
||||
url: "/api/auth/providers",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function SignInFlow({
|
||||
providerId,
|
||||
callbackUrl,
|
||||
redirect = true,
|
||||
authorizationParams = {},
|
||||
}) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function handleSignIn() {
|
||||
const result = await signIn(
|
||||
providerId,
|
||||
{ callbackUrl, redirect },
|
||||
authorizationParams
|
||||
)
|
||||
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signin-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={handleSignIn}>Sign in</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
124
node_modules/next-auth/src/client/__tests__/sign-out.test.js
generated
vendored
Normal file
124
node_modules/next-auth/src/client/__tests__/sign-out.test.js
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockSignOutResponse } from "./helpers/mocks"
|
||||
import { signOut } from "../../react"
|
||||
import { rest } from "msw"
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
const { location } = window
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
// Allows to mutate `window.location`...
|
||||
delete window.location
|
||||
window.location = {
|
||||
reload: jest.fn(),
|
||||
href: location.href,
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test("by default it redirects to the current URL if the server did not provide one", async () => {
|
||||
server.use(
|
||||
rest.post("*/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(window.location.href)
|
||||
})
|
||||
})
|
||||
|
||||
test("it redirects to the URL allowed by the server", async () => {
|
||||
render(<SignOutFlow callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(mockSignOutResponse.url)
|
||||
})
|
||||
})
|
||||
|
||||
test("if url contains a hash during redirection a page reload happens", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("*/api/auth/signout", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockSignOutResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(mockUrlWithHash)
|
||||
})
|
||||
})
|
||||
|
||||
test("will broadcast the signout event to other tabs", async () => {
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "signout",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function SignOutFlow({ callbackUrl, redirect = true }) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function handleSignOut() {
|
||||
const result = await signOut({ callbackUrl, redirect })
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signout-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={handleSignOut}>Sign out</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
140
node_modules/next-auth/src/client/__tests__/use-session-hook.test.js
generated
vendored
Normal file
140
node_modules/next-auth/src/client/__tests__/use-session-hook.test.js
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
import { rest } from "msw"
|
||||
import { renderHook } from "@testing-library/react-hooks"
|
||||
import { render, waitFor } from "@testing-library/react"
|
||||
import { SessionProvider, useSession, signOut } from "../../react"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
|
||||
const origConsoleError = console.error
|
||||
const { location } = window
|
||||
|
||||
let _href = window.location.href
|
||||
beforeAll(() => {
|
||||
// Prevent noise on the terminal... `next-auth` will log to `console.error`
|
||||
// every time a request fails, which makes the tests output very noisy...
|
||||
console.error = jest.fn()
|
||||
|
||||
// Allows to mutate `window.location`...
|
||||
delete window.location
|
||||
window.location = {}
|
||||
Object.defineProperty(window.location, "href", {
|
||||
get: () => _href,
|
||||
// whatwg-fetch or whatwg-url does not seem to work with relative URLs
|
||||
set: (href) => {
|
||||
_href = href.startsWith("/") ? `http://localhost${href}` : href
|
||||
return _href
|
||||
},
|
||||
})
|
||||
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
_href = "http://localhost/"
|
||||
|
||||
// clear the internal session cache...
|
||||
signOut({ redirect: false })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
console.error = origConsoleError
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("it won't allow to fetch the session in isolation without a session context", () => {
|
||||
function App() {
|
||||
useSession()
|
||||
return null
|
||||
}
|
||||
|
||||
expect(() => render(<App />)).toThrow(
|
||||
"[next-auth]: `useSession` must be wrapped in a <SessionProvider />"
|
||||
)
|
||||
})
|
||||
|
||||
test("when fetching the session, there won't be `data` and `status` will be 'loading'", () => {
|
||||
const { result } = renderHook(() => useSession(), {
|
||||
wrapper: SessionProvider,
|
||||
})
|
||||
|
||||
expect(result.current.data).toBe(undefined)
|
||||
expect(result.current.status).toBe("loading")
|
||||
})
|
||||
|
||||
test("when session is fetched, `data` will contain the session data and `status` will be 'authenticated'", async () => {
|
||||
const { result } = renderHook(() => useSession(), {
|
||||
wrapper: SessionProvider,
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data).toEqual(mockSession)
|
||||
expect(result.current.status).toBe("authenticated")
|
||||
})
|
||||
})
|
||||
|
||||
test("when it fails to fetch the session, `data` will be null and `status` will be 'unauthenticated'", async () => {
|
||||
server.use(
|
||||
rest.get(`http://localhost/api/auth/session`, (_, res, ctx) =>
|
||||
res(ctx.status(401), ctx.json({}))
|
||||
)
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useSession(), {
|
||||
wrapper: SessionProvider,
|
||||
})
|
||||
|
||||
return waitFor(() => {
|
||||
expect(result.current.data).toEqual(null)
|
||||
expect(result.current.status).toBe("unauthenticated")
|
||||
})
|
||||
})
|
||||
|
||||
test("it'll redirect to sign-in page if the session is required and the user is not authenticated", async () => {
|
||||
server.use(
|
||||
rest.get(`http://localhost/api/auth/session`, (req, res, ctx) =>
|
||||
res(ctx.status(401), ctx.json({}))
|
||||
)
|
||||
)
|
||||
|
||||
const callbackUrl = window.location.href
|
||||
const { result } = renderHook(() => useSession({ required: true }), {
|
||||
wrapper: SessionProvider,
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data).toEqual(null)
|
||||
expect(result.current.status).toBe("loading")
|
||||
})
|
||||
|
||||
expect(window.location.href).toBe(
|
||||
`http://localhost/api/auth/signin?${new URLSearchParams({
|
||||
error: "SessionRequired",
|
||||
callbackUrl,
|
||||
})}`
|
||||
)
|
||||
})
|
||||
|
||||
test("will call custom redirect logic if supplied when the user could not authenticate", async () => {
|
||||
server.use(
|
||||
rest.get(`http://localhost/api/auth/session`, (_, res, ctx) =>
|
||||
res(ctx.status(401), ctx.json({}))
|
||||
)
|
||||
)
|
||||
|
||||
const customRedirect = jest.fn()
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useSession({ required: true, onUnauthenticated: customRedirect }),
|
||||
{
|
||||
wrapper: SessionProvider,
|
||||
}
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data).toEqual(null)
|
||||
expect(result.current.status).toBe("loading")
|
||||
})
|
||||
|
||||
expect(customRedirect).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
120
node_modules/next-auth/src/client/_utils.ts
generated
vendored
Normal file
120
node_modules/next-auth/src/client/_utils.ts
generated
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { IncomingMessage } from "http"
|
||||
import type { LoggerInstance, Session } from ".."
|
||||
|
||||
export interface AuthClientConfig {
|
||||
baseUrl: string
|
||||
basePath: string
|
||||
baseUrlServer: string
|
||||
basePathServer: string
|
||||
/** Stores last session response */
|
||||
_session?: Session | null | undefined
|
||||
/** Used for timestamp since last sycned (in seconds) */
|
||||
_lastSync: number
|
||||
/**
|
||||
* Stores the `SessionProvider`'s session update method to be able to
|
||||
* trigger session updates from places like `signIn` or `signOut`
|
||||
*/
|
||||
_getSession: (...args: any[]) => any
|
||||
}
|
||||
|
||||
export interface CtxOrReq {
|
||||
req?: Partial<IncomingMessage> & { body?: any }
|
||||
ctx?: { req: Partial<IncomingMessage> & { body?: any } }
|
||||
}
|
||||
|
||||
/**
|
||||
* If passed 'appContext' via getInitialProps() in _app.js
|
||||
* then get the req object from ctx and use that for the
|
||||
* req value to allow `fetchData` to
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
export async function fetchData<T = any>(
|
||||
path: string,
|
||||
__NEXTAUTH: AuthClientConfig,
|
||||
logger: LoggerInstance,
|
||||
{ ctx, req = ctx?.req }: CtxOrReq = {}
|
||||
): Promise<T | null> {
|
||||
const url = `${apiBaseUrl(__NEXTAUTH)}/${path}`
|
||||
try {
|
||||
const options: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(req?.headers?.cookie ? { cookie: req.headers.cookie } : {}),
|
||||
},
|
||||
}
|
||||
|
||||
if (req?.body) {
|
||||
options.body = JSON.stringify(req.body)
|
||||
options.method = "POST"
|
||||
}
|
||||
|
||||
const res = await fetch(url, options)
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw data
|
||||
return Object.keys(data).length > 0 ? data : null // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error("CLIENT_FETCH_ERROR", { error: error as Error, url })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function apiBaseUrl(__NEXTAUTH: AuthClientConfig) {
|
||||
if (typeof window === "undefined") {
|
||||
// Return absolute path when called server side
|
||||
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
|
||||
}
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
export function now() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
export interface BroadcastMessage {
|
||||
event?: "session"
|
||||
data?: { trigger?: "signout" | "getSession" }
|
||||
clientId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
|
||||
* Only not using it directly, because Safari does not support it.
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
export function BroadcastChannel(name = "nextauth.message") {
|
||||
return {
|
||||
/** Get notified by other tabs/windows. */
|
||||
receive(onReceive: (message: BroadcastMessage) => void) {
|
||||
const handler = (event: StorageEvent) => {
|
||||
if (event.key !== name) return
|
||||
const message: BroadcastMessage = JSON.parse(event.newValue ?? "{}")
|
||||
if (message?.event !== "session" || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
}
|
||||
window.addEventListener("storage", handler)
|
||||
return () => window.removeEventListener("storage", handler)
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post(message: Record<string, unknown>) {
|
||||
if (typeof window === "undefined") return
|
||||
try {
|
||||
localStorage.setItem(
|
||||
name,
|
||||
JSON.stringify({ ...message, timestamp: now() })
|
||||
)
|
||||
} catch {
|
||||
/**
|
||||
* The localStorage API isn't always available.
|
||||
* It won't work in private mode prior to Safari 11 for example.
|
||||
* Notifications are simply dropped if an error is encountered.
|
||||
*/
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user