feat: add Go password session lifecycle
This commit is contained in:
1 parent
772795e7eb
commit
d0207fcebe
19 files changed
+2483
-16
No files matched your search
@@ -0,0 +1,84 @@
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user