testContributorsAndPublicRead
a public server: anyone pulls and reads; a contributor's token pushes and proposes only branches under their handle, signed as them; only the maintainer's merges, and removing a contributor stops their token
import { codebase, LIB, startServer } from "#codebase/tcmts/cli/test/e2e/contributions"; import { tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import assert from "node:assert/strict"; export const testContributorsAndPublicRead = () => withChildren(async (keep) => { const root = tempDir("contributions"); const server = codebase(root, "server"); server.write(LIB); server.tcm("add"); const url = await startServer(keep, server.dir, "tok", ["--public"]); // the maintainer adds a contributor from their own machine const owner = codebase(root, "owner"); owner.tcm("remote", "add", "origin", url, "--token", "tok"); owner.tcm("pull"); assert.match(owner.tcm("user"), /you're origin's maintainer\n {2}no contributors yet/); const added = owner.tcm("user", "add", "grace"); assert.match(added, /added @grace on origin: they push branches under @grace\//); const token = /their token \(shown once[^)]*\): (\S+)/.exec(added)![1]!; assert.ok(added.includes(`they connect with: tcm remote add origin ${url} --token ${token}`)); assert.match(owner.tcm("user"), /@grace {2}added \d{4}-\d\d-\d\d/); assert.match(owner.run("user", "add", "not a handle").out, /isn't a handle/); // anyone can pull and read; writing takes a token const reader = codebase(root, "reader"); reader.tcm("remote", "add", "origin", url); reader.tcm("pull"); assert.equal(reader.tcm("run", "lib.square", "3").trim(), "9"); assert.match( reader.tcm("user"), /you have no token for origin: you can pull and read contributions/, ); reader.tcm("branch", "mine"); reader.tcm("switch", "mine"); reader.write(`// tcm: namespace lib\nexport const half = (x: number): number => x / 2;\n`); reader.tcm("add"); assert.match(reader.run("push").out, /needs a token: anyone can pull from this server/); // a contributor's branches are under their handle, and what they write is signed by their token const grace = codebase(root, "grace"); grace.tcm("remote", "add", "origin", url, "--token", token); grace.tcm("pull"); assert.match(grace.tcm("user"), /you're @grace on origin/); grace.tcm("branch", "cubes"); grace.tcm("switch", "cubes"); grace.write(`// tcm: namespace lib\nexport const cube = (x: number): number => x ** 3;\n`); grace.tcm("add"); assert.match( grace.tcm("contribute", "--title", "Cubes"), /pushed origin\/@grace\/cubes[\s\S]*opened contribution #1 on origin/, ); assert.match( grace.tcm("contribution"), /#1 +open +Cubes +@grace\/cubes → main · @grace/, "signed @grace, not $TCM_AUTHOR", ); grace.tcm("contribution", "comment", "1", "Ready."); assert.match(grace.tcm("contribution", "view", "1"), /@grace [^\n]*\n {4}Ready\./); assert.match( reader.tcm("contribution", "view", "1"), /#1 Cubes[\s\S]*\+ lib\.cube/, "anyone reads a contribution", ); assert.match( grace.tcm("push", "--as", "spare"), /pushed origin\/@grace\/spare/, "--as names a branch under the handle too", ); // the server refuses any other branch, whatever a client asks const head = ( await ( await fetch(`${url}/branch`, { method: "POST", body: JSON.stringify({ name: "@grace/cubes" }), }) ).json() ).branch.head; for (const name of ["main", "@ada/cubes"]) { const res = await fetch(`${url}/set-branch`, { method: "POST", headers: { authorization: `Bearer ${token}` }, body: JSON.stringify({ name, head, expect: null }), }); assert.equal(res.status, 403); assert.match( (await res.json()).error, new RegExp(`@grace can't push ${name}: contributors push branches under @grace/`), ); } assert.match( grace.run("contribution", "merge", "1").out, /@grace can't merge-contribution: only the server's maintainer can/, ); // pushing again goes to the same branch, and so updates the contribution grace.write(`// tcm: namespace lib\nexport const cube = (x: number): number => x * x * x;\n`); grace.tcm("update"); assert.match(grace.tcm("push"), /pushed origin\/@grace\/cubes/); // the maintainer merges it; removing the contributor stops their token assert.match(owner.tcm("contribution", "merge", "1"), /merged #1 on origin/); assert.equal(server.tcm("run", "lib.cube", "2").trim(), "8"); assert.match( owner.tcm("user", "rm", "grace"), /removed @grace from origin: their token no longer works/, ); assert.match(grace.run("contribution").out, /missing or wrong token/); // the same codebase served without --public reads to no one without a token const read = (base: string, route: string) => fetch(`${base}/${route}`, { method: "POST", body: "{}" }); assert.equal((await read(url, "contributions")).status, 200); assert.equal((await read(url, "put-namespaces")).status, 401); const closed = await startServer(keep, server.dir, "tok"); assert.equal((await read(closed, "contributions")).status, 401); })