Blame · Line-by-line history
wiki-write-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.
| f0cd0be | 1 | /** |
| 2 | * Wiki writes require write access to the repository. | |
| 3 | * | |
| 4 | * POST /:owner/:repo/wiki (create) and POST /:owner/:repo/wiki/:slug/edit were | |
| 5 | * guarded by `requireAuth` alone — "is anyone signed in" — with no ownership | |
| 6 | * or collaborator check. Any registered account could create pages on, and | |
| 7 | * overwrite pages of, any repository's wiki. The edit path is destructive: it | |
| 8 | * UPDATEs title/body in place and bumps `revision`, replacing the owner's | |
| 9 | * content rather than appending to it. | |
| 10 | * | |
| 11 | * The private-repo gate added in app.tsx does NOT cover this: it only blocks | |
| 12 | * PRIVATE repos, and the bypass worked on public ones. | |
| 13 | * | |
| 14 | * delete and revert did check, but as `user.id !== resolved.repo.ownerId`, | |
| 15 | * which wrongly refuses collaborators and org members with legitimate write | |
| 16 | * access. All four now route through resolveRepoAccess. | |
| 17 | */ | |
| 18 | ||
| 19 | import { describe, expect, it } from "bun:test"; | |
| 20 | import { readFileSync } from "fs"; | |
| 21 | ||
| 22 | const SRC = readFileSync("src/routes/wikis.tsx", "utf8"); | |
| 23 | ||
| 24 | describe("every mutating wiki route is gated", () => { | |
| 25 | it("has a write gate at all four POST handlers", () => { | |
| 26 | const calls = SRC.match(/await denyWikiWrite\(/g) ?? []; | |
| 27 | expect(calls.length).toBe(4); | |
| 28 | }); | |
| 29 | ||
| 30 | it("no owner-only comparison survives", () => { | |
| 31 | // The over-restrictive shape that locked out collaborators. | |
| 32 | expect(SRC).not.toMatch(/user\.id !== resolved\.repo\.ownerId/); | |
| 33 | }); | |
| 34 | ||
| 35 | it("gates create before touching the form or inserting", () => { | |
| 36 | const create = SRC.slice( | |
| 37 | SRC.indexOf('wikiRoutes.post("/:owner/:repo/wiki", requireAuth'), | |
| 38 | SRC.indexOf("wikiRoutes.get(\"/:owner/:repo/wiki/:slug\", softAuth") | |
| 39 | ); | |
| 40 | const gate = create.indexOf("denyWikiWrite"); | |
| 41 | const insert = create.indexOf("db\n .insert(wikiPages)"); | |
| 42 | expect(gate).toBeGreaterThan(-1); | |
| 43 | if (insert > -1) expect(gate).toBeLessThan(insert); | |
| 44 | }); | |
| 45 | }); | |
| 46 | ||
| 47 | describe("the gate itself", () => { | |
| 48 | const helper = SRC.slice( | |
| 49 | SRC.indexOf("async function denyWikiWrite"), | |
| 50 | SRC.indexOf("async function denyWikiWrite") + 900 | |
| 51 | ); | |
| 52 | ||
| 53 | it("requires write, not merely read", () => { | |
| 54 | // A read-level check would still let any viewer of a public repo edit. | |
| 55 | expect(helper).toContain('satisfiesAccess(access, "write")'); | |
| 56 | }); | |
| 57 | ||
| 58 | it("derives visibility from the repo row rather than assuming public", () => { | |
| 59 | expect(helper).toContain("isPublic: !repo.isPrivate"); | |
| 60 | }); | |
| 61 | ||
| 62 | it("returns a redirect for the caller rather than throwing", () => { | |
| 63 | expect(helper).toContain("return c.redirect(redirectTo)"); | |
| 64 | expect(helper).toContain("return null"); | |
| 65 | }); | |
| 66 | }); |