Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

token-expiry.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

token-expiry.test.tsBlame96 lines · 1 contributor
b9b9f22ccantynz-alt1/**
2 * PAT expiration.
3 *
4 * The api_tokens.expires_at column has existed since the table was created,
5 * but nothing ever wrote to it: the create form had no expiry control and the
6 * POST handler never set the field, so every token ever issued was stored with
7 * expires_at = NULL. All three auth paths read that as "never expires", so
8 * PATs were effectively immortal and could not be rotated on a schedule.
9 *
10 * These cover the parsing rules and, crucially, that a rejected expiry never
11 * degrades into "no expiration".
12 */
13
14import { describe, expect, it } from "bun:test";
15import { parseExpiry, INVALID_EXPIRY } from "../routes/tokens";
16
17const NOW = new Date("2026-07-27T09:00:00.000Z");
18const DAY = 86_400_000;
19
20describe("parseExpiry — presets", () => {
21 for (const days of [7, 30, 60, 90]) {
22 it(`"${days}" → ${days} days from now`, () => {
23 const got = parseExpiry(String(days), "", NOW);
24 expect(got).toBeInstanceOf(Date);
25 expect((got as Date).getTime()).toBe(NOW.getTime() + days * DAY);
26 });
27 }
28
29 it('"never" → null (no expiration)', () => {
30 expect(parseExpiry("never", "", NOW)).toBeNull();
31 });
32
33 it("a blank choice defaults to 30 days, NOT to no-expiration", () => {
34 const got = parseExpiry("", "", NOW);
35 expect(got).toBeInstanceOf(Date);
36 expect((got as Date).getTime()).toBe(NOW.getTime() + 30 * DAY);
37 });
38});
39
40describe("parseExpiry — custom date", () => {
41 it("accepts a future yyyy-mm-dd and anchors to end-of-day UTC", () => {
42 const got = parseExpiry("custom", "2026-08-15", NOW);
43 expect(got).toBeInstanceOf(Date);
44 expect((got as Date).toISOString()).toBe("2026-08-15T23:59:59.999Z");
45 });
46
47 it("end-of-day anchoring keeps today's date usable, not instantly dead", () => {
48 // A user picking "today" should get the rest of the day, not a token that
49 // expired at 00:00 this morning.
50 const got = parseExpiry("custom", "2026-07-27", NOW);
51 expect(got).toBeInstanceOf(Date);
52 expect((got as Date).getTime()).toBeGreaterThan(NOW.getTime());
53 });
54
55 it("rejects a past date", () => {
56 expect(parseExpiry("custom", "2026-07-01", NOW)).toBe(INVALID_EXPIRY);
57 });
58
59 it("rejects a blank custom date", () => {
60 expect(parseExpiry("custom", "", NOW)).toBe(INVALID_EXPIRY);
61 });
62
63 it("rejects garbage", () => {
64 expect(parseExpiry("custom", "not-a-date", NOW)).toBe(INVALID_EXPIRY);
65 });
66});
67
68describe("parseExpiry — rejected input never becomes 'no expiration'", () => {
69 // The whole point of the INVALID_EXPIRY sentinel. If these returned null the
70 // caller would happily mint an immortal token from input it had rejected.
71 for (const bad of ["0", "-30", "1.5", "abc"]) {
72 it(`"${bad}" is rejected, not treated as never-expires`, () => {
73 const got = parseExpiry(bad, "", NOW);
74 expect(got).toBe(INVALID_EXPIRY);
75 expect(got).not.toBeNull();
76 });
77 }
78});
79
80describe("expiry enforcement contract", () => {
81 it("all three auth paths null-guard expiresAt before comparing", async () => {
82 // Guards against a regression where someone drops the `row.expiresAt &&`
83 // check: `new Date(null) < new Date()` is 1970 < now === true, which would
84 // instantly reject every no-expiration token in existence.
85 const { readFileSync } = await import("fs");
86 const paths = [
87 "src/middleware/api-auth.ts",
88 "src/middleware/auth.ts",
89 "src/lib/git-push-auth.ts",
90 ];
91 for (const p of paths) {
92 const src = readFileSync(p, "utf8");
93 expect(src).toContain("expiresAt &&");
94 }
95 });
96});