Skip to content
215 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 filterInvitations,
6 filterMembers,
7 invitationCounts,
8 invitationFilter,
9 invitationFilterOf,
10 invitationTone,
11 inviteKind,
12 memberFilters,
13 invitePageCopy,
14 workspaceInviteCopy,
15 cleanCode,
16 cleanProof,
17 invitePath,
18 inviteSignUpCopy,
19 inviteFor,
20 inviteLink,
21 inviteState,
22 landingFor,
23 looksAutomated,
24 signUpCopy,
25 suggestUsername,
26 welcomeCookie,
27 clearWelcome,
28 welcomes,
29} from "./invites.ts";
30
31const CODE = "g1t-k7m2-q9xd-4hpw-abcd-0123-4567-89ef-ghjk";
32
33test("the Invitations page sorts every state under one filter, and counts them", () => {
34 const statuses = ["pending", "awaiting_confirmation", "awaiting_answer", "redeemed", "declined", "expired", "revoked"] as const;
35 const invites = statuses.map((status) => ({ status }));
36 assert.equal(invitationFilterOf("pending"), "pending");
37 assert.equal(invitationFilterOf("awaiting_confirmation"), "pending");
38 assert.equal(invitationFilterOf("awaiting_answer"), "pending");
39 assert.equal(invitationFilterOf("redeemed"), "accepted");
40 assert.equal(invitationFilterOf("declined"), "declined");
41 assert.equal(invitationFilterOf("expired"), "expired");
42 assert.equal(invitationFilterOf("revoked"), "revoked");
43 assert.deepEqual(invitationCounts(invites), { all: 7, pending: 3, accepted: 1, declined: 1, expired: 1, revoked: 1 });
44 assert.equal(filterInvitations(invites, "all").length, 7);
45 assert.deepEqual(filterInvitations(invites, "pending").map((invite) => invite.status), ["pending", "awaiting_confirmation", "awaiting_answer"]);
46 assert.deepEqual(filterInvitations(invites, "accepted").map((invite) => invite.status), ["redeemed"]);
47 // The query names a filter, or means All.
48 assert.equal(invitationFilter("pending"), "pending");
49 assert.equal(invitationFilter("failed"), "all");
50 assert.equal(invitationFilter(null), "all");
51 // Waiting is amber, joined green, a no red, and over is quiet.
52 assert.equal(invitationTone("awaiting_answer"), "warn");
53 assert.equal(invitationTone("redeemed"), "success");
54 assert.equal(invitationTone("revoked"), "danger");
55 assert.equal(invitationTone("expired"), "neutral");
56});
57
58test("the Members page filters by role, two-factor, team and words", () => {
59 const members = [
60 { username: "ada", name: "Ada Lovelace", role: "owner" as const, two_factor: true },
61 { username: "grace", name: "Grace Hopper", role: "member" as const, two_factor: false },
62 { username: "linus", name: null, role: "member" as const, two_factor: true },
63 ];
64 const teams = { ada: [{ slug: "core" }], grace: [{ slug: "web" }, { slug: "core" }] };
65 assert.deepEqual(memberFilters(""), { role: "all", twoFactor: "all", team: null, query: "" });
66 assert.deepEqual(memberFilters("?role=owner&2fa=off&team=Core&q=%20ada%20"), { role: "owner", twoFactor: "off", team: "core", query: "ada" });
67 assert.deepEqual(memberFilters("?role=admin&2fa=maybe"), { role: "all", twoFactor: "all", team: null, query: "" });
68 const names = (filters: string) => filterMembers(members, memberFilters(filters), teams).map((member) => member.username);
69 assert.deepEqual(names(""), ["ada", "grace", "linus"]);
70 assert.deepEqual(names("?role=member"), ["grace", "linus"]);
71 assert.deepEqual(names("?2fa=off"), ["grace"]);
72 assert.deepEqual(names("?team=core"), ["ada", "grace"]);
73 assert.deepEqual(names("?q=hopper"), ["grace"]);
74 assert.deepEqual(names("?q=@LIN"), ["linus"]);
75 assert.deepEqual(names("?role=owner&team=web"), []);
76});
77
78test("everyone is offered a plain sign-up", () => {
79 assert.deepEqual(signUpCopy(), { primary: "Sign up", secondary: null });
80});
81
82test("an invite link is on g1t.sh unless told otherwise", () => {
83 assert.equal(inviteLink(CODE), `https://g1t.sh/invite/${CODE}`);
84 assert.equal(inviteLink(CODE, "http://localhost:8787/"), `http://localhost:8787/invite/${CODE}`);
85});
86
87const PROOF = "4f9c2a7e0b13d5c84f9c2a7e0b13d5c84f9c2a7e0b13d5c84f9c2a7e0b13d5c8";
88
89test("an invite email's proof is kept only when it looks like one, and goes along to the invite's page", () => {
90 assert.equal(cleanProof(PROOF), PROOF);
91 assert.equal(cleanProof(` ${PROOF.toUpperCase()} `), PROOF);
92 assert.equal(cleanProof("not-a-proof"), null);
93 assert.equal(cleanProof("abc"), null);
94 assert.equal(cleanProof("a".repeat(500)), null);
95 assert.equal(cleanProof(null), null);
96 assert.equal(invitePath(CODE, PROOF), `/invite/${CODE}?proof=${PROOF}`);
97 assert.equal(invitePath(CODE, null), `/invite/${CODE}`);
98 assert.equal(invitePath(CODE), `/invite/${CODE}`);
99});
100
101test("signing up from the invite email says the address is confirmed already; otherwise the code step applies", () => {
102 const base = { address: "ada@example.com", emailProven: false, workspace: { name: "Flagon, Inc." }, repository: null };
103 const proven = inviteSignUpCopy({ ...base, emailProven: true });
104 assert.equal(proven.intro, "You can join Flagon, Inc. as soon as you create it: accept the invitation then.");
105 assert.match(proven.confirmed ?? "", /^ada@example\.com is confirmed: you came here from the invite we emailed to it/);
106 assert.match(proven.hint, /confirmed already/);
107 assert.doesNotMatch(proven.hint, /code/);
108
109 // No proof (a code typed in, or a link passed on): nothing new is said.
110 const plain = inviteSignUpCopy(base);
111 assert.equal(plain.intro, "You can join Flagon, Inc. as soon as you confirm your email: accept the invitation then.");
112 assert.equal(plain.confirmed, null);
113 assert.equal(plain.hint, "Your invite was sent here. We email it a code to confirm it before you start.");
114
115 // An invite for anyone with the code has no address to prove.
116 const open = inviteSignUpCopy({ ...base, address: null, emailProven: true, workspace: null });
117 assert.equal(open.confirmed, null);
118 assert.equal(open.intro, "It takes a minute.");
119 assert.equal(open.hint, "We email it a code to confirm it before you start.");
120
121 const repo = inviteSignUpCopy({ ...base, workspace: null, repository: { name: "flagon-io/g1t" }, emailProven: true });
122 assert.equal(repo.intro, "You get flagon-io/g1t as soon as you create it.");
123});
124
125test("a pasted link or code is tidied to the code", () => {
126 assert.equal(cleanCode(CODE), CODE);
127 assert.equal(cleanCode(` ${CODE} `), CODE);
128 assert.equal(cleanCode(`https://g1t.sh/invite/${CODE}`), CODE);
129 assert.equal(cleanCode(`https://g1t.sh/register?invite=${CODE}&next=%2F`), CODE);
130 assert.equal(cleanCode("g1t-k7m2 q9xd"), "g1t-k7m2q9xd");
131 assert.equal(cleanCode(null), "");
132 assert.equal(cleanCode("x".repeat(500)).length, 80);
133});
134
135test("each invite says where it stands and whom it is for", () => {
136 const base = { redeemedBy: null, email: null, workspace: null };
137 assert.deepEqual(inviteState({ ...base, status: "pending" }), { label: "Pending", tone: "pending" });
138 assert.deepEqual(inviteState({ ...base, status: "redeemed", redeemedBy: "ada" }), { label: "Joined as @ada", tone: "done" });
139 assert.deepEqual(inviteState({ ...base, status: "awaiting_confirmation", redeemedBy: "ada" }), {
140 label: "@ada is confirming their email",
141 tone: "pending",
142 });
143 assert.deepEqual(inviteState({ ...base, status: "expired" }), { label: "Expired", tone: "dead" });
144 assert.deepEqual(inviteState({ ...base, status: "revoked" }), { label: "Revoked", tone: "dead" });
145 assert.equal(inviteFor({ ...base, status: "pending" }), "Anyone with the link");
146 assert.equal(inviteFor({ ...base, status: "pending", email: "ada@example.com", workspace: "acme" }), "ada@example.com");
147 assert.equal(inviteFor({ ...base, status: "pending", invitee: "daweazl", workspace: "flagon-io" }), "@daweazl");
148 // Which kind each is, beside whom it is for.
149 assert.deepEqual(inviteKind({ workspace: null }), { kind: "g1t", label: "Invite to g1t" });
150 assert.deepEqual(inviteKind({ workspace: "flagon-io" }), { kind: "workspace", label: "Invite to join flagon-io" });
151 assert.deepEqual(inviteState({ ...base, status: "awaiting_answer", redeemedBy: "daweazl" }), { label: "Waiting for @daweazl to accept", tone: "pending" });
152 assert.deepEqual(inviteState({ ...base, status: "declined", invitee: "daweazl" }), { label: "@daweazl declined", tone: "dead" });
153});
154
155test("bots that fill the hidden field or answer instantly are turned away", () => {
156 const form = (fields: Record<string, string>) => ({ get: (name: string) => fields[name] ?? null });
157 const now = 1_000_000;
158 assert.equal(looksAutomated(form({ website: "http://spam.example" }), now), true);
159 assert.equal(looksAutomated(form({ started: String(now - 200) }), now), true);
160 assert.equal(looksAutomated(form({ started: String(now - 10_000) }), now), false);
161 assert.equal(looksAutomated(form({}), now), false);
162 assert.equal(looksAutomated(form({ website: " " }), now), false);
163});
164
165test("a username is suggested from the invited address", () => {
166 assert.equal(suggestUsername("ada.lovelace@example.com"), "ada-lovelace");
167 assert.equal(suggestUsername("Margaret_Hamilton+g1t@example.com"), "margaret-hamilton");
168 assert.equal(suggestUsername("--x--@example.com"), "x");
169 assert.equal(suggestUsername(`${"a".repeat(38)}.b@example.com`), "a".repeat(38));
170 assert.equal(suggestUsername("...@example.com"), "");
171 assert.equal(suggestUsername(null), "");
172});
173
174test("an invite lands in its workspace, else its repository", () => {
175 assert.equal(landingFor({ workspace: { slug: "Flagon-IO" }, repository: null }), "flagon-io");
176 assert.equal(landingFor({ workspace: null, repository: { name: "flagon-io/g1t" } }), "flagon-io/g1t");
177 assert.equal(landingFor({ workspace: null, repository: null }), null);
178});
179
180test("the welcome is for one place, and ends", () => {
181 const set = welcomeCookie("flagon-io/g1t", true);
182 assert.match(set, /^g1t_welcome=flagon-io%2Fg1t; Path=\/; Max-Age=300; HttpOnly; SameSite=Lax; Secure$/);
183 const header = `a=1; ${set.split(";")[0]}; b=2`;
184 assert.equal(welcomes(header, "flagon-io/g1t"), true);
185 assert.equal(welcomes(header, "flagon-io"), false);
186 assert.equal(welcomes("g1t_welcome=flagon-io", "Flagon-IO"), true);
187 assert.equal(welcomes("g1t_welcome=%E0%A4%A", "flagon-io"), false);
188 assert.equal(welcomes("g1t_welcome=..%2F..%2Fx", "../../x"), false);
189 assert.equal(welcomes(null, "flagon-io"), false);
190 assert.match(clearWelcome(false), /^g1t_welcome=; Path=\/; Max-Age=0; HttpOnly; SameSite=Lax$/);
191});
192
193test("the two invites say which they are", () => {
194 // A workspace's People page: an invitation to accept or decline, which signs up whoever has no account.
195 const copy = workspaceInviteCopy("Flagon, Inc.");
196 assert.equal(copy.heading, "Invite to Flagon, Inc.");
197 assert.match(copy.hint, /invitation to join Flagon, Inc\..*join only if they accept/);
198 assert.match(copy.hint, /If they do not have a g1t account yet, they sign up from the invitation first/);
199 assert.doesNotMatch(copy.hint, /one of yours|shared invites/);
200});
201
202test("an invite's page names the invite it is", () => {
203 const base = { kind: "account" as const, invitedBy: { username: "syntaqx" }, workspace: null, repository: null, hasAccount: false };
204 const g1t = invitePageCopy(base, false);
205 assert.equal(`${g1t.before}${g1t.place ?? ""}${g1t.after}`, "@syntaqx invited you to g1t");
206 assert.match(g1t.about, /lets you make an account\. It does not add you to anyone's workspace/);
207 const join = invitePageCopy({ ...base, workspace: { name: "Flagon, Inc." } }, false);
208 assert.equal(`${join.before}${join.place}${join.after}`, "@syntaqx invited you to join Flagon, Inc. on g1t");
209 assert.equal(join.place, "Flagon, Inc.");
210 assert.match(join.about, /invitation to join Flagon, Inc\., which you accept or decline/);
211 assert.match(join.about, /You do not have a g1t account yet, so it also lets you make one/);
212 // Someone with an account, or signed in, just accepts.
213 assert.match(invitePageCopy({ ...base, kind: "workspace", workspace: { name: "Flagon, Inc." }, hasAccount: true }, false).about, /Accepting joins you to Flagon, Inc\./);
214 assert.equal(invitePageCopy({ ...base, invitedBy: null }, false).before, "The g1t team invited you to g1t");
215});