Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

review-requests.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.

review-requests.test.tsBlame135 lines · 1 contributor
3247f79Claude1/**
2 * Block J11 — PR review requests. Pure helpers + route-auth smokes.
3 */
4
5import { describe, it, expect } from "bun:test";
6import app from "../app";
7import {
8 REVIEW_SOURCES,
9 REVIEW_STATES,
10 isValidSource,
11 isValidState,
12 nextState,
13 __internal,
14} from "../lib/review-requests";
15
16describe("review-requests — isValidSource", () => {
17 it("accepts the three canonical sources", () => {
18 for (const s of REVIEW_SOURCES) expect(isValidSource(s)).toBe(true);
19 });
20
21 it("rejects unknown sources", () => {
22 expect(isValidSource("auto")).toBe(false);
23 expect(isValidSource("")).toBe(false);
24 expect(isValidSource("CODEOWNERS")).toBe(false); // case-sensitive
25 });
26});
27
28describe("review-requests — isValidState", () => {
29 it("accepts the four canonical states", () => {
30 for (const s of REVIEW_STATES) expect(isValidState(s)).toBe(true);
31 });
32
33 it("rejects unknown states", () => {
34 expect(isValidState("open")).toBe(false);
35 expect(isValidState("approved!")).toBe(false);
36 expect(isValidState("")).toBe(false);
37 });
38});
39
40describe("review-requests — nextState", () => {
41 it("dismissed is terminal — nothing moves it", () => {
42 expect(nextState("dismissed", "approved")).toBe("dismissed");
43 expect(nextState("dismissed", "changes_requested")).toBe("dismissed");
44 expect(nextState("dismissed", "commented")).toBe("dismissed");
45 expect(nextState("dismissed", "dismissed")).toBe("dismissed");
46 });
47
48 it("commented leaves state unchanged", () => {
49 expect(nextState("pending", "commented")).toBe("pending");
50 expect(nextState("approved", "commented")).toBe("approved");
51 expect(nextState("changes_requested", "commented")).toBe("changes_requested");
52 });
53
54 it("approved / changes_requested overwrite pending + each other", () => {
55 expect(nextState("pending", "approved")).toBe("approved");
56 expect(nextState("pending", "changes_requested")).toBe("changes_requested");
57 expect(nextState("approved", "changes_requested")).toBe("changes_requested");
58 expect(nextState("changes_requested", "approved")).toBe("approved");
59 });
60
61 it("dismissed outcome transitions non-dismissed to dismissed", () => {
62 expect(nextState("pending", "dismissed")).toBe("dismissed");
63 expect(nextState("approved", "dismissed")).toBe("dismissed");
64 expect(nextState("changes_requested", "dismissed")).toBe("dismissed");
65 });
66});
67
68describe("review-requests — sanitiseCandidates", () => {
69 const { sanitiseCandidates } = __internal;
70
71 it("drops nulls, undefineds, and empty strings", () => {
72 expect(sanitiseCandidates([null, undefined, "", "u1"], null)).toEqual([
73 "u1",
74 ]);
75 });
76
77 it("de-dupes preserving first-seen order", () => {
78 expect(sanitiseCandidates(["u1", "u2", "u1", "u3", "u2"], null)).toEqual([
79 "u1",
80 "u2",
81 "u3",
82 ]);
83 });
84
85 it("excludes the PR author from the result", () => {
86 expect(
87 sanitiseCandidates(["author", "u1", "author", "u2"], "author")
88 ).toEqual(["u1", "u2"]);
89 });
90
91 it("handles no-author case", () => {
92 expect(sanitiseCandidates(["u1"], null)).toEqual(["u1"]);
93 expect(sanitiseCandidates(["u1"], undefined)).toEqual(["u1"]);
94 });
95
96 it("returns [] for all-invalid input", () => {
97 expect(sanitiseCandidates([null, undefined, ""], null)).toEqual([]);
98 });
99
100 it("preserves the author if explicitly included with a different-string ID", () => {
101 // sanitiseCandidates only filters by exact ID equality
102 expect(sanitiseCandidates(["author-x"], "author")).toEqual(["author-x"]);
103 });
104});
105
106describe("review-requests — routes", () => {
107 it("POST /:o/:r/pulls/:n/reviewers requires auth (redirects unauthed)", async () => {
108 const res = await app.request(
109 "/alice/nope/pulls/1/reviewers",
110 { method: "POST", body: "username=bob" }
111 );
112 // requireAuth middleware redirects browsers to /login
113 expect([302, 401].includes(res.status)).toBe(true);
114 });
115
116 it("POST dismiss route requires auth", async () => {
117 const res = await app.request(
118 "/alice/nope/pulls/1/reviewers/x/dismiss",
119 { method: "POST" }
120 );
121 expect([302, 401].includes(res.status)).toBe(true);
122 });
123
124 it("POST with invalid bearer token → 401 JSON", async () => {
125 const res = await app.request(
126 "/alice/nope/pulls/1/reviewers",
127 {
128 method: "POST",
129 headers: { authorization: "Bearer glc_garbage" },
130 body: "username=bob",
131 }
132 );
133 expect(res.status).toBe(401);
134 });
135});