@substrat-run/contract-tests 0.160.0 → 0.162.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/directory-audit-suite.d.ts +8 -0
- package/dist/directory-audit-suite.d.ts.map +1 -1
- package/dist/directory-audit-suite.js +398 -3
- package/dist/directory-audit-suite.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/introspection-reads-suite.d.ts +35 -0
- package/dist/introspection-reads-suite.d.ts.map +1 -0
- package/dist/introspection-reads-suite.js +114 -0
- package/dist/introspection-reads-suite.js.map +1 -0
- package/dist/restore-role-suite.d.ts +17 -0
- package/dist/restore-role-suite.d.ts.map +1 -0
- package/dist/restore-role-suite.js +107 -0
- package/dist/restore-role-suite.js.map +1 -0
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +172 -21
- package/dist/scope-host-suite.js.map +1 -1
- package/package.json +3 -3
package/dist/scope-host-suite.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
2
|
import { connectorCalls, connectorTestFetch, resetConnectorCalls } from './connector-fixture.js';
|
|
3
3
|
import { connectionId, dataSubjectId, domainEvent, errorCodeOf, moduleId, SCOPE_GATE_REASONS, eventId, instant, moduleManifest, orgId, AUTO_ADMISSION_NOTE, permissionKey, platformActorId, platformRequestId, principalId, scopeId, tenantId, SCOPE_QUERY_ROW_MAX, LIST_PAGE_MAX, KERNEL_AUTHORED_EVENT_TYPES, EMAIL_SENT, PLATFORM_REQUEST_ENTITY_TYPE, SEND_EMAIL_KIND, isUndecodablePlatformRequest, platformRequestEntry, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, unheldMigrationDigest, withUpstreamReference, } from '@substrat-run/contracts';
|
|
4
|
-
import { INERT_SCOPE_REASON, BACKFILL_MOVE_ID, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, withheldOpsFailure, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, promoteReviewDigest, promoteReviewRefusalOf, rebindReviewRefusalOf, createUlid, ulid, WIRE_AUTH_FILTER_TEXT_MAX, } from '@substrat-run/kernel';
|
|
4
|
+
import { INERT_SCOPE_REASON, BACKFILL_MOVE_ID, SCOPE_COPY_LEASE_MS, isSearchIndexTable, platformIntentFailureMessage, withheldOpsFailure, withheldStaffFailure, REDACTED_DELIVERY_NOTE, REDACTED_FAILURE_NOTE, REDACTED_INTENT_MARKER, runPlatformSweep, startPlatformSweeper, promoteReviewDigest, promoteReviewRefusalOf, rebindReviewRefusalOf, createUlid, ulid, WIRE_AUTH_FILTER_TEXT_MAX, } from '@substrat-run/kernel';
|
|
5
5
|
import { billedMod, contractTestBareOps, contractTestInitialModules, gateModManifest, lateMod, TEST_LIFECYCLE, testModManifest, victimModManifest, } from './modules.js';
|
|
6
6
|
import { testOperations } from './test-operations.js';
|
|
7
7
|
import { derivedDigestOf, heldMigrations } from './held-migrations.js';
|
|
@@ -2368,6 +2368,21 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
2368
2368
|
// The spine: present and flagged system, so the UI can group it apart.
|
|
2369
2369
|
expect(byName.get('_substrat_outbox')?.system).toBe(true);
|
|
2370
2370
|
});
|
|
2371
|
+
it('counts what is in force apart from what a table keeps as tombstones (#2531)', async () => {
|
|
2372
|
+
const key = `introspect-${ulid().toLowerCase()}`;
|
|
2373
|
+
await host.admin.defineRole(staff, t1, { key, permissions: [permissionKey.parse('thing:read')], source: 'vertical' });
|
|
2374
|
+
const seat = { principalId: principalId.parse(ulid()), roleKey: key, node: { tenantId: t1, scopeId: s1 } };
|
|
2375
|
+
const tuples = async () => (await host.admin.listScopeTables(staff, t1, s1)).find((t) => t.name === '_substrat_tuples');
|
|
2376
|
+
const before = await tuples();
|
|
2377
|
+
expect(before.liveRowCount).toBeDefined();
|
|
2378
|
+
await host.admin.assignRole(staff, seat);
|
|
2379
|
+
expect(await tuples()).toMatchObject({ rowCount: before.rowCount + 1, liveRowCount: before.liveRowCount + 1 });
|
|
2380
|
+
// A revoke tombstones the seat: the row is kept, and it is no longer in force.
|
|
2381
|
+
await host.admin.unassignRole(staff, seat);
|
|
2382
|
+
expect(await tuples()).toMatchObject({ rowCount: before.rowCount + 1, liveRowCount: before.liveRowCount });
|
|
2383
|
+
// A table that keeps no tombstones answers no live count.
|
|
2384
|
+
expect((await host.admin.listScopeTables(staff, t1, s1)).find((t) => t.name === '_substrat_outbox')).not.toHaveProperty('liveRowCount');
|
|
2385
|
+
});
|
|
2371
2386
|
it('reads a bounded page of a table', async () => {
|
|
2372
2387
|
const stub = await host.getScope(alice, t1, s1);
|
|
2373
2388
|
await stub.invoke('test/write-marker', { v: 'page-row' });
|
|
@@ -3126,6 +3141,29 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
3126
3141
|
expect(await rowsOf(ops.elsewhere)).toEqual(before.elsewhere);
|
|
3127
3142
|
await host.settlePlatformRequest(t1, s1, unlinked, { status: 'done' });
|
|
3128
3143
|
});
|
|
3144
|
+
it("#2556: staff's row alone, for a withheld pass that left a tombstoned intent pending, is redacted too", async () => {
|
|
3145
|
+
const kind = `connector:erasure-${ulid()}`;
|
|
3146
|
+
const erased = dataSubjectId.parse(ulid());
|
|
3147
|
+
const spared = dataSubjectId.parse(ulid());
|
|
3148
|
+
const linked = await routeIntent(kind, erased, 'Anna Ek');
|
|
3149
|
+
const unlinked = await routeIntent(kind, spared, 'Bo Lund');
|
|
3150
|
+
// As the drain files it (`withheldStaffFailure`): no tenant, the drained scope, marker first.
|
|
3151
|
+
const transient = (operation, id, error) => host.admin.recordOpsFailure({
|
|
3152
|
+
actor: staff,
|
|
3153
|
+
...withheldStaffFailure({ operation, stage: 'transient', scopeId: s1 }, `${error}; reference = up${ulid().toLowerCase()}`, ulid(), platformIntentFailureMessage(id, 'pending (the call did not complete at its deployment): ')),
|
|
3154
|
+
});
|
|
3155
|
+
const ops = { linked: op('transient-linked'), unlinked: op('transient-unlinked') };
|
|
3156
|
+
await transient(ops.linked, linked, 'HTTP 500: Anna Ek is not on file');
|
|
3157
|
+
await transient(ops.unlinked, unlinked, 'HTTP 500: Bo Lund is not on file');
|
|
3158
|
+
const rowsOf = async (operation) => (await host.admin.listOpsFailures(staff, { operation })).map((r) => [r.tenantId, r.message]);
|
|
3159
|
+
const before = await rowsOf(ops.unlinked);
|
|
3160
|
+
expect((await rowsOf(ops.linked))[0][1]).toMatch(/^\[upstream reference = up[0-9a-z]+\] platform intent .* pending \(the call did not complete at its deployment\): HTTP 500: /);
|
|
3161
|
+
await host.admin.shredSubject(staff, t1, s1, erased);
|
|
3162
|
+
expect(await rowsOf(ops.linked)).toEqual([[null, REDACTED_FAILURE_NOTE]]);
|
|
3163
|
+
expect((await issueOf(ops.linked)).lastMessage).toBe(REDACTED_FAILURE_NOTE);
|
|
3164
|
+
expect(await rowsOf(ops.unlinked)).toEqual(before);
|
|
3165
|
+
await host.settlePlatformRequest(t1, s1, unlinked, { status: 'done' });
|
|
3166
|
+
});
|
|
3129
3167
|
/** Whose an exemplar is, as the directory stores it — not on the issue read shape. */
|
|
3130
3168
|
const exemplarOwnerOf = async (operation) => {
|
|
3131
3169
|
const dump = await host.admin.exportDirectory(staff);
|
|
@@ -7440,7 +7478,8 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
7440
7478
|
await host.admin.setVerticalListed(staff, slug, true);
|
|
7441
7479
|
await host.admin.promoteVersion(staff, slug, 'prod', first);
|
|
7442
7480
|
expect((await host.admin.getScopeRecord(staff, t2, install))?.verticalVersionId).toBe(next);
|
|
7443
|
-
|
|
7481
|
+
// Seven scopes, a snapshot and three promotions: about 3 s alone, past the 5 s default under CI load (#2576).
|
|
7482
|
+
}, 30_000);
|
|
7444
7483
|
it('publish refuses an auto-admitted prod version until a staff admit vouches for it', async () => {
|
|
7445
7484
|
// Listing is the moment OTHER tenants start trusting this code, so the version
|
|
7446
7485
|
// they would install needs a recorded human decision — the auto-admission note
|
|
@@ -9709,8 +9748,22 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
9709
9748
|
const at = (time) => `2026-10-24T${time}.000Z`;
|
|
9710
9749
|
/** The whole fleet, one full page: every read names its reach and its limit. */
|
|
9711
9750
|
const fleet = { reach: { fleet: true }, limit: LIST_PAGE_MAX };
|
|
9712
|
-
/**
|
|
9713
|
-
const
|
|
9751
|
+
/** The verticals this block registered: a record is written only beside its vertical's registration. */
|
|
9752
|
+
const registered = new Set();
|
|
9753
|
+
const register = async (slug, ownerTenant) => {
|
|
9754
|
+
await host.admin.registerVertical(staff, { slug, name: slug, source: 'cli', ...(ownerTenant ? { ownerTenant } : {}) });
|
|
9755
|
+
registered.add(slug);
|
|
9756
|
+
};
|
|
9757
|
+
const forget = async (slug) => {
|
|
9758
|
+
await host.admin.deleteVertical(staff, slug);
|
|
9759
|
+
registered.delete(slug);
|
|
9760
|
+
};
|
|
9761
|
+
/** Record `rec` for the script `<vertical>-<script>`, registering the vertical (platform-owned) first when no test did. */
|
|
9762
|
+
const put = async (vertical, script, versionId, rec, time) => {
|
|
9763
|
+
if (!registered.has(vertical))
|
|
9764
|
+
await register(vertical);
|
|
9765
|
+
await host.admin.recordWireAuth(staff, { deploymentRef: `${vertical}-${script}`, verticalSlug: vertical, versionId, record: rec, recordedAt: at(time) });
|
|
9766
|
+
};
|
|
9714
9767
|
it('keeps one record per deployment, the last upload’s, and lists one vertical’s by deployment', async () => {
|
|
9715
9768
|
const [a, b] = [slugOf('wire-a'), slugOf('wire-b')];
|
|
9716
9769
|
await put(a, 'serving', 'v1', record({ audience: `${a}-serving` }), '10:00:00');
|
|
@@ -9825,26 +9878,25 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
9825
9878
|
const [owner, other] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
|
|
9826
9879
|
await host.admin.createTenant(staff, { id: owner, slug: `wire-own-${owner.slice(-6).toLowerCase()}`, name: 'Wire Owner' });
|
|
9827
9880
|
await host.admin.createTenant(staff, { id: other, slug: `wire-oth-${other.slice(-6).toLowerCase()}`, name: 'Wire Other' });
|
|
9828
|
-
const [mine, theirs, platform
|
|
9829
|
-
await
|
|
9830
|
-
await
|
|
9831
|
-
await
|
|
9832
|
-
for (const [i, slug] of [mine, theirs, platform
|
|
9881
|
+
const [mine, theirs, platform] = [slugOf('wire-mine'), slugOf('wire-theirs'), slugOf('wire-plat')];
|
|
9882
|
+
await register(mine, owner);
|
|
9883
|
+
await register(theirs, other);
|
|
9884
|
+
await register(platform);
|
|
9885
|
+
for (const [i, slug] of [mine, theirs, platform].entries()) {
|
|
9833
9886
|
await put(slug, 'serving', null, record(), `16:00:0${i}`);
|
|
9834
9887
|
await put(slug, 'archive', 'v1', record(), `16:00:1${i}`);
|
|
9835
9888
|
}
|
|
9836
9889
|
const refs = async (reach, verticalSlug) => (await host.admin.listWireAuth(staff, { reach, limit: LIST_PAGE_MAX, ...(verticalSlug ? { verticalSlug } : {}) }))
|
|
9837
9890
|
.map((r) => r.deploymentRef)
|
|
9838
|
-
.filter((ref) => [mine, theirs, platform
|
|
9891
|
+
.filter((ref) => [mine, theirs, platform].some((slug) => ref.startsWith(`${slug}-`)));
|
|
9839
9892
|
expect(await refs({ tenantId: owner })).toEqual([`${mine}-archive`, `${mine}-serving`]);
|
|
9840
9893
|
expect(await refs({ tenantId: other })).toEqual([`${theirs}-archive`, `${theirs}-serving`]);
|
|
9841
9894
|
// Naming another tenant's vertical inside one tenant's reach reaches nothing, not that vertical.
|
|
9842
9895
|
expect(await refs({ tenantId: owner }, theirs)).toEqual([]);
|
|
9843
9896
|
expect(await refs({ tenantId: owner }, mine)).toEqual([`${mine}-archive`, `${mine}-serving`]);
|
|
9844
|
-
// A tenant that owns nothing reaches nothing; a vertical no tenant owns
|
|
9845
|
-
// know, is reached by the fleet alone.
|
|
9897
|
+
// A tenant that owns nothing reaches nothing; a vertical no tenant owns is reached by the fleet alone.
|
|
9846
9898
|
expect(await refs({ tenantId: tenantId.parse(ulid()) })).toEqual([]);
|
|
9847
|
-
for (const slug of [mine, theirs, platform
|
|
9899
|
+
for (const slug of [mine, theirs, platform]) {
|
|
9848
9900
|
expect(await refs({ fleet: true }, slug)).toEqual([`${slug}-archive`, `${slug}-serving`]);
|
|
9849
9901
|
}
|
|
9850
9902
|
// A page inside the reach pages over the tenant's records only.
|
|
@@ -9891,7 +9943,7 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
9891
9943
|
const owner = tenantId.parse(ulid());
|
|
9892
9944
|
await host.admin.createTenant(staff, { id: owner, slug: `wire-log-${owner.slice(-6).toLowerCase()}`, name: 'Wire Log' });
|
|
9893
9945
|
const mine = slugOf('wire-logged');
|
|
9894
|
-
await
|
|
9946
|
+
await register(mine, owner);
|
|
9895
9947
|
await put(mine, 'serving', null, record(), '17:00:00');
|
|
9896
9948
|
// The reach given last, as a caller may spell it: the log still names it first.
|
|
9897
9949
|
await host.admin.listWireAuth(staff, { limit: 5, verticalSlug: mine, reach: { tenantId: owner } });
|
|
@@ -9919,30 +9971,119 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
9919
9971
|
await host.admin.createTenant(staff, { id: second, slug: `wire-snd-${second.slice(-6).toLowerCase()}`, name: 'Wire Second' });
|
|
9920
9972
|
const slug = slugOf('wire-owned');
|
|
9921
9973
|
const refsOf = async (owner) => (await host.admin.listWireAuth(staff, { reach: { tenantId: owner }, limit: LIST_PAGE_MAX, verticalSlug: slug })).map((r) => r.deploymentRef);
|
|
9922
|
-
//
|
|
9974
|
+
// The owner reads what was recorded while it held the slug.
|
|
9975
|
+
await register(slug, first);
|
|
9923
9976
|
await put(slug, 'archive', 'v0', record(), '18:00:00');
|
|
9924
|
-
expect(await refsOf(first)).toEqual([]);
|
|
9925
|
-
await host.admin.registerVertical(staff, { slug, name: 'Owned', source: 'cli', ownerTenant: first });
|
|
9926
9977
|
expect(await refsOf(first)).toEqual([`${slug}-archive`]);
|
|
9927
9978
|
await put(slug, 'serving', 'v1', record(), '18:00:01');
|
|
9928
9979
|
expect(await refsOf(first)).toEqual([`${slug}-archive`, `${slug}-serving`]);
|
|
9929
9980
|
// An owner changes by deleting the vertical, which takes its records, and registering it again:
|
|
9930
9981
|
// the tenant that held it reads nothing after, the new owner reads only what was recorded since.
|
|
9931
|
-
await
|
|
9932
|
-
await
|
|
9982
|
+
await forget(slug);
|
|
9983
|
+
await register(slug, second);
|
|
9933
9984
|
expect(await refsOf(first)).toEqual([]);
|
|
9934
9985
|
expect(await refsOf(second)).toEqual([]);
|
|
9935
9986
|
await put(slug, 'serving', 'v2', record(), '18:00:02');
|
|
9936
9987
|
expect(await refsOf(first)).toEqual([]);
|
|
9937
9988
|
expect(await refsOf(second)).toEqual([`${slug}-serving`]);
|
|
9938
9989
|
});
|
|
9990
|
+
it('writes no record, and no admin row, for a vertical the registry does not hold', async () => {
|
|
9991
|
+
const never = slugOf('wire-unreg');
|
|
9992
|
+
const gone = slugOf('wire-gone');
|
|
9993
|
+
await put(gone, 'serving', 'v1', record(), '20:00:00');
|
|
9994
|
+
await forget(gone);
|
|
9995
|
+
for (const slug of [never, gone]) {
|
|
9996
|
+
await expectRefusal(host.admin.recordWireAuth(staff, { deploymentRef: `${slug}-serving`, verticalSlug: slug, versionId: 'v2', record: record(), recordedAt: at('20:00:01') }), 'not_found');
|
|
9997
|
+
expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: slug })).toEqual([]);
|
|
9998
|
+
}
|
|
9999
|
+
const rows = (await host.admin.auditLog(staff, { action: 'recordWireAuth' })).filter((r) => r.vertical === never || r.vertical === gone);
|
|
10000
|
+
// Only the one write made while `gone` was registered.
|
|
10001
|
+
expect(rows.map((r) => r.after.versionId)).toEqual(['v1']);
|
|
10002
|
+
// Registered again, the slug's records start empty: nothing written while unregistered survives.
|
|
10003
|
+
await register(gone);
|
|
10004
|
+
expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: gone })).toEqual([]);
|
|
10005
|
+
});
|
|
10006
|
+
it('keeps a walk’s hold on a record, by binding name, and refuses a hold it cannot read', async () => {
|
|
10007
|
+
const a = slugOf('wire-hold');
|
|
10008
|
+
const hold = { reason: 'verify', bindings: ['SCOPE', 'SUBSTRAT_WIRE_STATE'], at: at('22:00:00') };
|
|
10009
|
+
await put(a, 'archive', 'v1', { ...record(), walkHold: hold }, '22:00:00');
|
|
10010
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toEqual(hold);
|
|
10011
|
+
// The next upload of the script writes a record without it.
|
|
10012
|
+
await put(a, 'archive', 'v1', record(), '22:00:01');
|
|
10013
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toBeUndefined();
|
|
10014
|
+
const good = { deploymentRef: `${a}-archive`, verticalSlug: a, versionId: 'v1', recordedAt: at('22:00:02') };
|
|
10015
|
+
for (const walkHold of [
|
|
10016
|
+
{ ...hold, reason: 'because' },
|
|
10017
|
+
{ ...hold, at: 'yesterday' },
|
|
10018
|
+
{ ...hold, bindings: [''] },
|
|
10019
|
+
{ ...hold, bindings: Array.from({ length: 65 }, (_, i) => `B${i}`) },
|
|
10020
|
+
{ ...hold, value: 'not a field' },
|
|
10021
|
+
// The count of names past the cap is a positive whole number when given.
|
|
10022
|
+
{ ...hold, more: 0 },
|
|
10023
|
+
{ ...hold, more: 1.5 },
|
|
10024
|
+
]) {
|
|
10025
|
+
await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: { ...record(), walkHold } }), 'validation_failed');
|
|
10026
|
+
}
|
|
10027
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toBeUndefined();
|
|
10028
|
+
// A hold of more names than it keeps: the first ones, and how many more.
|
|
10029
|
+
const many = { ...hold, bindings: Array.from({ length: 64 }, (_, i) => `B${i}`), more: 6 };
|
|
10030
|
+
await put(a, 'archive', 'v1', { ...record(), walkHold: many }, '22:00:02');
|
|
10031
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record.walkHold).toEqual(many);
|
|
10032
|
+
await put(a, 'archive', 'v1', record(), '22:00:02');
|
|
10033
|
+
// `unverified`, the walk's record of a script it could not confirm: lists claimed by none, and
|
|
10034
|
+
// only beside a hold. Without the hold, or beside lists, it is refused.
|
|
10035
|
+
const unverified = withheld('unverified');
|
|
10036
|
+
await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: unverified }), 'validation_failed');
|
|
10037
|
+
await expectRefusal(host.admin.recordWireAuth(staff, { ...good, record: { ...record(), withheld: 'unverified', walkHold: hold } }), 'validation_failed');
|
|
10038
|
+
await put(a, 'archive', 'v1', { ...unverified, walkHold: hold }, '22:00:03');
|
|
10039
|
+
expect((await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a }))[0].record).toMatchObject({ withheld: 'unverified', state: 'legacy-only', routerKids: [], walkHold: hold });
|
|
10040
|
+
});
|
|
10041
|
+
it('audits a pass of the fleet walk as walkWireAuth, naming scripts and reasons, never a value', async () => {
|
|
10042
|
+
const entry = {
|
|
10043
|
+
apply: true,
|
|
10044
|
+
confirm: false,
|
|
10045
|
+
from: null,
|
|
10046
|
+
next: 'acme/crm|01HZZZZZZZZZZZZZZZZZZZZZZZ',
|
|
10047
|
+
walked: 3,
|
|
10048
|
+
updated: ['acme-crm'],
|
|
10049
|
+
held: ['acme-crm-01hzzzzzzzzzzzzzzzzzzzzzzz'],
|
|
10050
|
+
released: [],
|
|
10051
|
+
skipped: [{ deploymentRef: 'acme-crm-01hyyyyyyyyyyyyyyyyyyyyyyy', reason: 'no-entry' }],
|
|
10052
|
+
failed: [{ deploymentRef: 'acme-crm-01hzzzzzzzzzzzzzzzzzzzzzzz', reason: 'verify' }],
|
|
10053
|
+
};
|
|
10054
|
+
await host.admin.recordWireWalk(staff, entry);
|
|
10055
|
+
const rows = (await host.admin.auditLog(staff, { action: 'walkWireAuth' })).filter((r) => r.after.next === entry.next);
|
|
10056
|
+
expect(rows).toHaveLength(1);
|
|
10057
|
+
expect(rows[0].actor).toBe(staff);
|
|
10058
|
+
expect(rows[0].after).toEqual(entry);
|
|
10059
|
+
});
|
|
10060
|
+
it('restores the owners and the records together, from one dump', async () => {
|
|
10061
|
+
const [first, second] = [tenantId.parse(ulid()), tenantId.parse(ulid())];
|
|
10062
|
+
await host.admin.createTenant(staff, { id: first, slug: `wire-rst-${first.slice(-6).toLowerCase()}`, name: 'Wire Restore' });
|
|
10063
|
+
const slug = slugOf('wire-restore');
|
|
10064
|
+
await register(slug, first);
|
|
10065
|
+
await put(slug, 'serving', 'v1', record(), '21:00:00');
|
|
10066
|
+
const backup = await host.admin.exportDirectory(staff);
|
|
10067
|
+
// Diverge past the copy: the vertical deleted (its records with it) and taken by another tenant.
|
|
10068
|
+
await host.admin.createTenant(staff, { id: second, slug: `wire-rs2-${second.slice(-6).toLowerCase()}`, name: 'Wire Restore 2' });
|
|
10069
|
+
await forget(slug);
|
|
10070
|
+
await register(slug, second);
|
|
10071
|
+
await put(slug, 'archive', 'v2', record(), '21:00:01');
|
|
10072
|
+
await host.admin.restoreDirectory(staff, backup);
|
|
10073
|
+
registered.add(slug);
|
|
10074
|
+
// The copy's owner reads the copy's records, and nothing of the divergence came back with it.
|
|
10075
|
+
const reads = async (reach) => (await host.admin.listWireAuth(staff, { reach, limit: LIST_PAGE_MAX, verticalSlug: slug })).map((r) => [r.deploymentRef, r.versionId]);
|
|
10076
|
+
expect(await reads({ tenantId: first })).toEqual([[`${slug}-serving`, 'v1']]);
|
|
10077
|
+
expect(await reads({ tenantId: second })).toEqual([]);
|
|
10078
|
+
expect(await reads({ fleet: true })).toEqual([[`${slug}-serving`, 'v1']]);
|
|
10079
|
+
});
|
|
9939
10080
|
it('forgets a vertical’s records with the vertical', async () => {
|
|
9940
10081
|
const a = slugOf('wire-del');
|
|
9941
|
-
await
|
|
10082
|
+
await register(a);
|
|
9942
10083
|
await put(a, 'serving', null, record(), '13:00:00');
|
|
9943
10084
|
const kept = slugOf('wire-kept');
|
|
9944
10085
|
await put(kept, 'serving', null, record(), '13:00:01');
|
|
9945
|
-
await
|
|
10086
|
+
await forget(a);
|
|
9946
10087
|
expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: a })).toEqual([]);
|
|
9947
10088
|
// Another vertical's record is not touched.
|
|
9948
10089
|
expect(await host.admin.listWireAuth(staff, { ...fleet, verticalSlug: kept })).toHaveLength(1);
|
|
@@ -10638,6 +10779,16 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
10638
10779
|
name: 'Impostor Co',
|
|
10639
10780
|
}), 'conflict', /already taken/);
|
|
10640
10781
|
});
|
|
10782
|
+
it('words the tenant slug refusal without the tenant that holds the slug (#2556)', async () => {
|
|
10783
|
+
const holder = tenantId.parse(ulid());
|
|
10784
|
+
const slug = `held-${holder.toLowerCase()}`;
|
|
10785
|
+
await host.admin.createTenant(staff, { id: holder, slug, name: 'Holder Co' });
|
|
10786
|
+
const taken = await host.admin
|
|
10787
|
+
.createTenant(staff, { id: tenantId.parse(ulid()), slug, name: 'Impostor Co' })
|
|
10788
|
+
.then(() => null, (e) => e);
|
|
10789
|
+
expect(taken?.message).toBe(`tenant slug '${slug}' is already taken (slugs are unique)`);
|
|
10790
|
+
expect(taken?.message).not.toContain(holder);
|
|
10791
|
+
});
|
|
10641
10792
|
it('enumerates the scopes under a tenant, and filters by status (§4.5)', async () => {
|
|
10642
10793
|
const all = await host.admin.listScopes(staff, { tenantId: t5 });
|
|
10643
10794
|
expect(all.length).toBeGreaterThanOrEqual(2);
|