Files
NianAIGC/tests/auth-logout-contract.test.ts

85 lines
3.2 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { afterEach, describe, expect, it, vi } from "vitest";
import * as logoutRoute from "@/app/api/auth/logout/route";
type LogoutFixture = {
version: 1;
path: string;
methods: string[];
status: number;
location: string;
requiresAuthentication: boolean;
cookieFixture: string;
duplicateBaseCookieWrite: boolean;
};
type SessionCookieFixture = {
cookie: {
chunkNames: string[];
attributes: { httpOnly: boolean; sameSite: string; path: string };
clear: { value: string; maxAgeSeconds: number };
};
};
const fixtureUrl = new URL("../contracts/auth/logout-v1.json", import.meta.url);
async function loadFixture(): Promise<LogoutFixture> {
return JSON.parse(await readFile(fixtureUrl, "utf8")) as LogoutFixture;
}
async function loadCookieFixture(relativePath: string): Promise<SessionCookieFixture> {
return JSON.parse(await readFile(new URL(`../contracts/auth/${relativePath}`, import.meta.url), "utf8")) as SessionCookieFixture;
}
function setCookieLines(response: Response): string[] {
const headers = response.headers as Headers & { getSetCookie?: () => string[] };
return headers.getSetCookie?.() ?? [response.headers.get("set-cookie") ?? ""];
}
afterEach(() => vi.unstubAllEnvs());
describe("logout HTTP v1 cross-language contract", () => {
it("allows anonymous GET and POST and returns the same 307 redirect", async () => {
const fixture = await loadFixture();
expect({ version: fixture.version, path: fixture.path, methods: fixture.methods }).toEqual({
version: 1,
path: "/api/auth/logout",
methods: ["GET", "POST"]
});
expect(Object.keys(logoutRoute).sort()).toEqual(["GET", "POST", "runtime"]);
expect(fixture.requiresAuthentication).toBe(false);
for (const method of fixture.methods) {
const response = await logoutRoute[method as "GET" | "POST"](
new Request("https://app.example.test/api/auth/logout", { method })
);
expect(response.status, method).toBe(fixture.status);
expect(response.headers.get("location"), method).toBe(fixture.location);
}
});
it("clears all 20 legacy chunk names and preserves the externally visible duplicate base write", async () => {
const fixture = await loadFixture();
const cookieFixture = await loadCookieFixture(fixture.cookieFixture);
vi.stubEnv("ZHINIAN_AUTH_COOKIE_SECURE", "true");
const response = await logoutRoute.POST(new Request("http://127.0.0.1/api/auth/logout", { method: "POST" }));
const lines = setCookieLines(response);
const names = lines.map((line) => line.slice(0, line.indexOf("=")));
expect(lines).toHaveLength(cookieFixture.cookie.chunkNames.length + (fixture.duplicateBaseCookieWrite ? 1 : 0));
expect(names).toEqual([
...cookieFixture.cookie.chunkNames,
...(fixture.duplicateBaseCookieWrite ? [cookieFixture.cookie.chunkNames[0]] : [])
]);
for (const line of lines) {
expect(line).toContain("HttpOnly");
expect(line).toContain("Path=/");
expect(line).toContain("SameSite=lax");
expect(line).toContain("Secure");
expect(line).toContain(`Max-Age=${cookieFixture.cookie.clear.maxAgeSeconds}`);
}
});
});