import { afterEach, describe, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; import { GET, POST } from "@/app/api/session/route"; import { SESSION_COOKIE } from "@/lib/cookie"; import { getSession } from "@/lib/session-store"; // A fetch stub that walks the three Minnect calls. `overrides` lets a test // force a non-200 or a weird body at any step. function makeFetchStub(overrides: Partial> = {}) { return vi.fn(async (input: RequestInfo | URL): Promise => { const url = typeof input === "string" ? input : input.toString(); if (url.endsWith("/api/v4/auth/login")) { return ( overrides.login ?? new Response( JSON.stringify({ data: { accessToken: "profile-access", refreshToken: "profile-refresh", expiresIn: 3600 }, }), { status: 200, headers: { "content-type": "application/json" } }, ) ); } if (url.endsWith("/api/v4/auth/me")) { return ( overrides.me ?? new Response( JSON.stringify({ data: { userId: "user-123", email: "person@example.com", firstName: "Pat", lastName: "Rivera", profilePictureUrl: "https://cdn.example.com/pat.jpg", role: "user", }, }), { status: 200, headers: { "content-type": "application/json" } }, ) ); } if (url.endsWith("/api/v4/auth/external-services/feed")) { return ( overrides.feed ?? new Response(JSON.stringify({ accessToken: "feed-access" }), { status: 200, headers: { "content-type": "application/json" }, }) ); } throw new Error(`unexpected fetch to ${url}`); }); } function postRequest(body: unknown): NextRequest { return new NextRequest("http://localhost/api/session", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); }); describe("POST /api/session", () => { it("wrong password: 401, generic message, no cookie, no session", async () => { const serverError = { data: null, errors: [{ message: "User not found in our records", code: "UnauthorizedException" }], }; vi.stubGlobal( "fetch", makeFetchStub({ login: new Response(JSON.stringify(serverError), { status: 401, headers: { "content-type": "application/json" }, }), }), ); const res = await POST(postRequest({ email: "person@example.com", password: "wrong" })); const json = await res.json(); expect(res.status).toBe(401); expect(json).toEqual({ error: "Email or password is incorrect." }); // The Minnect message must not leak through. expect(JSON.stringify(json)).not.toContain("records"); // No cookie is set. expect(res.cookies.get(SESSION_COOKIE)).toBeUndefined(); }); it("missing fields: 400, no cookie", async () => { vi.stubGlobal("fetch", makeFetchStub()); const res = await POST(postRequest({ email: "person@example.com" })); expect(res.status).toBe(400); expect(res.cookies.get(SESSION_COOKIE)).toBeUndefined(); }); it("correct login: 200, person body, opaque session cookie, server session created", async () => { vi.stubGlobal("fetch", makeFetchStub()); const res = await POST(postRequest({ email: "person@example.com", password: "right" })); const json = await res.json(); expect(res.status).toBe(200); expect(json).toEqual({ name: "Pat Rivera", email: "person@example.com", pictureUrl: "https://cdn.example.com/pat.jpg", }); const cookie = res.cookies.get(SESSION_COOKIE); expect(cookie).toBeDefined(); expect(cookie?.httpOnly).toBe(true); expect(cookie?.sameSite).toBe("lax"); // The cookie is an opaque id, not a Minnect token. const id = cookie!.value; expect(id).not.toContain("profile-access"); expect(id).not.toContain("feed-access"); // The tokens live server-side only. const session = getSession(id); expect(session?.profileAccessToken).toBe("profile-access"); expect(session?.profileRefreshToken).toBe("profile-refresh"); expect(session?.feedAccessToken).toBe("feed-access"); }); it("feed-token exchange fails: 502, feed message, no cookie", async () => { vi.stubGlobal( "fetch", makeFetchStub({ feed: new Response(JSON.stringify({ data: null }), { status: 500, headers: { "content-type": "application/json" }, }), }), ); const res = await POST(postRequest({ email: "person@example.com", password: "right" })); const json = await res.json(); expect(res.status).toBe(502); expect(json).toEqual({ error: "Could not open the feed." }); expect(res.cookies.get(SESSION_COOKIE)).toBeUndefined(); }); }); describe("GET /api/session", () => { it("no cookie: 401 sign_in", async () => { const res = await GET(new NextRequest("http://localhost/api/session")); const json = await res.json(); expect(res.status).toBe(401); expect(json).toEqual({ error: "sign_in" }); }); it("unknown session id: 401 sign_in", async () => { const res = await GET( new NextRequest("http://localhost/api/session", { headers: { cookie: `${SESSION_COOKIE}=does-not-exist` }, }), ); const json = await res.json(); expect(res.status).toBe(401); expect(json).toEqual({ error: "sign_in" }); }); it("valid session: returns the person's name and email", async () => { vi.stubGlobal("fetch", makeFetchStub()); const postRes = await POST(postRequest({ email: "person@example.com", password: "right" })); const id = postRes.cookies.get(SESSION_COOKIE)!.value; const res = await GET( new NextRequest("http://localhost/api/session", { headers: { cookie: `${SESSION_COOKIE}=${id}` }, }), ); const json = await res.json(); expect(res.status).toBe(200); expect(json.name).toBe("Pat Rivera"); expect(json.email).toBe("person@example.com"); }); });