/** * A player's page is an address before it is a page. * * Three things have to agree about `/profile/` and none of * them can see the others: destinations.ts parses the address, Astro builds * one file for every address under the prefix, and a rewrite in front of the * bucket is what puts the two together. The failure is silent in both * directions - a dead link that answers 200 with the wrong page, or a page * that never loads for a handle because something upstream took it for a file. * * Run with `npm test`. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { actorFromPath, profileHref } from "../src/destinations.ts"; const src = fileURLToPath(new URL("../src/", import.meta.url)); test("the prefix has a page behind it", () => { assert.ok( existsSync(`${src}pages/profile/index.astro`), "nothing builds /profile, so every player's address is a 404", ); }); test("an address names a handle or a DID, and both survive the trip", () => { assert.equal( actorFromPath("/profile/alice.example.com"), "alice.example.com", ); assert.equal(actorFromPath("/profile/did:plc:abc123"), "did:plc:abc123"); // A browser is within its rights to send the colons encoded. assert.equal(actorFromPath("/profile/did%3Aplc%3Aabc123"), "did:plc:abc123"); // A trailing slash is the same page, not a dead end. assert.equal(actorFromPath("/profile/did:plc:abc123/"), "did:plc:abc123"); // Nobody named. assert.equal(actorFromPath("/profile/"), null); assert.equal(actorFromPath("/profile"), null); assert.equal(actorFromPath("/blog"), null); // A malformed escape is not an address, and must not throw on the way out. assert.equal(actorFromPath("/profile/%E0%A4%A"), null); }); test("the site writes the handle, and the DID where there is no handle", () => { const did = "did:plc:abc123"; // Nothing is escaped: an @ and a colon are both legal in a path segment, // and an address is written to be read. assert.equal( profileHref({ did, handle: "alice.example" }), "/profile/@alice.example", ); assert.equal(profileHref({ did, handle: null }), `/profile/${did}`); // Both forms come back out of the address they went into, and so does one // that arrived escaped from somewhere that tidied it. assert.equal( actorFromPath(profileHref({ did, handle: "alice.example" })), "@alice.example", ); assert.equal(actorFromPath(profileHref({ did, handle: null })), did); assert.equal(actorFromPath("/profile/%40alice.example"), "@alice.example"); }); test("the account menu's Your account goes to the player's own page", async () => { const account = await readFile(`${src}account.ts`, "utf8"); assert.match( account, /profileHref\(session\)/, "Your account points somewhere that is not the player's own page", ); }); // The dev server has no CloudFront in front of it, so the config carries the // same rewrite. Without it a player's page is a 404 in development and only in // development, which is the worst place for the difference to live. test("the dev server serves the shell for an address under the prefix", async () => { const config = await readFile( fileURLToPath(new URL("../astro.config.mjs", import.meta.url)), "utf8", ); // Both shells, from one table: the profile's and the record's. assert.match(config, /for \(const prefix of \["\/profile", "\/matches"\]\)/); assert.match(config, /url\.startsWith\(`\$\{prefix\}\/`\)/); assert.match(config, /req\.url = prefix/); }); test("the page reads the address rather than being built per player", async () => { const page = await readFile(`${src}pages/profile/index.astro`, "utf8"); assert.doesNotMatch( page, /getStaticPaths/, "a page per player cannot be built: there is no list of players", ); }); // The page and the card are two drawings of one claim, in two languages, in // two repositories' worth of code. Nothing links them, and the ways they can // drift are all quiet: a figure renamed on one, a figure that is a score here // and a count there, or the card saying "camo" while the page says // "patterns". Somebody reading a post and then the page it points at sees // both within a second of each other. test("the page and the card count the same things, in the same two kinds", async () => { const page = await readFile(`${src}profile-page.ts`, "utf8"); const card = await readFile( fileURLToPath(new URL("../../services/api/src/unfurl.rs", import.meta.url)), "utf8", ); // Only what is declared as a label: the blocks also name a module to // import and a figure to read, and neither is what is printed. const labels = (source, block, { pattern, closes }) => { const start = source.indexOf(block); assert.ok(start !== -1, `${block} is gone`); const end = source.indexOf(closes, start); assert.ok(end !== -1, `${block} does not close`); return [...source.slice(start, end).matchAll(pattern)].map((m) => m[1]); }; // A TypeScript array of specs, and a Rust vec! of pairs. Each ends where // its own language ends it. const written = { pattern: /label:\s*"([^"]+)"/g, closes: "\n];" }; const owned = { pattern: /"([^"]+)"\.to_owned\(\)/g, closes: "\n }" }; assert.deepEqual(labels(page, "const SCORES", written), ["Rating", "K/D"]); assert.deepEqual(labels(page, "const COUNTS", written), [ "Matches", "Forces", "Camo", ]); // The card builds its own from two functions rather than two constants. assert.deepEqual(labels(card, "fn scores(&self)", owned), ["Rating", "K/D"]); assert.deepEqual(labels(card, "fn activity(&self)", owned), [ "Matches", "Forces", "Camo", ]); }); // --- the Challenge button ------------------------------------------------- // // A press on a player's page has to survive a navigation, and sometimes a // whole OAuth round trip, before anything can act on it. The stash is the one // piece of that with logic worth asserting: peek must not spend the answer // the router is about to read, and a taken challenge must not fire twice. test("a pending challenge is peeked without being spent, and taken once", async () => { const store = new Map(); globalThis.sessionStorage = { getItem: (key) => store.get(key) ?? null, setItem: (key, value) => store.set(key, String(value)), removeItem: (key) => store.delete(key), }; const stash = await import("../src/challenge-stash.ts"); assert.equal(stash.hasPendingChallenge(), false); assert.equal(stash.takePendingChallenge(), null); stash.stashChallenge("@alice.example"); assert.equal(stash.hasPendingChallenge(), true); // Peeking twice still leaves it there: the router decides which screen to // put up from this before it decides to spend it. assert.equal(stash.hasPendingChallenge(), true); assert.equal(stash.takePendingChallenge(), "@alice.example"); assert.equal(stash.hasPendingChallenge(), false); }); test("the challenge opens a duel with them in it, not the Play screen", async () => { const router = await readFile(`${src}router.ts`, "utf8"); assert.match( router, /takePendingChallenge\(\)/, "the router never spends the stash, so a Challenge press lands on Play", ); assert.match( router, /kind: "fresh",\s*\n?\s*mode: "duel",\s*\n?\s*invite: challenged,/, "the lobby is opened without the player who was challenged", ); // The lobby seats them once and only from a snapshot that has seats: a // fresh lobby has no slots until a scenario is chosen. const lobby = await readFile(`${src}screens/lobby.ts`, "utf8"); const fn = lobby.match(/function seatTheChallenged\([\s\S]*?\n \}/); assert.ok(fn, "seatTheChallenged moved or was renamed"); assert.match(fn[0], /if \(!toSeat \|\| !seats\.length\) return;/); assert.match(fn[0], /toSeat = null;/, "a pending invite could fire twice"); assert.match(fn[0], /socket\?\.inviteHandle\(theirs\.slot/); }); // Signed out, Challenge is still a press: it opens the site's one sign-in // dialog and stashes the challenge on the way out, so the round trip to the // provider comes back into the fight rather than onto the front page. A // disabled button would be a dead end for the reader this page is most for. test("signed out, Challenge signs you in and remembers what you pressed", async () => { const page = await readFile(`${src}profile-page.ts`, "utf8"); const fn = page.match(/function challengeButton\([\s\S]*?\n\}/); assert.ok(fn, "challengeButton moved or was renamed"); assert.match( fn[0], /openSignIn\(\{ onBeforeConnect: \(\) => stashChallenge\(who\) \}\)/, ); // The session is read at press time, not at render time: this page is a // document somebody can leave open while their session ends. assert.match(fn[0], /currentSession\(\)/); // And it is absent on your own page, where the answer is nobody. assert.match(fn[0], /session\?\.did === actor\.did\) return null;/); // The other half of the round trip: the OAuth callback comes back to the // bare root with no hash, so something has to recognise the stash as a // destination or the challenge is lost on the doorstep. const router = await readFile(`${src}router.ts`, "utf8"); assert.match( router, /if \(hasPendingChallenge\(\)\) return "matches";/, "a challenge stashed before sign-in has nowhere to land on the way back", ); }); test("neither Challenge nor the follow tick is drawn about yourself", async () => { const page = await readFile(`${src}profile-page.ts`, "utf8"); const mark = page.match(/function accountMark\([\s\S]*?\n\}/); assert.ok(mark, "accountMark moved or was renamed"); // "You follow yourself" is not a fact, and there is no relationship to ask // the appview about. assert.match(mark[0], /session\.did !== actor\.did/); }); // The display face is applied through one closed list of selectors, and a // heading that is not named in it silently inherits the body face — which is // how a player's handle shipped set in the wrong family. This holds the one // heading on this page to that list. test("a player's handle is set in the site's loud face", async () => { const css = await readFile(`${src}styles.css`, "utf8"); const list = css.slice( css.indexOf(".brand-name,"), css.indexOf("font-family: var(--font-display);"), ); assert.match( list, /\.profile-name,/, "the handle is not in the display-face list, so it falls back to body text", ); });