import assert from "node:assert/strict" import test from "node:test" import { isSameSession } from "../session-request-guard.ts" import { deferred, loadTypeScriptModule } from "../test-support/load-typescript-module.mjs" function fixture() { let session = { user: { id: 1 }, accessToken: "test-session-a" } let expirations = 0 const response = deferred() const client = loadTypeScriptModule(new URL("./client.ts", import.meta.url), { "@/lib/auth": { readSession: () => session, expireSession: () => { session = null; expirations += 1 }, }, "@/lib/session-request-guard": { isSameSession }, "@/i18n/messages": { translateCurrentMessage: (key) => key }, }, { process: { env: {} }, Headers, FormData, fetch: () => response.promise }) return { client, response, get session() { return session }, set session(next) { session = next }, get expirations() { return expirations }, } } function expiredResponse() { return new Response(JSON.stringify({ success: false, errorCode: 3002, message: "Expired" }), { status: 401, headers: { "Content-Type": "application/json" }, }) } for (const method of ["request", "requestBlob"]) { test(`${method}: old auth failure cannot expire a newer login`, async () => { const f = fixture() const pending = f.client[method]("/test") f.session = { user: { id: 2 }, accessToken: "test-session-b" } f.response.resolve(expiredResponse()) await assert.rejects(pending, { errorCode: 3002 }) assert.equal(f.session.user.id, 2) assert.equal(f.expirations, 0) }) test(`${method}: matching auth failure expires only its own session`, async () => { const f = fixture() const pending = f.client[method]("/test") f.response.resolve(expiredResponse()) await assert.rejects(pending, { errorCode: 3002 }) assert.equal(f.session, null) assert.equal(f.expirations, 1) }) test(`${method}: unauthenticated failures do not expire a dashboard session`, async () => { const f = fixture() const pending = f.client[method]("/test", { skipAuth: true }) f.response.resolve(expiredResponse()) await assert.rejects(pending, { errorCode: 3002 }) assert.equal(f.expirations, 0) }) } test("old logout and profile update cannot overwrite a new account", async () => { for (const action of ["logout", "updateProfile"]) { let session = { user: { id: 1 }, accessToken: "test-session-a" } const response = deferred() let writes = 0 const auth = loadTypeScriptModule(new URL("./auth.ts", import.meta.url), { "@/lib/auth": { readSession: () => session, clearSession: () => { session = null; writes += 1 }, writeSession: (next) => { session = next; writes += 1 }, }, "@/lib/session-request-guard": { isSameSession }, "@/lib/api/client": { request: () => response.promise }, }) const pending = auth[action]({ nickname: "Updated" }) session = { user: { id: 2 }, accessToken: "test-session-b" } response.resolve({ user: { id: 1 }, accessToken: "test-session-a" }) await pending assert.equal(session.user.id, 2) assert.equal(writes, 0) } }) for (const newerLogin of [true, false]) { test(`logout navigation honors current session (newer login: ${newerLogin})`, async () => { let session = null const pendingLogout = deferred() const redirects = [] const auth = loadTypeScriptModule(new URL("../../components/auth-provider.tsx", import.meta.url), { react: { createContext: () => ({ Provider: "context" }), startTransition: (callback) => callback(), useCallback: (callback) => callback, useEffect: () => {}, }, "react/jsx-runtime": { jsx: (type, props) => ({ type, props }) }, "next/navigation": { usePathname: () => "/dashboard", useRouter: () => ({ replace: (url) => redirects.push(url) }), }, "@/components/session-provider": { SessionProvider: "session", useSession: () => ({ session, ready: true, signOut: () => pendingLogout.promise }), }, "@/lib/auth": { readSession: () => session }, }) const dashboardElement = auth.AuthProvider({ children: null }).props.children const context = dashboardElement.type(dashboardElement.props) const signOut = context.props.value.signOut() if (newerLogin) session = { user: { id: 2 }, accessToken: "test-session-b" } pendingLogout.resolve() await signOut assert.equal(redirects.length, newerLogin ? 0 : 1) }) }