testRegistrySignInAndStars

tcmts.cli.test.e2e.registry.testRegistrySignInAndStars · #jki7jxwe · term

Signing in to the registry, in Chrome: a visitor signs in with AT Protocol OAuth from the page (the fake network says yes at once), stars a library (a record in their own repository, which the registry checks and counts), and sees what people they follow did and what happened to the libraries they use (those in a codebase they give the registry); signed out, the filters aren't offered and the star asks them to sign in. The registry runs under /registry behind tcm serve --proxy, as tcm's own does at tcm.codes/registry: its pages, assets, API and sign-in all under the path.

import { CHROME, hydrated, withPage } from "#codebase/tcmts/cli/test/browser";
import { startFakeAtproto } from "#codebase/tcmts/cli/test/e2e/examples";
import { withChildren } from "#codebase/tcmts/cli/test/e2e/harness";
import { registryOn } from "#codebase/tcmts/cli/test/e2e/registry";
import { codebase, onFakeNetwork } from "#codebase/tcmts/cli/test/e2e/releases";
import { startServerInfo } from "#codebase/tcmts/cli/test/e2e/sync";
import assert from "node:assert/strict";
import http from "node:http";

export const testRegistrySignInAndStars = () =>
  withChildren(async (keep) => {
    // (DIDs as the network has them: a browser's OAuth client checks their form, 24 base32 letters)
    const alice = {
      did: "did:plc:aliceaaaaaaaaaaaaaaaaaaa",
      handle: "alice.test",
      password: "a-pass",
    };
    const maya = {
      did: "did:plc:mayaaaaaaaaaaaaaaaaaaaaa",
      handle: "maya.test",
      password: "m-pass",
    };
    const net = await startFakeAtproto(keep, [alice, maya]);
    const { ok } = onFakeNetwork(keep, net);
    const geo = codebase("registry sign-in geo");
    geo.save(
      `// tcm: namespace geo\nexport const manifest = { name: "geo", description: "Shapes." };\nexport const area = (side: number) => side * side;\n`,
      "add",
    );
    const source = (await startServerInfo(keep, geo.dir, "tok", "--public")).url;
    await ok(
      geo.dir,
      "release",
      "geo@1.0.0",
      "--announce",
      "--as",
      "alice.test",
      "--source",
      source,
    );
    // maya follows alice, and her codebase holds geo, added from alice's announcement
    net.seed(maya.did, "app.bsky.graph.follow", "f1", {
      subject: alice.did,
      createdAt: new Date().toISOString(),
    });
    const mine = codebase("registry sign-in maya");
    mine.save(`// tcm: namespace app\nexport const total = () => 1;\n`, "add");
    await ok(mine.dir, "lib", "add", "@alice.test/geo@1.0.0");
    const mineUrl = (await startServerInfo(keep, mine.dir, "tok", "--public")).url;

    const registry = await registryOn(keep, net, { base: "/registry" });
    await registry.until("the release to be mirrored", async () =>
      /class="ev"/.test(await registry.page("/")),
    );
    // signed out: no filters of one's own, no stars yet
    assert.doesNotMatch(await registry.page("/"), /People I follow/);
    assert.match(await registry.page("/?show=following"), /Sign in to see/);
    assert.doesNotMatch(await registry.page("/libraries"), /★/);
    // (the registry's OAuth client metadata, for a registry on a domain)
    const client = (await (await fetch(`${registry.url}/api/oauth/client`)).json()) as Record<
      string,
      unknown
    >;
    assert.equal(client.client_id, `${registry.url}/api/oauth/client`);
    assert.equal(client.scope, "atproto repo:codes.tcm.star");
    // behind a proxy that terminates TLS (Railway's, then tcm serve's), it names the address visitors asked: the
    // Host they asked, https as the first proxy says, and the path
    const asked = (url: string, headers: Record<string, string>) =>
      new Promise<Record<string, unknown>>((resolve, reject) =>
        http
          .get(url, { headers }, (r) => {
            let body = "";
            r.setEncoding("utf8")
              .on("data", (d: string) => (body += d))
              .on("end", () => resolve(JSON.parse(body) as Record<string, unknown>));
          })
          .on("error", reject),
      );
    const behind = await asked(`${registry.url}/api/oauth/client`, {
      host: "tcm.example",
      "x-forwarded-proto": "https",
    });
    assert.equal(behind.client_id, "https://tcm.example/registry/api/oauth/client");
    assert.deepEqual(behind.redirect_uris, ["https://tcm.example/registry/"]);
    assert.equal(behind.client_uri, "https://tcm.example/registry/");
    // (and as a proxy that says the host, rather than passing it, does)
    const said = await asked(`${registry.own}/api/oauth/client`, {
      "x-forwarded-host": "tcm.example",
      "x-forwarded-proto": "https",
    });
    assert.equal(said.client_id, "https://tcm.example/registry/api/oauth/client");
    // its pages' links and assets are under the path
    const home = await registry.page("/");
    assert.match(home, /href="\/registry\/libraries"/);
    assert.match(home, /src="\/registry\/_app\/assets\//);
    assert.doesNotMatch(home, /(href|src)="\/(?!registry\/)[^"]/);
    if (!CHROME) return console.log("skipped the browser: no Chrome");

    await withPage(`${registry.url}/@alice.test/geo`, async (p) => {
      const text = (sel: string) =>
        p.evaluate<string>(`document.querySelector(${JSON.stringify(sel)})?.textContent ?? ""`);
      const waitText = async (sel: string, re: RegExp, what: string) => {
        const got = await p.waitFor<string>(
          `document.querySelector(${JSON.stringify(sel)})?.textContent ?? ""`,
          (t) => re.test(t),
          30_000,
        );
        if (!re.test(got))
          assert.fail(
            `${what}: ${JSON.stringify(got)} at ${await p.evaluate<string>("location.href")} (${net.oauth.signIns} sign-in(s))\npage errors: ${p.errors.join("\n")}\npage said: ${p.logs.join("\n")}\nregistry:\n${registry.out().slice(-3000)}`,
          );
      };
      const waitLabel = async (sel: string, re: RegExp, what: string) => {
        const got = await p.waitFor<string>(
          `document.querySelector(${JSON.stringify(sel)})?.getAttribute("aria-label") ?? ""`,
          (t) => re.test(t),
          30_000,
        );
        if (!re.test(got))
          assert.fail(`${what}: ${JSON.stringify(got)}\npage errors: ${p.errors.join("\n")}`);
      };
      const press = (sel: string) =>
        p.evaluate(
          `(() => { const el = document.querySelector(${JSON.stringify(sel)}); el?.click(); return !!el; })()`,
        );
      // (as typing does: React reads the value its input event carries)
      const fill = (sel: string, value: string) =>
        p.evaluate(
          `(() => { const el = document.querySelector(${JSON.stringify(sel)}); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set.call(el, ${JSON.stringify(value)}); el.dispatchEvent(new Event("input", { bubbles: true })); return true; })()`,
        );
      const load = async (path: string) => {
        const at = `/registry${path}`;
        await p.evaluate(`location.assign(${JSON.stringify(at)})`);
        await p.waitFor<string>("location.pathname + location.search", (v) => v === at, 30_000);
        await hydrated(p);
      };
      await hydrated(p);

      // starring signed out asks to sign in
      await press(".star button");
      await waitText(".toasts", /Sign in to star it/, "a star, signed out");

      // signing in: a handle, the network's sign-in page (which says yes), and back to the page it started from
      // (one more than announcing's)
      const before = net.oauth.signIns;
      await press(".account button");
      await waitText(".signin-pop form", /Continue/, "the sign-in dialog");
      await fill(".signin-pop input", "maya.test");
      await press(".signin-pop button[type=submit]");
      await waitLabel(".account .avatar-btn", /Signed in as @maya\.test/, "signed in");
      assert.equal(await p.evaluate("location.pathname"), "/registry/@alice.test/geo");
      assert.equal(net.oauth.signIns, before + 1);

      // a star: written to her repository, checked by the registry, counted
      await waitText(".star", /☆ Star\s*0/, "no stars yet");
      await press(".star button");
      await waitText(".star", /★ Starred\s*1/, "starred");
      assert.ok(net.records.has(`at://${maya.did}/codes.tcm.star/${alice.did}:geo`));
      await load("/@alice.test/geo");
      await waitText(".star", /★ Starred\s*1/, "starred, as the registry renders it for her");

      // people she follows: alice's release
      await load("/?show=following");
      assert.equal(await p.evaluate("document.querySelectorAll('.ev').length"), 1);
      assert.match(await text(".ev"), /@alice\.test.*released/);
      // libraries she uses: none she's announced; then the ones her codebase holds
      await load("/?show=mine");
      assert.equal(await p.evaluate("document.querySelectorAll('.ev').length"), 0);
      await fill(".codebaseform input", mineUrl);
      await press(".codebaseform button");
      await p.waitFor<number>("document.querySelectorAll('.ev').length", (n) => n === 1, 30_000);
      assert.match(await text(".ev"), /geo/);

      // taking the star back
      await load("/@alice.test/geo");
      await press(".star button");
      await waitText(".star", /☆ Star\s*0/, "unstarred");

      // signing out
      await press(".account button");
      await waitText(".menu-pop .signout", /Sign out/, "the account menu");
      await press(".menu-pop .signout");
      await waitText(".account", /^Sign in$/, "signed out");
      assert.deepEqual(p.errors, []);
    });
    // what anyone sees: the star she took back isn't counted
    assert.doesNotMatch(await registry.page("/libraries"), /★/);
  })

uses