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

repo-access.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.

repo-access.test.tsBlame146 lines · 1 contributor
23d1a81Claude1/**
2 * Unit tests for src/middleware/repo-access.ts#resolveRepoAccess.
3 *
4 * We stub `../db` with `mock.module` (same pattern as import-verify.test.ts)
5 * so we never touch Neon. The fake `db.select(...)` chain returns per-test
6 * configurable rows — one for the `repositories` lookup (owner check) and
7 * one for the `repo_collaborators` lookup. We inspect which table the query
8 * is `.from()`-ing to decide which row to return.
9 *
10 * See import-verify.test.ts for the reasoning behind the defensive defaults
11 * (`mock.module` registrations don't unwind across files in a single
12 * `bun test` run, so we reset state in `afterAll`).
13 */
14
15import { describe, it, expect, mock, afterAll } from "bun:test";
16
17type RepoRow = { id: string; ownerId: string } | undefined;
18type CollabRow = { role: "read" | "write" | "admin" } | undefined;
19
20let _nextRepoRow: RepoRow;
21let _nextCollabRow: CollabRow;
22// The last Drizzle table handle passed to `.from(...)` — we peek at it when
23// `.limit(1)` resolves so we can return the correct shape for this query.
24let _lastFrom: any = null;
25
26const _chain: any = {
27 from: (table: any) => {
28 _lastFrom = table;
29 return _chain;
30 },
31 leftJoin: () => _chain,
32 innerJoin: () => _chain,
33 rightJoin: () => _chain,
34 where: () => _chain,
35 orderBy: () => _chain,
36 groupBy: () => _chain,
37 limit: async () => {
38 // Heuristic: the schema defines tables as plain objects whose keys
39 // include the column definitions. We distinguish repositories from
40 // repo_collaborators by the presence of `ownerId` vs `acceptedAt`.
41 const t = _lastFrom;
42 if (t && typeof t === "object") {
43 if ("acceptedAt" in t || "invitedAt" in t) {
44 return _nextCollabRow ? [_nextCollabRow] : [];
45 }
46 if ("ownerId" in t || "diskPath" in t) {
47 return _nextRepoRow ? [_nextRepoRow] : [];
48 }
49 }
50 return [];
51 },
52};
53
54const _fakeDb = {
55 db: {
56 select: () => _chain,
57 insert: () => _chain,
58 update: () => _chain,
59 delete: () => _chain,
60 },
61 getDb: () => ({
62 select: () => _chain,
63 insert: () => _chain,
64 update: () => _chain,
65 delete: () => _chain,
66 }),
67};
68mock.module("../db", () => _fakeDb);
69
70afterAll(() => {
71 _nextRepoRow = undefined;
72 _nextCollabRow = undefined;
73 _lastFrom = null;
74});
75
76const REPO_ID = "11111111-1111-1111-1111-111111111111";
77const OWNER_ID = "22222222-2222-2222-2222-222222222222";
78const OTHER_USER_ID = "33333333-3333-3333-3333-333333333333";
79
80describe("resolveRepoAccess", () => {
81 it('owner returns "owner"', async () => {
82 _nextRepoRow = { id: REPO_ID, ownerId: OWNER_ID };
83 _nextCollabRow = undefined;
84 const { resolveRepoAccess } = await import("../middleware/repo-access");
85 const access = await resolveRepoAccess({
86 repoId: REPO_ID,
87 userId: OWNER_ID,
88 isPublic: false,
89 });
90 expect(access).toBe("owner");
91 });
92
93 it("collaborator with accepted invite returns their role", async () => {
94 // Viewer is NOT the owner (so the owner check falls through), but they
95 // have an accepted "write" collaborator row.
96 _nextRepoRow = { id: REPO_ID, ownerId: OWNER_ID };
97 _nextCollabRow = { role: "write" };
98 const { resolveRepoAccess } = await import("../middleware/repo-access");
99 const access = await resolveRepoAccess({
100 repoId: REPO_ID,
101 userId: OTHER_USER_ID,
102 isPublic: false,
103 });
104 expect(access).toBe("write");
105 });
106
107 it("pending invite (acceptedAt=null) does NOT grant access", async () => {
108 // The middleware filters `acceptedAt IS NOT NULL` in the WHERE clause,
109 // so a pending row would never be returned by the real DB — we simulate
110 // that by returning no collaborator row. The user should fall through
111 // to the public/private fallback; here the repo is private, so "none".
112 _nextRepoRow = { id: REPO_ID, ownerId: OWNER_ID };
113 _nextCollabRow = undefined;
114 const { resolveRepoAccess } = await import("../middleware/repo-access");
115 const access = await resolveRepoAccess({
116 repoId: REPO_ID,
117 userId: OTHER_USER_ID,
118 isPublic: false,
119 });
120 expect(access).toBe("none");
121 });
122
123 it('public repo + no collaborator row returns "read"', async () => {
124 _nextRepoRow = { id: REPO_ID, ownerId: OWNER_ID };
125 _nextCollabRow = undefined;
126 const { resolveRepoAccess } = await import("../middleware/repo-access");
127 const access = await resolveRepoAccess({
128 repoId: REPO_ID,
129 userId: OTHER_USER_ID,
130 isPublic: true,
131 });
132 expect(access).toBe("read");
133 });
134
135 it('private repo + no collaborator row returns "none"', async () => {
136 _nextRepoRow = { id: REPO_ID, ownerId: OWNER_ID };
137 _nextCollabRow = undefined;
138 const { resolveRepoAccess } = await import("../middleware/repo-access");
139 const access = await resolveRepoAccess({
140 repoId: REPO_ID,
141 userId: OTHER_USER_ID,
142 isPublic: false,
143 });
144 expect(access).toBe("none");
145 });
146});