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