返回 CodeWhale
admin-login-integration.test.ts
根目录 / web / lib / admin-login-integration.test.ts
1 import { beforeEach, describe, expect, it, vi } from "vitest";
2
3 const mocks = vi.hoisted(() => ({ getAgentEnv: vi.fn(), limit: vi.fn() }));
4 vi.mock("@/lib/community-agent", async (importOriginal) => {
5 const actual = await importOriginal<typeof import("@/lib/community-agent")>();
6 return {
7 ...actual,
8 getAgentEnv: mocks.getAgentEnv,
9 safeEqual: vi.fn(actual.safeEqual),
10 createSession: vi.fn(actual.createSession),
11 };
12 });
13
14 import { POST } from "@/app/api/admin/login/route";
15 import { createSession, safeEqual, validateSession } from "@/lib/community-agent";
16
17 const FIXTURE_TOKEN = "local-admin-integration-fixture";
18 const sessions = new Map<string, string>();
19 const kv = {
20 put: vi.fn(async (key: string, value: string) => { sessions.set(key, value); }),
21 get: vi.fn(async (key: string) => sessions.get(key) ?? null),
22 };
23 const sessionKv = kv as unknown as NonNullable<Parameters<typeof createSession>[0]>;
24
25 function request(token = FIXTURE_TOKEN, ip = "192.0.2.1") {
26 return new Request("https://admin.example.test/api/admin/login?locale=en", {
27 method: "POST",
28 headers: {
29 "Content-Type": "application/x-www-form-urlencoded",
30 "CF-Connecting-IP": ip,
31 },
32 body: new URLSearchParams({ token }),
33 });
34 }
35
36 beforeEach(() => {
37 vi.clearAllMocks();
38 sessions.clear();
39 const attempts = new Map<string, number>();
40 mocks.limit.mockImplementation(async ({ key }: { key: string }) => {
41 const count = (attempts.get(key) ?? 0) + 1;
42 attempts.set(key, count);
43 return { success: count <= 5 };
44 });
45 mocks.getAgentEnv.mockResolvedValue({
46 MAINTAINER_TOKEN: FIXTURE_TOKEN,
47 ADMIN_LOGIN_LIMITER: { limit: mocks.limit },
48 CURATED_KV: sessionKv,
49 });
50 });
51
52 describe("admin login with real credential and session helpers", () => {
53 it("issues a cookie for a stored session that the real validator accepts", async () => {
54 const response = await POST(request());
55 expect(response.status).toBe(303);
56 expect(response.headers.get("Location")).toBe("https://admin.example.test/en/admin");
57 const sid = response.cookies.get("mt_sid")?.value;
58 expect(sid).toMatch(/^[A-Za-z0-9_-]{43}$/);
59 expect(sessions.size).toBe(1);
60 expect(await validateSession(sessionKv, sid)).toBe(true);
61 expect(safeEqual).toHaveBeenCalledExactlyOnceWith(FIXTURE_TOKEN, FIXTURE_TOKEN);
62 expect(createSession).toHaveBeenCalledExactlyOnceWith(sessionKv);
63 const cookie = response.headers.get("Set-Cookie")?.toLowerCase();
64 expect(cookie).toContain("httponly");
65 expect(cookie).toContain("secure");
66 expect(cookie).toContain("samesite=strict");
67 expect(cookie).toContain("max-age=86400");
68 expect(kv.put.mock.calls[0]).toEqual([
69 expect.any(String), expect.any(String), { expirationTtl: 86400 },
70 ]);
71 sessions.clear();
72 expect(await validateSession(sessionKv, sid)).toBe(false);
73 });
74
75 it("rejects a wrong credential without creating a session", async () => {
76 const response = await POST(request("wrong-local-fixture"));
77 expect(response.status).toBe(303);
78 expect(response.headers.get("Location")).toBe("https://admin.example.test/en/admin?err=1");
79 expect(response.headers.get("Set-Cookie")).toBeNull();
80 expect(safeEqual).toHaveBeenCalledExactlyOnceWith("wrong-local-fixture", FIXTURE_TOKEN);
81 expect(createSession).not.toHaveBeenCalled();
82 expect(kv.put).not.toHaveBeenCalled();
83 expect(sessions.size).toBe(0);
84 });
85
86 it("refuses a correct credential after five attempts without reading or comparing it", async () => {
87 for (let index = 0; index < 5; index++) {
88 expect((await POST(request(`wrong-${index}`, `192.0.2.${index}`))).status).toBe(303);
89 }
90 const denied = request(FIXTURE_TOKEN, "198.51.100.1");
91 const response = await POST(denied);
92 expect(response.status).toBe(429);
93 expect(response.headers.get("Retry-After")).toBe("60");
94 expect(response.headers.get("Cache-Control")).toBe("no-store");
95 expect(denied.bodyUsed).toBe(false);
96 expect(safeEqual).toHaveBeenCalledTimes(5);
97 expect(createSession).not.toHaveBeenCalled();
98 expect(kv.put).not.toHaveBeenCalled();
99 expect(sessions.size).toBe(0);
100 expect(new Set(mocks.limit.mock.calls.map(([options]) => options.key)).size).toBe(1);
101 });
102
103 it.each(["missing", "failed"])("fails closed before comparison when the limiter is %s", async (mode) => {
104 if (mode === "missing") {
105 mocks.getAgentEnv.mockResolvedValue({ MAINTAINER_TOKEN: FIXTURE_TOKEN, CURATED_KV: sessionKv });
106 } else {
107 mocks.limit.mockRejectedValue(new Error("fixture limiter unavailable"));
108 }
109 const denied = request();
110 const response = await POST(denied);
111 expect(response.status).toBe(503);
112 expect(response.headers.get("Cache-Control")).toBe("no-store");
113 expect(response.headers.get("Set-Cookie")).toBeNull();
114 expect(denied.bodyUsed).toBe(false);
115 expect(safeEqual).not.toHaveBeenCalled();
116 expect(createSession).not.toHaveBeenCalled();
117 expect(kv.put).not.toHaveBeenCalled();
118 expect(sessions.size).toBe(0);
119 });
120 });
121
121 lines TYPESCRIPT