testContributorsAndPublicRead

tcmts.cli.test.e2e.contributions.testContributorsAndPublicRead · #h5j2jvou · term

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);
  })

uses