@bongos/core 1.19.1023 → 1.19.1025
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 +55 -40
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0213-platform-terms-acceptance-is-the-hubs-own-witness.md +2 -0
- package/docs/api/openapi.json +58 -4
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +18 -16
- package/docs/copy-registry.json +43 -25
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_255_access_requests_invite_kind.sql +52 -0
- package/modules/government/catalog.js +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/onboarding/routes/access-requests.js +62 -11
- package/modules/platform-identity/connection-suggestions.js +1 -1
- package/modules/platform-identity/migrations/platform_identity_024_terms_grandfathered.sql +68 -0
- package/modules/platform-identity/terms-acceptance.js +34 -14
- package/modules/public-landing/public/terms.html +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/bongos/auth-github.js +4 -2
- package/src/bongos/db-kernel.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/access_requests_invite.mjs +81 -4
- package/tests/application_lifecycle.mjs +174 -6
- package/tests/auth_inactive_builder.mjs +84 -0
- package/tests/connection_suggestions.mjs +1 -1
- package/tests/connections_api.mjs +1 -1
- package/tests/handle_machinery.mjs +2 -0
- package/tests/people_name_only.mjs +2 -1
- package/tests/profile_connections_ui.mjs +2 -2
- package/tests/profile_rollup_consent.mjs +3 -0
- package/tests/terms_acceptance.mjs +264 -14
- package/tests/terms_grandfather_db.mjs +144 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// tests/terms_grandfather_db.mjs — task 1003360 (owner decision on blocker
|
|
2
|
+
// 1000123; ADR 0217 D3, amending ADR 0213 D4). Real DB.
|
|
3
|
+
//
|
|
4
|
+
// Proves migration platform_identity_024 against real SQL: an account the hub
|
|
5
|
+
// had already witnessed is published immediately after the migration, without
|
|
6
|
+
// signing in again; its acceptance is queryably distinct from a real sign-in;
|
|
7
|
+
// the grandfather runs once; and the owner's next hub sign-in replaces it.
|
|
8
|
+
// tests/terms_acceptance.mjs pins the same rules DB-free (the statement shapes
|
|
9
|
+
// and the executed conflict branch); only a transaction engine can run the
|
|
10
|
+
// migration's DO block and its UPDATEs.
|
|
11
|
+
//
|
|
12
|
+
// It builds its own tables in a throwaway schema, so it cannot see or disturb a
|
|
13
|
+
// shared database's rows, and the migration's one-shot guard (keyed on the
|
|
14
|
+
// column existing) starts from a genuinely fresh table every run.
|
|
15
|
+
//
|
|
16
|
+
// Real-DB integration test: self-skips without a reachable Postgres; in the
|
|
17
|
+
// INTEGRATION set (scripts/gds/run-unit-tests.js) so the default DB-free lane
|
|
18
|
+
// never runs it.
|
|
19
|
+
//
|
|
20
|
+
// Run: DATABASE_URL=... node tests/terms_grandfather_db.mjs
|
|
21
|
+
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { strict as assert } from 'node:assert';
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const { Pool } = require('pg');
|
|
29
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
30
|
+
const { accountActiveSql } = require('../modules/platform-identity/account-visibility.js');
|
|
31
|
+
const { TERMS_VERSION } = require('../modules/platform-identity/terms-acceptance.js');
|
|
32
|
+
|
|
33
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
|
|
35
|
+
const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
|
|
36
|
+
|
|
37
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined, max: 1 });
|
|
38
|
+
let client;
|
|
39
|
+
try {
|
|
40
|
+
client = await pool.connect();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.log(`terms_grandfather_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
43
|
+
await pool.end().catch(() => {});
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SCHEMA = `t1003360_${process.pid}`;
|
|
48
|
+
const LEGACY = 90336001, SIGNED = 90336002, PROV = 90336003, LATE = 90336004;
|
|
49
|
+
const q = (sql, params) => client.query(sql, params);
|
|
50
|
+
const row = async (id) => (await q(
|
|
51
|
+
'SELECT profile_state, terms_accepted_at, terms_version, terms_accepted_source FROM platform_identity_accounts WHERE github_id = $1', [id],
|
|
52
|
+
)).rows[0];
|
|
53
|
+
|
|
54
|
+
let pass = 0;
|
|
55
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await q(`CREATE SCHEMA ${SCHEMA}`);
|
|
59
|
+
await q(`SET search_path TO ${SCHEMA}`);
|
|
60
|
+
// The columns upsertAccount writes and reads: 001 the table, 007 consent, 008
|
|
61
|
+
// the handle, 009 display_name_source, 019 the acceptance pair.
|
|
62
|
+
for (const f of ['001_tables', '007_profile_consent', '008_handle', '009_profile_fields', '019_terms_acceptance']) {
|
|
63
|
+
await q(migrationSql(`platform_identity_${f}.sql`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The world 024 lands on: a legacy account the hub witnessed before the terms
|
|
67
|
+
// existed, one that accepted by a real sign-in since, and one never witnessed.
|
|
68
|
+
await q(
|
|
69
|
+
`INSERT INTO platform_identity_accounts (github_id, github_login, handle, profile_state, terms_accepted_at, terms_version)
|
|
70
|
+
VALUES ($1, 'legacy-b', 'legacy-b', 'active', NULL, NULL),
|
|
71
|
+
($2, 'signed-b', 'signed-b', 'active', '2026-09-01T10:00:00Z', '2026-08-29'),
|
|
72
|
+
($3, 'prov-b', 'prov-b', 'provisional', NULL, NULL)`,
|
|
73
|
+
[LEGACY, SIGNED, PROV],
|
|
74
|
+
);
|
|
75
|
+
const published = async () => (await q(
|
|
76
|
+
`SELECT github_id::text FROM platform_identity_accounts WHERE ${accountActiveSql()} ORDER BY github_id`,
|
|
77
|
+
)).rows.map((r) => Number(r.github_id));
|
|
78
|
+
ok((await published()).join() === String(SIGNED), 'before 024: the legacy account is dark (ADR 0213 D4, the state the owner chose against)');
|
|
79
|
+
|
|
80
|
+
await q(migrationSql('platform_identity_024_terms_grandfathered.sql'));
|
|
81
|
+
|
|
82
|
+
// Done-when 1: visible immediately, without signing in again.
|
|
83
|
+
ok((await published()).join() === [LEGACY, SIGNED].join(), 'after 024: the legacy account is published again, and nobody else newly is');
|
|
84
|
+
|
|
85
|
+
// Done-when 2: queryably distinct from a real sign-in.
|
|
86
|
+
const legacy = await row(LEGACY);
|
|
87
|
+
ok(legacy.terms_accepted_source === 'grandfathered', 'the legacy acceptance is marked grandfathered');
|
|
88
|
+
ok(legacy.terms_version === '2026-08-29' && legacy.terms_accepted_at !== null, 'and names the text and moment it was recorded under');
|
|
89
|
+
const signed = await row(SIGNED);
|
|
90
|
+
ok(signed.terms_accepted_source === 'signin', 'an acceptance a real sign-in wrote is marked signin');
|
|
91
|
+
ok(signed.terms_accepted_at.toISOString() === '2026-09-01T10:00:00.000Z', 'and its date is left exactly as the builder gave it');
|
|
92
|
+
const prov = await row(PROV);
|
|
93
|
+
ok(prov.profile_state === 'provisional' && prov.terms_accepted_at === null && prov.terms_accepted_source === null,
|
|
94
|
+
'a never-witnessed account is neither activated nor grandfathered — it was never public');
|
|
95
|
+
const counts = (await q(
|
|
96
|
+
`SELECT terms_accepted_source AS s, count(*)::int AS n FROM platform_identity_accounts
|
|
97
|
+
WHERE terms_accepted_source IS NOT NULL GROUP BY 1 ORDER BY 1`,
|
|
98
|
+
)).rows.map((r) => `${r.s}:${r.n}`).join();
|
|
99
|
+
ok(counts === 'grandfathered:1,signin:1', `an audit can count the two kinds apart (${counts})`);
|
|
100
|
+
|
|
101
|
+
// One-shot: an account that turns up legacy-shaped later is not grandfathered by a replay.
|
|
102
|
+
await q(
|
|
103
|
+
`INSERT INTO platform_identity_accounts (github_id, github_login, handle, profile_state) VALUES ($1, 'late-b', 'late-b', 'active')`,
|
|
104
|
+
[LATE],
|
|
105
|
+
);
|
|
106
|
+
await q(migrationSql('platform_identity_024_terms_grandfathered.sql'));
|
|
107
|
+
ok((await row(LATE)).terms_accepted_at === null, 'replaying 024 grandfathers nobody new — the grandfather was an event, not a rule');
|
|
108
|
+
|
|
109
|
+
// The owner's next hub sign-in replaces the grandfather with their own acceptance.
|
|
110
|
+
await pi.upsertAccount({ githubId: LEGACY, githubLogin: 'legacy-b', activate: true }, { pool: client });
|
|
111
|
+
const after = await row(LEGACY);
|
|
112
|
+
ok(after.terms_accepted_source === 'signin', 'the next sign-in replaces the grandfather with a signin');
|
|
113
|
+
ok(after.terms_accepted_at > legacy.terms_accepted_at && after.terms_version === TERMS_VERSION,
|
|
114
|
+
'dated when they signed in, under the text served then');
|
|
115
|
+
await pi.upsertAccount({ githubId: LEGACY, githubLogin: 'legacy-b', activate: true }, { pool: client });
|
|
116
|
+
ok((await row(LEGACY)).terms_accepted_at.getTime() === after.terms_accepted_at.getTime(), 'and a second sign-in does not restamp it');
|
|
117
|
+
|
|
118
|
+
await pi.upsertAccount({ githubId: SIGNED, githubLogin: 'signed-b', activate: true }, { pool: client });
|
|
119
|
+
ok((await row(SIGNED)).terms_accepted_at.toISOString() === '2026-09-01T10:00:00.000Z', 'an existing signin is never restamped either');
|
|
120
|
+
|
|
121
|
+
await pi.upsertAccount({ githubId: PROV, githubLogin: 'prov-b' }, { pool: client });
|
|
122
|
+
ok((await row(PROV)).terms_accepted_source === null, 'a non-activating upsert records no acceptance');
|
|
123
|
+
|
|
124
|
+
// The deploy window: 024 has committed but the old server wrote this sign-in,
|
|
125
|
+
// with no source. It is a real acceptance, so the next sign-in keeps its date.
|
|
126
|
+
await q(`UPDATE platform_identity_accounts SET terms_accepted_at = '2026-09-02T08:00:00Z', terms_version = '2026-08-29' WHERE github_id = $1`, [LATE]);
|
|
127
|
+
await pi.upsertAccount({ githubId: LATE, githubLogin: 'late-b', activate: true }, { pool: client });
|
|
128
|
+
const late = await row(LATE);
|
|
129
|
+
ok(late.terms_accepted_at.toISOString() === '2026-09-02T08:00:00.000Z' && late.terms_accepted_source === 'signin',
|
|
130
|
+
'an acceptance written without a source keeps its date and is marked signin');
|
|
131
|
+
|
|
132
|
+
// Last, and unparameterised: nothing after an erroring statement depends on how the session recovers.
|
|
133
|
+
await assert.rejects(
|
|
134
|
+
q(`UPDATE platform_identity_accounts SET terms_accepted_source = 'implied' WHERE github_id = ${LEGACY}`),
|
|
135
|
+
/check constraint/i,
|
|
136
|
+
);
|
|
137
|
+
ok(true, 'the source column refuses a third kind of acceptance');
|
|
138
|
+
|
|
139
|
+
console.log(`\nterms_grandfather_db.mjs: ${pass} passed`);
|
|
140
|
+
} finally {
|
|
141
|
+
await q(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`).catch(() => {});
|
|
142
|
+
client.release();
|
|
143
|
+
await pool.end().catch(() => {});
|
|
144
|
+
}
|