Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

160 linesCodeBlameRaw
1// node --test "scripts/deploy/*.test.mjs" (npm run test:deploy)
2//
3// Invite a friend's statements, run as identity runs them: read out of
4// services/identity/src (the constants there are written out in full) and
5// run on SQLite, which D1 is, against identity's migrations applied in
6// order. Twenty invites each; a revoked one comes back, a used one is
7// spent; signing up from the link spends it and records who invited whom.
8import assert from "node:assert/strict";
9import { readFileSync, readdirSync } from "node:fs";
10import { join } from "node:path";
11import { DatabaseSync } from "node:sqlite";
12import { test } from "node:test";
13
14import { ROOT } from "./stack.mjs";
15
16const SRC = join(ROOT, "services", "identity", "src");
17
18/** Every `const NAME: &str = r#"…"#;` in `file`, by name. */
19function statements(file) {
20 const text = readFileSync(join(SRC, file), "utf8");
21 const found = {};
22 for (const match of text.matchAll(/const (\w+): &str =\s*r#"([\s\S]*?)"#;/g)) found[match[1]] = match[2];
23 return found;
24}
25
26const SQL = { ...statements("invites.rs"), ...statements(join("invites", "friends.rs")) };
27
28function migrated() {
29 const db = new DatabaseSync(":memory:");
30 const dir = join(ROOT, "services", "identity", "migrations");
31 for (const file of readdirSync(dir)
32 .filter((name) => name.endsWith(".sql"))
33 .sort()) {
34 db.exec(readFileSync(join(dir, file), "utf8"));
35 }
36 return db;
37}
38
39const LIMIT = 20;
40const LATER = "2099-01-01T00:00:00.000Z";
41const EARLIER = "2000-01-01T00:00:00.000Z";
42let next = 0;
43
44/** Makes an invite to g1t for `inviter` the way `create_invite` does: within the allowance. */
45function invite(db, inviter, email, { limit = LIMIT, expiresAt = LATER } = {}) {
46 const id = `inv_${++next}`;
47 const row = db
48 .prepare(SQL.INSERT_WITHIN_ALLOWANCE)
49 .get(id, `hash_${id}`, "g1t-abcd", null, email, "account", null, inviter, null, "user", EARLIER, expiresAt, null, null, inviter, limit);
50 return row?.id ?? null;
51}
52
53const used = (db, inviter) => db.prepare(SQL.USED).get(inviter).n;
54
55test("the statements are all there", () => {
56 for (const name of ["INSERT", "INSERT_WITHIN_ALLOWANCE", "USED", "GRANTED", "PENDING_FOR", "REVOKE", "SPEND_CODE", "INSERT_USER_WITH_CODE"]) {
57 assert.ok(SQL[name], `${name} not found`);
58 }
59});
60
61test("twenty invites each: the twenty-first is refused in the statement that writes", () => {
62 const db = migrated();
63 for (let n = 1; n <= LIMIT; n++) assert.ok(invite(db, "usr_bo", `friend${n}@example.com`), `invite ${n}`);
64 assert.equal(used(db, "usr_bo"), LIMIT);
65 assert.equal(invite(db, "usr_bo", "one-too-many@example.com"), null);
66 // Somebody else's are their own.
67 assert.ok(invite(db, "usr_cy", "friend1@example.com"));
68 // Invitations into a workspace never count.
69 db.prepare(SQL.INSERT).run(
70 "inv_ws",
71 "hash_ws",
72 "g1t-wxyz",
73 null,
74 "x@example.com",
75 "account",
76 "wsp_1",
77 "usr_bo",
78 null,
79 "none",
80 EARLIER,
81 LATER,
82 null,
83 "member",
84 );
85 assert.equal(used(db, "usr_bo"), LIMIT);
86});
87
88test("revoking an unused invite gives it back; an expired one never counted", () => {
89 const db = migrated();
90 const ids = [];
91 for (let n = 1; n <= LIMIT; n++) ids.push(invite(db, "usr_bo", `friend${n}@example.com`));
92 const revoked = db.prepare(SQL.REVOKE).get("usr_bo", ids[0]);
93 assert.equal(revoked?.id, ids[0]);
94 assert.equal(used(db, "usr_bo"), LIMIT - 1);
95 assert.ok(invite(db, "usr_bo", "another@example.com"));
96 // Someone else cannot revoke it.
97 assert.equal(db.prepare(SQL.REVOKE).get("usr_eve", ids[1]), undefined);
98 // An expired, never used invite is not counted.
99 const db2 = migrated();
100 invite(db2, "usr_bo", "late@example.com", { expiresAt: EARLIER });
101 assert.equal(used(db2, "usr_bo"), 0);
102});
103
104test("a used invite is spent: revoking it does nothing and it still counts", () => {
105 const db = migrated();
106 const id = invite(db, "usr_bo", "ada@example.com");
107 db.prepare(SQL.SPEND_CODE).run("usr_ada", id);
108 db.prepare("UPDATE invites SET applied_at = ? WHERE id = ?").run(EARLIER, id);
109 assert.equal(db.prepare(SQL.REVOKE).get("usr_bo", id), undefined);
110 assert.equal(used(db, "usr_bo"), 1);
111 // Revoked while its account was still confirming: stopped, and still spent.
112 const waiting = invite(db, "usr_bo", "cy@example.com");
113 db.prepare(SQL.SPEND_CODE).run("usr_cy", waiting);
114 assert.equal(db.prepare(SQL.REVOKE).get("usr_bo", waiting)?.id, waiting);
115 assert.equal(used(db, "usr_bo"), 2);
116});
117
118test("a pending invite for the same address is found; a revoked one is not", () => {
119 const db = migrated();
120 const id = invite(db, "usr_bo", "ada@example.com");
121 assert.equal(db.prepare(SQL.PENDING_FOR).get("usr_bo", "ada@example.com").n, 1);
122 assert.equal(db.prepare(SQL.PENDING_FOR).get("usr_cy", "ada@example.com").n, 0);
123 db.prepare(SQL.REVOKE).get("usr_bo", id);
124 assert.equal(db.prepare(SQL.PENDING_FOR).get("usr_bo", "ada@example.com").n, 0);
125});
126
127test("staff grants add to the twenty and can take some back", () => {
128 const db = migrated();
129 const grant = db.prepare("INSERT INTO invite_grants (id, target_kind, target_id, amount, note, granted_by, created_at) VALUES (?, 'user', ?, ?, ?, ?, ?)");
130 grant.run("igr_1", "usr_bo", 10, "launch partner", "staff@example.com", EARLIER);
131 grant.run("igr_2", "usr_bo", -3, null, "staff@example.com", EARLIER);
132 grant.run("igr_3", "usr_cy", 50, null, "staff@example.com", EARLIER);
133 assert.equal(db.prepare(SQL.GRANTED).get("usr_bo").n, 7);
134 assert.equal(db.prepare(SQL.GRANTED).get("usr_dee").n, 0);
135});
136
137test("signing up from the link spends the invite once and records who invited whom", () => {
138 const db = migrated();
139 const id = invite(db, "usr_bo", "ada@example.com");
140 db.prepare(SQL.SPEND_CODE).run("usr_ada", id);
141 db.prepare(SQL.INSERT_USER_WITH_CODE).run("usr_ada", "ada", "ada@example.com", "hash", 1, id);
142 const user = db.prepare("SELECT username, email_verified_at FROM users WHERE id = 'usr_ada'").get();
143 assert.equal(user.username, "ada");
144 // From the email's own link: the address starts confirmed.
145 assert.ok(user.email_verified_at);
146 const row = db.prepare("SELECT inviter_id, redeemed_by, redeemed_at, sealed_code FROM invites WHERE id = ?").get(id);
147 assert.equal(row.inviter_id, "usr_bo");
148 assert.equal(row.redeemed_by, "usr_ada");
149 assert.ok(row.redeemed_at);
150 // A second sign-up with the same code spends nothing and makes no account.
151 db.prepare(SQL.SPEND_CODE).run("usr_eve", id);
152 db.prepare(SQL.INSERT_USER_WITH_CODE).run("usr_eve", "eve", "eve@example.com", "hash", 0, id);
153 assert.equal(db.prepare("SELECT count(*) AS n FROM users WHERE id = 'usr_eve'").get().n, 0);
154 assert.equal(db.prepare("SELECT redeemed_by FROM invites WHERE id = ?").get(id).redeemed_by, "usr_ada");
155 // Without the proof the address waits to be confirmed like any other.
156 const other = invite(db, "usr_bo", "dee@example.com");
157 db.prepare(SQL.SPEND_CODE).run("usr_dee", other);
158 db.prepare(SQL.INSERT_USER_WITH_CODE).run("usr_dee", "dee", "dee@example.com", "hash", 0, other);
159 assert.equal(db.prepare("SELECT email_verified_at FROM users WHERE id = 'usr_dee'").get().email_verified_at, null);
160});