1
11import { describe, expect, it } from "vitest";
12import {
13 SCOPE_IMPLIES,
14 minimalScopes,
15 grants,
16 curlExample,
17 expiryState,
18 keyStatus,
19 relativeTime,
20 shortDate,
21 expiryBounds,
22} from "../api-keys";
23
24const T0 = Date.parse("2026-01-15T12:00:00Z");
25
26describe("scope implication (the hierarchy)", () => {
27 it("ADMIN implies WRITE and READ; WRITE implies READ; READ implies only itself", () => {
28 expect(SCOPE_IMPLIES.ADMIN).toEqual(["ADMIN", "WRITE", "READ"]);
29 expect(SCOPE_IMPLIES.WRITE).toEqual(["WRITE", "READ"]);
30 expect(SCOPE_IMPLIES.READ).toEqual(["READ"]);
31 });
32
33 it("the stored set is MINIMAL — implied scopes are dropped", () => {
34
35 expect(minimalScopes(["ADMIN", "WRITE", "READ"])).toEqual(["ADMIN"]);
36 expect(minimalScopes(["WRITE", "READ"])).toEqual(["WRITE"]);
37 expect(minimalScopes(["READ"])).toEqual(["READ"]);
38
39 expect(minimalScopes(["READ", "ADMIN"])).toEqual(["ADMIN"]);
40 });
41
42 it("grants() resolves through the chain", () => {
43 expect(grants(["ADMIN"], "READ")).toBe(true);
44 expect(grants(["ADMIN"], "WRITE")).toBe(true);
45 expect(grants(["WRITE"], "READ")).toBe(true);
46 expect(grants(["READ"], "WRITE")).toBe(false);
47 expect(grants([], "READ")).toBe(false);
48 });
49});
50
51describe("the curl example generator", () => {
52 const PREFIX = "fk_live_ab12cd34";
53
54 it("carries the Bearer placeholder (masked prefix, never a raw key)", () => {
55 const ex = curlExample(PREFIX, "GET", "http://localhost:4000/api/v1/pages/p1");
56 expect(ex).toContain(`Authorization: Bearer ${PREFIX}…(your key)`);
57 expect(ex).toContain("http://localhost:4000/api/v1/pages/p1");
58 expect(ex).toContain("curl -X GET");
59 });
60
61 it("adds Content-Type + -d only for a body", () => {
62 const noBody = curlExample(PREFIX, "GET", "http://x/api/v1/pages");
63 expect(noBody).not.toContain("-d");
64 expect(noBody).not.toContain("Content-Type");
65
66 const body = curlExample(
67 PREFIX,
68 "POST",
69 "http://x/api/v1/workspaces/w1/pages",
70 '{"title":"Hi"}',
71 );
72 expect(body).toContain("Content-Type: application/json");
73 expect(body).toContain(`-d '{"title":"Hi"}'`);
74 expect(body).toContain("curl -X POST");
75 });
76
77 it("is syntactically a real command line", () => {
78 const ex = curlExample(PREFIX, "PATCH", "http://x/api/v1/pages/p1", '{"title":"x"}');
79
80 const lines = ex.split("\n");
81 expect(lines.at(-1)).toBe(" http://x/api/v1/pages/p1");
82 for (let i = 0; i < lines.length - 1; i++) {
83 expect(lines[i]?.endsWith("\\"), `line ${i} must continue`).toBe(true);
84 }
85 });
86});
87
88describe("expiry state (the red < 7 d window)", () => {
89 it("no expiry → never", () => {
90 expect(expiryState(null, T0)).toBe("never");
91 expect(expiryState(undefined, T0)).toBe("never");
92 });
93
94 it("expired when in the past", () => {
95 expect(expiryState("2026-01-01T00:00:00Z", T0)).toBe("expired");
96 });
97
98 it("soon (red) when ≤ 7 days away", () => {
99 expect(expiryState("2026-01-21T12:00:00Z", T0)).toBe("soon");
100 expect(expiryState("2026-01-16T12:00:00Z", T0)).toBe("soon");
101 expect(expiryState("2026-01-22T11:59:00Z", T0)).toBe("soon");
102 });
103
104 it("active when > 7 days away", () => {
105 expect(expiryState("2026-02-01T00:00:00Z", T0)).toBe("active");
106 });
107
108 it("keyStatus precedence: Revoked > Expired > Active", () => {
109 expect(
110 keyStatus({ revokedAt: "2026-01-01T00:00:00Z", expiresAt: "2026-01-01T00:00:00Z" }, T0),
111 ).toBe("Revoked");
112 expect(keyStatus({ revokedAt: null, expiresAt: "2026-01-01T00:00:00Z" }, T0)).toBe("Expired");
113 expect(keyStatus({ revokedAt: null, expiresAt: "2026-06-01T00:00:00Z" }, T0)).toBe("Active");
114 expect(keyStatus({ revokedAt: null, expiresAt: null }, T0)).toBe("Active");
115 });
116});
117
118describe("relative time (last used)", () => {
119 it("never / just now", () => {
120 expect(relativeTime(null, T0)).toBe("never");
121 expect(relativeTime("2026-01-15T11:59:30Z", T0)).toBe("just now");
122 });
123
124 it("minutes / hours / days", () => {
125 expect(relativeTime("2026-01-15T11:55:00Z", T0)).toBe("5 m ago");
126 expect(relativeTime("2026-01-15T09:00:00Z", T0)).toBe("3 h ago");
127 expect(relativeTime("2026-01-12T12:00:00Z", T0)).toBe("3 d ago");
128 });
129});
130
131describe("expiry bounds (the form window)", () => {
132 it("min = now + 1 h, max = now + 1 y", () => {
133 const b = expiryBounds(T0);
134 expect(new Date(b.min).getTime()).toBeCloseTo(T0 + 3_600_000, -6);
135 expect(new Date(b.max).getTime()).toBeCloseTo(T0 + 365 * 24 * 3_600_000, -6);
136 expect(b.min).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
137 });
138});
139
140describe("short date label", () => {
141 it("formats a readable date", () => {
142 const s = shortDate("2027-01-05T00:00:00Z");
143 expect(s).toMatch(/2027/);
144 expect(s).toMatch(/Jan/);
145 });
146});
147