testRegistrySignInAndStars
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
- tcmts.cli.test.browser.CHROME
- tcmts.cli.test.browser.hydrated
- tcmts.cli.test.browser.withPage
- tcmts.cli.test.e2e.examples.startFakeAtproto
- tcmts.cli.test.e2e.harness.withChildren
- tcmts.cli.test.e2e.registry.registryOn
- tcmts.cli.test.e2e.releases.codebase
- tcmts.cli.test.e2e.releases.onFakeNetwork
- tcmts.cli.test.e2e.sync.startServerInfo