@substrat-run/contract-tests 0.82.0 → 0.84.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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/list-suite.d.ts +3 -0
- package/dist/list-suite.d.ts.map +1 -0
- package/dist/list-suite.js +181 -0
- package/dist/list-suite.js.map +1 -0
- package/dist/modules.d.ts +209 -0
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +85 -0
- package/dist/modules.js.map +1 -1
- package/dist/permission-suite.d.ts.map +1 -1
- package/dist/permission-suite.js +17 -0
- package/dist/permission-suite.js.map +1 -1
- package/dist/scope-host-suite.d.ts.map +1 -1
- package/dist/scope-host-suite.js +148 -0
- package/dist/scope-host-suite.js.map +1 -1
- package/package.json +3 -3
package/dist/scope-host-suite.js
CHANGED
|
@@ -144,6 +144,29 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
144
144
|
expect(new Date(row.occurred_at).getTime()).not.toBeNaN();
|
|
145
145
|
expect(row.pii_class).toBe('none');
|
|
146
146
|
});
|
|
147
|
+
it('ctx.now() is one instant for the whole operation, and the envelope agrees (#812)', async () => {
|
|
148
|
+
const stub = await host.getScope(alice, t1, s1);
|
|
149
|
+
const { first, second } = await stub.invoke('test/now');
|
|
150
|
+
// A valid instant, and the SAME one on both reads. Two rows written in one
|
|
151
|
+
// transaction cannot disagree about when they were written — that is the
|
|
152
|
+
// promise a frozen clock rests on, not an implementation detail.
|
|
153
|
+
expect(instant.safeParse(first).success).toBe(true);
|
|
154
|
+
expect(second).toBe(first);
|
|
155
|
+
// And the event announcing the work carries the same instant as the work.
|
|
156
|
+
const rows = await stub.invoke('test/read-outbox');
|
|
157
|
+
const row = rows[rows.length - 1];
|
|
158
|
+
expect(row.occurred_at).toBe(first);
|
|
159
|
+
});
|
|
160
|
+
it('ctx.now() advances between operations — stable is not frozen (#812)', async () => {
|
|
161
|
+
const stub = await host.getScope(alice, t1, s1);
|
|
162
|
+
const a = await stub.invoke('test/now');
|
|
163
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
164
|
+
const b = await stub.invoke('test/now');
|
|
165
|
+
// Held within an operation, re-read for the next one. A host that stamped
|
|
166
|
+
// once per process would pass every assertion above and be useless.
|
|
167
|
+
expect(Date.parse(b.first)).toBeGreaterThanOrEqual(Date.parse(a.first));
|
|
168
|
+
expect(b.first).not.toBe(a.first);
|
|
169
|
+
});
|
|
147
170
|
it('rejects PII-classed events without a subjectId (§6.1)', async () => {
|
|
148
171
|
const stub = await host.getScope(alice, t1, s1);
|
|
149
172
|
await expect(stub.invoke('test/emit-unclassified-pii')).rejects.toThrow(/subjectId/);
|
|
@@ -246,6 +269,44 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
246
269
|
expect((await host.listPlatformRequestHistory(t1, s1, { status: 'failed' })).every((r) => r.status === 'failed')).toBe(true);
|
|
247
270
|
expect((await host.listPlatformRequestHistory(t1, s1, { limit: 1 })).length).toBe(1);
|
|
248
271
|
});
|
|
272
|
+
/**
|
|
273
|
+
* #841. `lastError` says WHAT happened; this says WHO refused — and until it existed the
|
|
274
|
+
* dashboard assumed the provider every time, so a `permission denied: protocol:read`
|
|
275
|
+
* raised by our own check, before a byte left the platform, was rendered as the provider's
|
|
276
|
+
* own words. Pinned in the shared suite because both adapters store it in their own spine
|
|
277
|
+
* table, and a read-back that disagreed between them would be worse than no read at all.
|
|
278
|
+
*/
|
|
279
|
+
it('journals WHO refused beside what was refused, and survives the round trip (#841)', async () => {
|
|
280
|
+
const stub = await host.getScope(alice, t1, s1);
|
|
281
|
+
const id = await stub.invoke('platform/request', { kind: 'connector:test', payload: { doc: 9 } });
|
|
282
|
+
const row = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
|
|
283
|
+
await host.settlePlatformRequest(t1, s1, row.id, {
|
|
284
|
+
status: 'failed',
|
|
285
|
+
lastError: 'permission denied: protocol:read',
|
|
286
|
+
failure: { origin: 'platform', code: 'permission_denied', permission: 'protocol:read' },
|
|
287
|
+
});
|
|
288
|
+
const settled = (await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' })).find((r) => r.id === id);
|
|
289
|
+
expect(settled.failure).toEqual({
|
|
290
|
+
origin: 'platform',
|
|
291
|
+
code: 'permission_denied',
|
|
292
|
+
permission: 'protocol:read',
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
/**
|
|
296
|
+
* A caller that does not attribute leaves the column NULL, and NULL must read back as
|
|
297
|
+
* "nobody classified this" rather than as an origin nobody decided. This is the shape a
|
|
298
|
+
* control plane older than the field produces, and the drawer renders it by saying
|
|
299
|
+
* nothing about origin at all.
|
|
300
|
+
*/
|
|
301
|
+
it('leaves attribution null when the settling caller did not attribute (#841)', async () => {
|
|
302
|
+
const stub = await host.getScope(alice, t1, s1);
|
|
303
|
+
const id = await stub.invoke('platform/request', { kind: 'connector:test', payload: { doc: 10 } });
|
|
304
|
+
const row = (await host.listPlatformRequests(t1, s1)).find((r) => r.id === id);
|
|
305
|
+
await host.settlePlatformRequest(t1, s1, row.id, { status: 'failed', lastError: 'something broke' });
|
|
306
|
+
const settled = (await host.listPlatformRequestHistory(t1, s1, { kind: 'connector:test' })).find((r) => r.id === id);
|
|
307
|
+
expect(settled.lastError).toBe('something broke');
|
|
308
|
+
expect(settled.failure).toBeNull();
|
|
309
|
+
});
|
|
249
310
|
it('newest first, so a limit is a recency window rather than an arbitrary page (#618)', async () => {
|
|
250
311
|
const stub = await host.getScope(alice, t1, s1);
|
|
251
312
|
const first = await stub.invoke('platform/request', { kind: 'order-check', payload: { n: 1 } });
|
|
@@ -1583,6 +1644,93 @@ export function scopeHostContractSuite(adapterName, makeFixture, opts = {}) {
|
|
|
1583
1644
|
expect(await host.admin.listConnectionGrants(staff, gt)).toEqual([]);
|
|
1584
1645
|
});
|
|
1585
1646
|
});
|
|
1647
|
+
// -- the read-back: what the SCOPE says a connection may do (#726 gap 1) ----
|
|
1648
|
+
//
|
|
1649
|
+
// The grants above are the DIRECTORY's record. This is the other half, and the
|
|
1650
|
+
// one a deployment can reach: the delivered tuples, read from where they are
|
|
1651
|
+
// enforced. #716 found `protocol:attach` missing from a live connection after
|
|
1652
|
+
// months of silent failure precisely because nothing outside the control plane
|
|
1653
|
+
// could answer this question.
|
|
1654
|
+
//
|
|
1655
|
+
// The invariant that matters is agreement with the checker: whatever this
|
|
1656
|
+
// returns must be exactly what `check` would allow for that connection in that
|
|
1657
|
+
// scope. A read-back that disagreed would be worse than none, because it is the
|
|
1658
|
+
// read an operator would believe. That agreement is asserted against real
|
|
1659
|
+
// evaluation in `permission-suite` (which has the probe operation); what is
|
|
1660
|
+
// pinned here is the shape — liveness, inheritance, and per-connection narrowing.
|
|
1661
|
+
describe('connection grants, read back from the scope (#726)', () => {
|
|
1662
|
+
const rt = tenantId.parse(ulid());
|
|
1663
|
+
const rs = scopeId.parse(ulid());
|
|
1664
|
+
const rconn = connectionId.parse(ulid());
|
|
1665
|
+
const SCOPED = permissionKey.parse('protocol:record-signature');
|
|
1666
|
+
const INHERITED = permissionKey.parse('protocol:attach');
|
|
1667
|
+
const EXPIRED = permissionKey.parse('protocol:read');
|
|
1668
|
+
beforeAll(async () => {
|
|
1669
|
+
await host.admin.createTenant(staff, { id: rt, slug: 'tenant-readback', name: 'Readback' });
|
|
1670
|
+
await host.provisionScope(staff, { tenantId: rt, scopeId: rs, vertical: 'signing' });
|
|
1671
|
+
await host.admin.activateScope(staff, rt, rs);
|
|
1672
|
+
await host.admin.createConnection(staff, {
|
|
1673
|
+
id: rconn,
|
|
1674
|
+
tenantId: rt,
|
|
1675
|
+
vertical: 'signing',
|
|
1676
|
+
provider: 'signer',
|
|
1677
|
+
label: 'Signer',
|
|
1678
|
+
secret: { accessToken: 'tok-readback' },
|
|
1679
|
+
});
|
|
1680
|
+
// Scope-targeted, tenant-wide, and one already past its expiry.
|
|
1681
|
+
await host.admin.grantToConnection(staff, {
|
|
1682
|
+
connectionId: rconn,
|
|
1683
|
+
permission: SCOPED,
|
|
1684
|
+
node: { tenantId: rt, scopeId: rs },
|
|
1685
|
+
grantedBy: staff,
|
|
1686
|
+
});
|
|
1687
|
+
await host.admin.grantToConnection(staff, {
|
|
1688
|
+
connectionId: rconn,
|
|
1689
|
+
permission: INHERITED,
|
|
1690
|
+
node: { tenantId: rt, scopeId: null },
|
|
1691
|
+
grantedBy: staff,
|
|
1692
|
+
});
|
|
1693
|
+
await host.admin.grantToConnection(staff, {
|
|
1694
|
+
connectionId: rconn,
|
|
1695
|
+
permission: EXPIRED,
|
|
1696
|
+
node: { tenantId: rt, scopeId: rs },
|
|
1697
|
+
expiresAt: instant.parse('2020-01-01T00:00:00.000Z'),
|
|
1698
|
+
grantedBy: staff,
|
|
1699
|
+
});
|
|
1700
|
+
});
|
|
1701
|
+
it('answers with the live grants, tenant-wide ones included', async () => {
|
|
1702
|
+
const grants = await host.connectionGrantsInScope(rt, rs);
|
|
1703
|
+
expect(grants.map((g) => g.permission).sort()).toEqual([INHERITED, SCOPED].sort());
|
|
1704
|
+
expect(grants.every((g) => g.connectionId === rconn)).toBe(true);
|
|
1705
|
+
});
|
|
1706
|
+
it('omits an expired grant — it would not be enforced either', async () => {
|
|
1707
|
+
const grants = await host.connectionGrantsInScope(rt, rs);
|
|
1708
|
+
expect(grants.map((g) => g.permission)).not.toContain(EXPIRED);
|
|
1709
|
+
});
|
|
1710
|
+
it('is scoped to the connection — another connection in the tenant is not reported', async () => {
|
|
1711
|
+
const other = connectionId.parse(ulid());
|
|
1712
|
+
await host.admin.createConnection(staff, {
|
|
1713
|
+
id: other,
|
|
1714
|
+
tenantId: rt,
|
|
1715
|
+
vertical: 'signing',
|
|
1716
|
+
provider: 'other-signer',
|
|
1717
|
+
label: 'Other',
|
|
1718
|
+
secret: { accessToken: 'tok-other' },
|
|
1719
|
+
});
|
|
1720
|
+
await host.admin.grantToConnection(staff, {
|
|
1721
|
+
connectionId: other,
|
|
1722
|
+
permission: EXPIRED,
|
|
1723
|
+
node: { tenantId: rt, scopeId: rs },
|
|
1724
|
+
grantedBy: staff,
|
|
1725
|
+
});
|
|
1726
|
+
const byConnection = new Map();
|
|
1727
|
+
for (const g of await host.connectionGrantsInScope(rt, rs)) {
|
|
1728
|
+
byConnection.set(g.connectionId, [...(byConnection.get(g.connectionId) ?? []), g.permission]);
|
|
1729
|
+
}
|
|
1730
|
+
expect(byConnection.get(other)).toEqual([EXPIRED]);
|
|
1731
|
+
expect(byConnection.get(rconn)?.sort()).toEqual([INHERITED, SCOPED].sort());
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1586
1734
|
// -- connector state: a connector's own durable bookkeeping (#101 gap 3) ---
|
|
1587
1735
|
describe('connector state', () => {
|
|
1588
1736
|
it('round-trips arbitrary JSON keyed by (connection, key)', async () => {
|