testTypedTables

tcmts.cli.test.e2e.data.testTypedTables · #nmoa7ctf · term

typed tables: writes are checked; changed row types are verified, migrated, or refused; old pages reload

import { call, crm, hashOf, waitFor } from "#codebase/tcmts/cli/test/e2e/data";
import { okIn, plainEnv, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";

export const testTypedTables = () =>
  withChildren(async (keep) => {
    const dir = tempDir("data");
    const tcm = (...args: string[]) => okIn(dir, ...args);
    tcm("init");
    tcm("lib", "add", "tcm", "server", "--at", "lib.server");
    crm(dir, {});
    tcm("add");
    assert.match(tcm("test", "crm"), /1 passed, 0 failed/);
    assert.match(tcm("view", "crm.users"), /runs on the server: it uses lib\.server\.table/);

    const child = keep(
      spawn(process.execPath, [tcmBin(), "start", "crm.main", "--port", "0"], {
        cwd: dir,
        env: plainEnv(),
      }),
    );
    let out = "";
    child.stdout.setEncoding("utf8").on("data", (d: string) => (out += d));
    child.stderr.setEncoding("utf8").on("data", (d: string) => (out += d));
    await waitFor(() => /url\s+http:\/\/\S+/.test(out));
    const url = /url\s+(http:\/\/\S+)/.exec(out)![1]!;

    await call(url, hashOf(dir, "crm.addUser"), ["ada", "ada@x"]);
    await call(url, hashOf(dir, "crm.addUser"), ["grace", "grace@x"]);
    const v1List = hashOf(dir, "crm.listUsers");
    assert.equal((await call(url, v1List, [])).body, `["ada","grace"]`);
    assert.equal((await call(url, hashOf(dir, "crm.badInsert"), [])).status, 500);
    await waitFor(() => /table "users": value\.email is missing/.test(out));
    assert.match(tcm("data"), /users\s+2 row\(s\)/);

    // a compatible change: the stored rows still fit, so they just work
    crm(dir, { field: `; plan?: "free" | "pro"`, value: `, plan: "pro" as const` });
    assert.match(
      tcm("update"),
      /table "users": 2 stored row\(s\) were written as an earlier type and still fit the new one/,
    );
    await waitFor(
      async () => (await call(url, hashOf(dir, "crm.listUsers"), [])).body === `["ada","grace"]`,
    );

    // an incompatible change: tcm says so at update time, and reads refuse rather than return the wrong shape
    crm(dir, {
      field: `; plan?: "free" | "pro"; active: boolean`,
      value: `, plan: "pro" as const, active: true`,
    });
    const update = tcm("update");
    assert.match(
      update,
      /table "users": 2 stored row\(s\) don't fit the new type, e\.g\. \S+: row\.active is missing/,
    );
    assert.match(update, /reading them will fail until table\(\) gets a migrate function/);
    await waitFor(async () => (await call(url, hashOf(dir, "crm.listUsers"), [])).status === 500);
    await waitFor(() =>
      /written as an older type and doesn't fit the current one \(row\.active is missing\)/.test(
        out,
      ),
    );

    // with a migration: rows are upgraded as they're read, and the result is checked too
    crm(dir, {
      field: `; plan?: "free" | "pro"; active: boolean`,
      opts: `, { migrate: (old: Omit<User, "active">) => ({ ...old, active: true }) }`,
      value: `, plan: "pro" as const, active: true`,
    });
    assert.match(
      tcm("update"),
      /migrate takes them as they.re read \(or all now: tcm data migrate users\)/,
    );
    await waitFor(
      async () => (await call(url, hashOf(dir, "crm.listUsers"), [])).body === `["ada","grace"]`,
    );
    assert.doesNotMatch(
      tcm("data"),
      /written under \d+ row types/,
      "every row now has the current row type",
    );

    // a page still on v1 reads rows written by newer code: 410, so it reloads instead of misreading them
    const stale = await call(url, v1List, []);
    assert.equal(stale.status, 410);
    assert.match(stale.body, /written by a newer version of this app; reload/);
  })

uses