@substrat-run/contract-tests 0.122.1 → 0.124.0
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/dist/emitted-suite.d.ts +3 -0
- package/dist/emitted-suite.d.ts.map +1 -0
- package/dist/emitted-suite.js +102 -0
- package/dist/emitted-suite.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/module-log-suite.d.ts +6 -0
- package/dist/module-log-suite.d.ts.map +1 -0
- package/dist/module-log-suite.js +80 -0
- package/dist/module-log-suite.js.map +1 -0
- package/dist/modules.d.ts +395 -0
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +192 -2
- package/dist/modules.js.map +1 -1
- package/dist/schedule-suite.d.ts.map +1 -1
- package/dist/schedule-suite.js +131 -6
- package/dist/schedule-suite.js.map +1 -1
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +103 -4
- package/dist/scope-host-suite.js.map +1 -1
- package/dist/sql-limits-suite.d.ts.map +1 -1
- package/dist/sql-limits-suite.js +29 -2
- package/dist/sql-limits-suite.js.map +1 -1
- package/dist/system-switch-suite.d.ts.map +1 -1
- package/dist/system-switch-suite.js +226 -20
- package/dist/system-switch-suite.js.map +1 -1
- package/package.json +3 -3
package/dist/scope-host-suite.js
CHANGED
|
@@ -1166,7 +1166,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
1166
1166
|
it('facets the outbox, and keeps an ERASED payload out of the null bucket (#1239)', async () => {
|
|
1167
1167
|
const stub = await host.getScope(alice, t1, s1);
|
|
1168
1168
|
const subject = ulid();
|
|
1169
|
-
// Three events: two whose payload carries `secret
|
|
1169
|
+
// Three events: two whose payload carries `secret` — both classed pseudonymous,
|
|
1170
|
+
// since they name a subject — and one that never had it.
|
|
1170
1171
|
await stub.invoke('test/emit-event', { subject, secret: 'kept' });
|
|
1171
1172
|
await stub.invoke('test/emit-event', { subject: ulid(), secret: 'kept' });
|
|
1172
1173
|
await stub.invoke('test/emit-event');
|
|
@@ -1180,7 +1181,10 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
1180
1181
|
type: 'test.happened',
|
|
1181
1182
|
});
|
|
1182
1183
|
expect(before.erased).toBe(0);
|
|
1183
|
-
|
|
1184
|
+
// #1762: a classified event is withheld from a payload grouping, so the two
|
|
1185
|
+
// `kept` events are counted apart rather than bucketed.
|
|
1186
|
+
expect(before.buckets.find((b) => b.value === 'kept')).toBeUndefined();
|
|
1187
|
+
expect(before.withheldPersonal).toBeGreaterThanOrEqual(2);
|
|
1184
1188
|
// The event that never carried the field is a genuine null bucket — an
|
|
1185
1189
|
// absence in the data, which is a different fact from an erasure.
|
|
1186
1190
|
expect(before.buckets.find((b) => b.value === null).count).toBeGreaterThanOrEqual(1);
|
|
@@ -1192,12 +1196,69 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
1192
1196
|
});
|
|
1193
1197
|
// THE RULE: the erased event is counted apart, and did NOT join the null
|
|
1194
1198
|
// bucket. Folding it in would show a reader a clean distribution with no
|
|
1195
|
-
// hint that part of the history was redacted.
|
|
1199
|
+
// hint that part of the history was redacted. It moves from withheld to
|
|
1200
|
+
// erased — counted once, in the one place that says what happened to it.
|
|
1196
1201
|
expect(after.erased).toBe(1);
|
|
1197
|
-
expect(after.
|
|
1202
|
+
expect(after.withheldPersonal).toBe(before.withheldPersonal - 1);
|
|
1198
1203
|
expect(after.buckets.find((b) => b.value === null)?.count ?? 0).toBe(before.buckets.find((b) => b.value === null).count);
|
|
1199
1204
|
// And the denominator still counts it: the event happened, whatever it said.
|
|
1200
1205
|
expect(after.total).toBe(before.total);
|
|
1206
|
+
expect(after.truncated).toBe(false);
|
|
1207
|
+
expect(after.buckets.reduce((n, b) => n + b.count, 0) + after.erased + after.withheldPersonal).toBe(after.total);
|
|
1208
|
+
});
|
|
1209
|
+
it('withholds personal-data events from a payload grouping, and only there (#1762)', async () => {
|
|
1210
|
+
const stub = await host.getScope(alice, t1, s1);
|
|
1211
|
+
const shredded = ulid();
|
|
1212
|
+
// One payload field under every class. Grouped by it, only the `none` events may
|
|
1213
|
+
// produce buckets: a bucket per address is a search for a person.
|
|
1214
|
+
await stub.invoke('test/emit-classified', { pii: 'none', email: 'ops@acme.test' });
|
|
1215
|
+
await stub.invoke('test/emit-classified', { pii: 'none', email: 'ops@acme.test' });
|
|
1216
|
+
await stub.invoke('test/emit-classified', { pii: 'pseudonymous', email: 'anna@acme.test' });
|
|
1217
|
+
await stub.invoke('test/emit-classified', { pii: 'pseudonymous', email: 'anna@acme.test' });
|
|
1218
|
+
await stub.invoke('test/emit-classified', { pii: 'direct', email: 'bo@acme.test' });
|
|
1219
|
+
await stub.invoke('test/emit-classified', { pii: 'direct', email: 'cy@acme.test', subject: shredded });
|
|
1220
|
+
await host.admin.shredSubject(staff, t1, s1, dataSubjectId.parse(shredded));
|
|
1221
|
+
const byEmail = await host.admin.facetEvents(staff, t1, s1, {
|
|
1222
|
+
groupBy: { kind: 'payload', field: 'email' },
|
|
1223
|
+
type: 'test.classified',
|
|
1224
|
+
});
|
|
1225
|
+
expect(byEmail.buckets.map((b) => [b.value, b.count])).toEqual([['ops@acme.test', 2]]);
|
|
1226
|
+
// Three classified events still carry their payload; the fourth was shredded and
|
|
1227
|
+
// is counted as erased, not twice.
|
|
1228
|
+
expect(byEmail.withheldPersonal).toBe(3);
|
|
1229
|
+
expect(byEmail.erased).toBe(1);
|
|
1230
|
+
expect(byEmail.total).toBe(6);
|
|
1231
|
+
expect(byEmail.buckets.reduce((n, b) => n + b.count, 0) + byEmail.erased + byEmail.withheldPersonal).toBe(byEmail.total);
|
|
1232
|
+
// The access row says what the read held back, on this co-located branch as on the
|
|
1233
|
+
// delegated one — `delegatedReadParams.facetEvents` is the one projection.
|
|
1234
|
+
const rows = (await host.admin.accessLog(staff, { method: 'facetEvents' }))
|
|
1235
|
+
.filter((e) => e.scopeId === s1)
|
|
1236
|
+
.map((e) => (typeof e.params === 'string' ? JSON.parse(e.params) : e.params))
|
|
1237
|
+
.filter((p) => p.groupBy?.field === 'email');
|
|
1238
|
+
expect(rows.at(-1)).toMatchObject({ withheldPersonal: 3 });
|
|
1239
|
+
expect(rows.at(-1)).not.toHaveProperty('withheldReason');
|
|
1240
|
+
// The row's params are cut at 500 chars, and `type` is unbounded: what was withheld
|
|
1241
|
+
// must survive a request long enough to be cut.
|
|
1242
|
+
const long = `test.${'x'.repeat(600)}`;
|
|
1243
|
+
await host.admin.facetEvents(staff, t1, s1, { groupBy: { kind: 'payload', field: 'email' }, type: long });
|
|
1244
|
+
const cut = (await host.admin.accessLog(staff, { method: 'facetEvents' }))
|
|
1245
|
+
.filter((e) => e.scopeId === s1)
|
|
1246
|
+
.map((e) => (typeof e.params === 'string' ? e.params : JSON.stringify(e.params)))
|
|
1247
|
+
.filter((text) => text.includes('x'.repeat(100)));
|
|
1248
|
+
expect(cut).toHaveLength(1);
|
|
1249
|
+
expect(cut[0].startsWith('{"withheldPersonal":0,')).toBe(true);
|
|
1250
|
+
// An envelope grouping is not payload content: every class is counted, and the
|
|
1251
|
+
// class itself stays available as a dimension.
|
|
1252
|
+
const byClass = await host.admin.facetEvents(staff, t1, s1, {
|
|
1253
|
+
groupBy: { kind: 'piiClass' },
|
|
1254
|
+
type: 'test.classified',
|
|
1255
|
+
});
|
|
1256
|
+
expect(Object.fromEntries(byClass.buckets.map((b) => [b.value, b.count]))).toEqual({
|
|
1257
|
+
none: 2,
|
|
1258
|
+
pseudonymous: 2,
|
|
1259
|
+
direct: 2,
|
|
1260
|
+
});
|
|
1261
|
+
expect([byClass.erased, byClass.withheldPersonal, byClass.total]).toEqual([0, 0, 6]);
|
|
1201
1262
|
});
|
|
1202
1263
|
it('carries WHEN each bucket last saw an event, not only how many (#1234)', async () => {
|
|
1203
1264
|
const stub = await host.getScope(alice, t1, s1);
|
|
@@ -1244,6 +1305,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
1244
1305
|
// as erased would tell a reader that history was withheld from them when it
|
|
1245
1306
|
// was simply never written.
|
|
1246
1307
|
expect(facet.erased).toBe(0);
|
|
1308
|
+
expect(facet.withheldPersonal).toBe(0);
|
|
1247
1309
|
expect(facet.buckets.find((b) => b.value === null).count).toBe(1);
|
|
1248
1310
|
expect(facet.total).toBe(3);
|
|
1249
1311
|
});
|
|
@@ -2321,6 +2383,43 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
2321
2383
|
const log = await host.admin.auditLog(staff, { limit: 500 });
|
|
2322
2384
|
expect(log.some((e) => e.action === 'restoreDirectory')).toBe(true);
|
|
2323
2385
|
});
|
|
2386
|
+
// A directory dump is untrusted input exactly as a scope dump is (#1143), and
|
|
2387
|
+
// both adapters refuse it with the same words. The DO path always judged it;
|
|
2388
|
+
// the node path ran the DDL through `exec`, appended statements included.
|
|
2389
|
+
it('refuses a dump whose DDL carries a second statement, and leaves the directory as it was (#1143)', async () => {
|
|
2390
|
+
const backup = await host.admin.exportDirectory(staff);
|
|
2391
|
+
const smuggling = {
|
|
2392
|
+
name: 'directory_probe',
|
|
2393
|
+
ddl: 'CREATE TABLE directory_probe (id TEXT); CREATE TABLE smuggled (id TEXT);',
|
|
2394
|
+
columns: ['id'],
|
|
2395
|
+
rows: [['1']],
|
|
2396
|
+
};
|
|
2397
|
+
await expect(host.admin.restoreDirectory(staff, { ...backup, tables: [...backup.tables, smuggling] })).rejects.toThrow(/more than one statement/);
|
|
2398
|
+
const after = (await host.admin.exportDirectory(staff)).tables.map((t) => t.name);
|
|
2399
|
+
expect(after).not.toContain('smuggled');
|
|
2400
|
+
expect(after).not.toContain('directory_probe');
|
|
2401
|
+
// Refused before anything was dropped: the platform still resolves and serves.
|
|
2402
|
+
expect(await host.admin.getTenant(staff, t1)).toBeDefined();
|
|
2403
|
+
const stub = await host.getScope(alice, t1, s1);
|
|
2404
|
+
await stub.invoke('test/write-marker', { v: 'after-refused-directory-restore' });
|
|
2405
|
+
});
|
|
2406
|
+
// The positive twin: the check does not refuse every extra table, and a `;` inside
|
|
2407
|
+
// a string literal is not read as a statement boundary.
|
|
2408
|
+
it('still restores an honest extra table whose DDL contains a quoted semicolon', async () => {
|
|
2409
|
+
const backup = await host.admin.exportDirectory(staff);
|
|
2410
|
+
const honest = {
|
|
2411
|
+
name: 'directory_probe',
|
|
2412
|
+
ddl: "CREATE TABLE directory_probe (id TEXT, note TEXT DEFAULT 'a;b')",
|
|
2413
|
+
columns: ['id', 'note'],
|
|
2414
|
+
rows: [['1', 'ok']],
|
|
2415
|
+
};
|
|
2416
|
+
await host.admin.restoreDirectory(staff, { ...backup, tables: [...backup.tables, honest] });
|
|
2417
|
+
const restored = (await host.admin.exportDirectory(staff)).tables.find((t) => t.name === 'directory_probe');
|
|
2418
|
+
expect(restored?.rows).toEqual([['1', 'ok']]);
|
|
2419
|
+
// Back to the copy, which does not carry the probe.
|
|
2420
|
+
await host.admin.restoreDirectory(staff, backup);
|
|
2421
|
+
expect((await host.admin.exportDirectory(staff)).tables.some((t) => t.name === 'directory_probe')).toBe(false);
|
|
2422
|
+
});
|
|
2324
2423
|
});
|
|
2325
2424
|
// -- restore (§8's write half): load a dump into an EXISTING scope in place ---
|
|
2326
2425
|
describe('scope restore — backup/backout (§8)', () => {
|