@bongos/core 1.19.171 → 1.19.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +37 -22
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +10 -1
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/api/openapi.json +136 -3
- package/docs/api-reference.md +9 -2
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
- package/modules/platform-identity/migrations/platform_identity_008_handle.sql +70 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/platform-identity.js +196 -5
- package/modules/platform-identity/routes/profile.js +47 -1
- package/modules/platform-identity/routes/public-profile.js +49 -0
- package/modules/platform-identity/tests/platform-identity.mjs +192 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/handle_machinery.mjs +143 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// tests/handle_machinery.mjs — P01, task 1002797 (goal 1000045; real DB).
|
|
2
|
+
//
|
|
3
|
+
// Proves migration 008's seeding + the go-forward handle machinery against real
|
|
4
|
+
// SQL (the plpgsql seeding loop and the partial unique index cannot run on
|
|
5
|
+
// pg-mem — the tests/profile_rollup_consent.mjs precedent):
|
|
6
|
+
// 1. seeding dedupes case-colliding logins oldest-first with -2, -3 suffixes;
|
|
7
|
+
// reserved logins never seed bare; an unusable login → builder-<github_id>
|
|
8
|
+
// 2. upsertAccount seeds a NEW account's handle, suffixing past taken ones
|
|
9
|
+
// 3. ensureHandle never clobbers a seeded handle
|
|
10
|
+
// 4. setAccountHandle: rename keeps the old handle in previous_handle and
|
|
11
|
+
// starts the rate-limit clock; a second rename inside the window is
|
|
12
|
+
// refused; an expired window allows; a case-insensitive collision → taken;
|
|
13
|
+
// reserved words are refused
|
|
14
|
+
//
|
|
15
|
+
// Real-DB integration test: self-skips without a reachable Postgres; in the
|
|
16
|
+
// INTEGRATION set so the default DB-free lane never runs it.
|
|
17
|
+
//
|
|
18
|
+
// Run: DATABASE_URL=... node tests/handle_machinery.mjs
|
|
19
|
+
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const { Pool } = require('pg');
|
|
27
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
28
|
+
|
|
29
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
|
|
31
|
+
const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
|
|
32
|
+
|
|
33
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
34
|
+
try {
|
|
35
|
+
await pool.query('SELECT 1');
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(`handle_machinery.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
38
|
+
await pool.end().catch(() => {});
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// High-numbered fixture ids so a shared DB can never collide with real data.
|
|
43
|
+
const TWIN_A = 9002797001, TWIN_B = 9002797002, RESV = 9002797003, JUNK = 9002797004, FRESH = 9002797005;
|
|
44
|
+
const ALL_IDS = [TWIN_A, TWIN_B, RESV, JUNK, FRESH];
|
|
45
|
+
|
|
46
|
+
const q = (sql, params) => pool.query(sql, params);
|
|
47
|
+
async function cleanup() {
|
|
48
|
+
await q(`DELETE FROM platform_identity_accounts WHERE github_id = ANY($1::bigint[])`, [ALL_IDS]).catch(() => {});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let pass = 0;
|
|
52
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
|
|
53
|
+
const handleOf = async (id) => (await pi.getAccount(id, { pool })).handle;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// The module's actual migrations (idempotent). 008 runs AFTER the fixture
|
|
57
|
+
// rows exist so its seeding loop has NULL handles to seed — exactly the
|
|
58
|
+
// legacy-rows-at-deploy shape.
|
|
59
|
+
await q(migrationSql('platform_identity_001_tables.sql'));
|
|
60
|
+
await q(migrationSql('platform_identity_004_projects.sql'));
|
|
61
|
+
await q(migrationSql('platform_identity_005_builder_activity.sql'));
|
|
62
|
+
await q(migrationSql('platform_identity_007_profile_consent.sql'));
|
|
63
|
+
// On a DB where 008 already ran the columns exist and the raw inserts below
|
|
64
|
+
// simply land with handle NULL — the re-run seeds only those. Idempotence is
|
|
65
|
+
// part of what this proves.
|
|
66
|
+
await q(migrationSql('platform_identity_008_handle.sql')).catch(() => {});
|
|
67
|
+
await cleanup();
|
|
68
|
+
|
|
69
|
+
await q(
|
|
70
|
+
`INSERT INTO platform_identity_accounts (github_id, github_login, created_at)
|
|
71
|
+
VALUES ($1, 'P01SeedTwin', now() - interval '10 minutes'),
|
|
72
|
+
($2, 'p01seedtwin', now() - interval '9 minutes'),
|
|
73
|
+
($3, 'admin', now() - interval '8 minutes'),
|
|
74
|
+
($4, '___', now() - interval '7 minutes')`,
|
|
75
|
+
[TWIN_A, TWIN_B, RESV, JUNK],
|
|
76
|
+
);
|
|
77
|
+
await q(migrationSql('platform_identity_008_handle.sql'));
|
|
78
|
+
|
|
79
|
+
// --- 1. seeding ----------------------------------------------------------
|
|
80
|
+
ok((await handleOf(TWIN_A)) === 'p01seedtwin', 'oldest of a case-colliding pair seeds the bare handle');
|
|
81
|
+
ok((await handleOf(TWIN_B)) === 'p01seedtwin-2', 'the younger twin gets the -2 suffix');
|
|
82
|
+
ok((await handleOf(RESV)) === 'admin-2', 'a reserved login never seeds bare — suffixed from -2');
|
|
83
|
+
ok((await handleOf(JUNK)) === `builder-${JUNK}`, 'a login that contributes nothing seeds builder-<github_id>');
|
|
84
|
+
|
|
85
|
+
// --- 2. go-forward seeding via upsertAccount ------------------------------
|
|
86
|
+
const fresh = await pi.upsertAccount({ githubId: FRESH, githubLogin: 'P01SeedTwin' }, { pool });
|
|
87
|
+
ok(fresh.handle === 'p01seedtwin-3', 'a new account collides past every taken suffix');
|
|
88
|
+
|
|
89
|
+
// --- 3. ensureHandle never clobbers ---------------------------------------
|
|
90
|
+
const again = await pi.ensureHandle({ githubId: FRESH, githubLogin: 'renamed-login' }, { pool });
|
|
91
|
+
ok(again === 'p01seedtwin-3', 'ensureHandle on a seeded account returns the existing handle untouched');
|
|
92
|
+
|
|
93
|
+
// --- 4. renames -----------------------------------------------------------
|
|
94
|
+
const r1 = await pi.setAccountHandle({ githubId: FRESH, handle: 'P01-Renamed' }, { pool });
|
|
95
|
+
ok(r1.handle === 'p01-renamed' && r1.previous_handle === 'p01seedtwin-3',
|
|
96
|
+
'rename lowercases, keeps the old handle in the 301 slot');
|
|
97
|
+
|
|
98
|
+
const r2 = await pi.setAccountHandle({ githubId: FRESH, handle: 'p01-again' }, { pool });
|
|
99
|
+
ok(r2.error === 'rate_limited' && !!r2.retry_at, 'a second rename inside the window is refused with retry_at');
|
|
100
|
+
ok((await handleOf(FRESH)) === 'p01-renamed', 'the refused rename changed nothing');
|
|
101
|
+
|
|
102
|
+
await q(`UPDATE platform_identity_accounts SET handle_changed_at = now() - interval '8 days' WHERE github_id = $1`, [FRESH]);
|
|
103
|
+
const r3 = await pi.setAccountHandle({ githubId: FRESH, handle: 'p01-again' }, { pool });
|
|
104
|
+
ok(r3.handle === 'p01-again' && r3.previous_handle === 'p01-renamed', 'an expired window allows the rename; the 301 slot rolls forward');
|
|
105
|
+
|
|
106
|
+
await q(`UPDATE platform_identity_accounts SET handle_changed_at = now() - interval '8 days' WHERE github_id = $1`, [FRESH]);
|
|
107
|
+
const r4 = await pi.setAccountHandle({ githubId: FRESH, handle: 'P01SEEDTWIN' }, { pool });
|
|
108
|
+
ok(r4.error === 'handle_taken', 'a case-insensitive collision with a live handle is refused');
|
|
109
|
+
|
|
110
|
+
const r5 = await pi.setAccountHandle({ githubId: FRESH, handle: 'u' }, { pool });
|
|
111
|
+
ok(r5.error === 'reserved_handle', 'reserved words are refused on rename');
|
|
112
|
+
|
|
113
|
+
// --- 5. resolution (R05, task 1002281) ------------------------------------
|
|
114
|
+
// FRESH is 'p01-again' with previous 'p01-renamed'; everything is still
|
|
115
|
+
// provisional (fail-closed) at this point.
|
|
116
|
+
const prov = await pi.resolveHandle('p01-again', { pool });
|
|
117
|
+
ok(prov === null, 'a provisional account is not addressable — even by its current handle');
|
|
118
|
+
|
|
119
|
+
await q(`UPDATE platform_identity_accounts SET profile_state = 'active' WHERE github_id = ANY($1::bigint[])`, [[FRESH, TWIN_A]]);
|
|
120
|
+
|
|
121
|
+
const cur = await pi.resolveHandle('P01-Again', { pool });
|
|
122
|
+
ok(cur && cur.account && cur.account.handle === 'p01-again', 'the current handle resolves case-insensitively');
|
|
123
|
+
|
|
124
|
+
const moved = await pi.resolveHandle('p01-renamed', { pool });
|
|
125
|
+
ok(moved && moved.moved_to === 'p01-again', 'the previous handle answers a move to the current one');
|
|
126
|
+
|
|
127
|
+
const page = await pi.getPublicProfileByHandle('p01-again', { pool });
|
|
128
|
+
ok(page && page.account && page.account.member_since && !('github_id' in page.account),
|
|
129
|
+
'the public read carries the identity band, never the join key');
|
|
130
|
+
ok('cross_project' in page, 'the cross_project key is always present on a 200 (null = hidden/none)');
|
|
131
|
+
|
|
132
|
+
// Another active account claims the freed previous handle → the exact
|
|
133
|
+
// current-handle match beats the stale 301 (documented squat-forward rule).
|
|
134
|
+
await q(`UPDATE platform_identity_accounts SET handle = 'p01-renamed' WHERE github_id = $1`, [TWIN_A]);
|
|
135
|
+
const claimed = await pi.resolveHandle('p01-renamed', { pool });
|
|
136
|
+
ok(claimed && claimed.account && Number(claimed.account.github_id) === TWIN_A,
|
|
137
|
+
'an exact current-handle match wins over another account previous-handle claim');
|
|
138
|
+
|
|
139
|
+
console.log(`\nhandle_machinery: ${pass} assertions passed`);
|
|
140
|
+
} finally {
|
|
141
|
+
await cleanup();
|
|
142
|
+
await pool.end().catch(() => {});
|
|
143
|
+
}
|