85 lines
3.2 KiB
TypeScript
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}`);
|
|
}
|
|
});
|
|
});
|