| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import type { Contributors, Profile, ProfileWorkspace, RepoPath, Result, Viewer } from "@g1t/contracts"; |
| 5 | |
| 6 | import { type CardSources, buildCard, cardHref, committedLabel, committedWithin, parseRepo } from "./hovercard.ts"; |
| 7 | |
| 8 | const NOW = Date.parse("2026-10-08T12:00:00Z"); |
| 9 | const DAY = 86_400_000; |
| 10 | |
| 11 | const ada: Profile = { |
| 12 | username: "ada", |
| 13 | name: "Ada Lovelace", |
| 14 | bio: "Engines.", |
| 15 | location: "London", |
| 16 | website: "https://ada.example", |
| 17 | pronouns: "she/her", |
| 18 | timezone: "Europe/London", |
| 19 | avatar: "cafe", |
| 20 | createdAt: "2026-01-01T00:00:00Z", |
| 21 | }; |
| 22 | |
| 23 | const me: Viewer = { id: "usr_me", username: "me", kind: "user", verified: true } as Viewer; |
| 24 | |
| 25 | /** |
| 26 | * Stand-ins for identity and repos that behave as they do: workspaces are |
| 27 | * shown only when the viewer shares them, or when ada has a public project |
| 28 | * there; the repository answers only viewers who may read it. |
| 29 | */ |
| 30 | function sources(over: Partial<CardSources> = {}): CardSources & { asked: { viewer: Viewer; publicIn: string[] }[] } { |
| 31 | const asked: { viewer: Viewer; publicIn: string[] }[] = []; |
| 32 | const memberships: ProfileWorkspace[] = [ |
| 33 | { slug: "acme", name: "Acme", avatar: null }, |
| 34 | { slug: "secret-lab", name: "Secret Lab", avatar: null }, |
| 35 | ]; |
| 36 | return { |
| 37 | asked, |
| 38 | profile: async (username) => (username === "ada" ? ada : null), |
| 39 | accountId: async (username) => (username === "ada" ? "usr_ada" : null), |
| 40 | publicNamespaces: async () => ["acme"], |
| 41 | profileWorkspaces: async (_username, viewer, publicIn) => { |
| 42 | asked.push({ viewer, publicIn }); |
| 43 | // secret-lab is private: only someone who belongs to it sees it. |
| 44 | return memberships.filter((one) => publicIn.includes(one.slug) || (viewer?.username === "insider" && one.slug === "secret-lab")); |
| 45 | }, |
| 46 | contributors: async (path: RepoPath, viewer: Viewer): Promise<Result<Contributors>> => { |
| 47 | if (path.name === "private" && viewer?.username !== "insider") { |
| 48 | return { ok: false, error: { code: "not_found", message: "Repository not found." } } as Result<Contributors>; |
| 49 | } |
| 50 | return { |
| 51 | ok: true, |
| 52 | value: { |
| 53 | head: "h", |
| 54 | commit: "h", |
| 55 | computedAt: null, |
| 56 | pending: false, |
| 57 | partial: false, |
| 58 | total: 1, |
| 59 | commits: 3, |
| 60 | weeks: [], |
| 61 | contributors: [{ kind: "user", name: "ada", username: "ada", avatar: "cafe", commits: 3, firstAt: "2026-09-01T00:00:00Z", lastAt: new Date(NOW - 3 * DAY).toISOString(), weeks: [] }], |
| 62 | }, |
| 63 | }; |
| 64 | }, |
| 65 | ...over, |
| 66 | }; |
| 67 | } |
| 68 | |
| 69 | test("the card is the public profile, signed out too", async () => { |
| 70 | const card = await buildCard("Ada", null, null, sources(), NOW); |
| 71 | assert.ok(card && card.kind === "user"); |
| 72 | assert.equal(card.username, "ada"); |
| 73 | assert.equal(card.name, "Ada Lovelace"); |
| 74 | assert.equal(card.pronouns, "she/her"); |
| 75 | assert.equal(card.bio, "Engines."); |
| 76 | assert.equal(card.location, "London"); |
| 77 | assert.equal(card.avatar, "cafe"); |
| 78 | assert.equal(card.committed, null, "no repository, nothing about commits"); |
| 79 | // Never an address. |
| 80 | assert.ok(!JSON.stringify(card).includes("@")); |
| 81 | }); |
| 82 | |
| 83 | test("a private workspace is hidden from someone outside it", async () => { |
| 84 | const outside = sources(); |
| 85 | const outsider = await buildCard("ada", me, null, outside, NOW); |
| 86 | assert.deepEqual(outsider?.kind === "user" && outsider.workspaces.map((one) => one.slug), ["acme"]); |
| 87 | // The viewer is passed through, for identity to decide. |
| 88 | assert.equal(outside.asked[0]!.viewer, me); |
| 89 | assert.deepEqual(outside.asked[0]!.publicIn, ["acme"]); |
| 90 | const insider = await buildCard("ada", { ...me, username: "insider" } as Viewer, null, sources(), NOW); |
| 91 | assert.deepEqual(insider?.kind === "user" && insider.workspaces.map((one) => one.slug), ["acme", "secret-lab"]); |
| 92 | }); |
| 93 | |
| 94 | test("commits to a repository show only to those who may read it", async () => { |
| 95 | const open = await buildCard("ada", null, { namespace: "acme", name: "web" }, sources(), NOW); |
| 96 | assert.equal(open?.kind === "user" && open.committed, "week"); |
| 97 | const hidden = await buildCard("ada", me, { namespace: "acme", name: "private" }, sources(), NOW); |
| 98 | assert.equal(hidden?.kind === "user" && hidden.committed, null); |
| 99 | }); |
| 100 | |
| 101 | test("the card carries the time zone a profile gives, and none when it gives none", async () => { |
| 102 | const card = await buildCard("ada", me, null, sources(), NOW); |
| 103 | assert.equal(card?.kind === "user" && card.timezone, "Europe/London"); |
| 104 | const without = await buildCard("ada", me, null, sources({ profile: async () => ({ ...ada, timezone: null }) }), NOW); |
| 105 | assert.equal(without?.kind === "user" && without.timezone, null); |
| 106 | }); |
| 107 | |
| 108 | test("a failing service leaves its part out, not the card", async () => { |
| 109 | const card = await buildCard( |
| 110 | "ada", |
| 111 | me, |
| 112 | { namespace: "acme", name: "web" }, |
| 113 | sources({ profileWorkspaces: async () => Promise.reject(new Error("down")), contributors: async () => Promise.reject(new Error("down")) }), |
| 114 | NOW, |
| 115 | ); |
| 116 | assert.ok(card && card.kind === "user"); |
| 117 | assert.deepEqual(card.workspaces, []); |
| 118 | assert.equal(card.committed, null); |
| 119 | }); |
| 120 | |
| 121 | test("g1t has its own card; ghost and strangers have none", async () => { |
| 122 | assert.deepEqual(await buildCard("g1t", null, null, sources(), NOW), { kind: "g1t", username: "g1t" }); |
| 123 | assert.equal(await buildCard("ghost", me, null, sources(), NOW), null); |
| 124 | assert.equal(await buildCard("nobody", me, null, sources(), NOW), null); |
| 125 | assert.equal(await buildCard("../etc", me, null, sources(), NOW), null); |
| 126 | }); |
| 127 | |
| 128 | test("workspaces past three are counted", async () => { |
| 129 | const many = Array.from({ length: 5 }, (_, index) => ({ slug: `w${index}`, name: `W${index}`, avatar: null })); |
| 130 | const card = await buildCard("ada", me, null, sources({ profileWorkspaces: async () => many }), NOW); |
| 131 | assert.ok(card && card.kind === "user"); |
| 132 | assert.equal(card.workspaces.length, 3); |
| 133 | assert.equal(card.more_workspaces, 2); |
| 134 | }); |
| 135 | |
| 136 | test("how recently, in the words the card uses", () => { |
| 137 | assert.equal(committedWithin(new Date(NOW - 2 * 3_600_000).toISOString(), NOW), "day"); |
| 138 | assert.equal(committedWithin(new Date(NOW - 6 * DAY).toISOString(), NOW), "week"); |
| 139 | assert.equal(committedWithin(new Date(NOW - 20 * DAY).toISOString(), NOW), "month"); |
| 140 | assert.equal(committedWithin(new Date(NOW - 60 * DAY).toISOString(), NOW), null); |
| 141 | assert.equal(committedWithin(null, NOW), null); |
| 142 | assert.equal(committedLabel("week"), "Committed to this repository in the past week"); |
| 143 | }); |
| 144 | |
| 145 | test("the card's address and its repository", () => { |
| 146 | assert.equal(cardHref("Ada", "acme/web"), "/-/hovercard/user/ada?repo=acme%2Fweb"); |
| 147 | assert.equal(cardHref("ada", null), "/-/hovercard/user/ada"); |
| 148 | assert.deepEqual(parseRepo("acme/web"), { namespace: "acme", name: "web" }); |
| 149 | assert.equal(parseRepo("acme"), null); |
| 150 | assert.equal(parseRepo("../x/y"), null); |
| 151 | }); |