Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
11 kB · 247 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248/** * A player's page is an address before it is a page. * * Three things have to agree about `/profile/<handle-or-did>` 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", );});