Skip to content

Commit fe18ff2

Browse files
committed
test(spec): cover canExport in the security-service contract suite
The stub that "implements the full surface" needs the new member, and the fail-closed posture is worth pinning next to getReadFilter's: `undefined` there means no restriction, `false` here means denied — a consumer that reads them the same way leaks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PDbwCy9Jrc1chhR2vnAUos
1 parent 85ec90d commit fe18ff2

1 file changed

Lines changed: 18 additions & 0 deletions

File tree

packages/spec/src/contracts/security-service.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function makeService(overrides: Partial<ISecurityService> = {}): ISecurityServic
1717
return {
1818
getReadFilter: async () => undefined,
1919
getReadableFields: async () => [],
20+
canExport: async () => true,
2021
resolvePermissionSetNames: async () => [],
2122
explain: async () => ({}) as any,
2223
listAudienceBindingSuggestions: async () => ({
@@ -35,6 +36,7 @@ describe('Security Service Contract', () => {
3536
for (const m of [
3637
'getReadFilter',
3738
'getReadableFields',
39+
'canExport',
3840
'resolvePermissionSetNames',
3941
'explain',
4042
'listAudienceBindingSuggestions',
@@ -45,6 +47,22 @@ describe('Security Service Contract', () => {
4547
}
4648
});
4749

50+
it('canExport: an access-narrowing answer — false denies, and absence is feature-detectable', async () => {
51+
// [#3544] The bulk-egress question. It fails CLOSED, so a consumer must
52+
// never read a `false` (or a throw) as "no restriction" the way it may
53+
// read getReadFilter's `undefined`.
54+
const denied = makeService({ canExport: async () => false });
55+
await expect(denied.canExport('deal', { userId: 'u1' })).resolves.toBe(false);
56+
57+
const allowed = makeService({ canExport: async () => true });
58+
await expect(allowed.canExport('deal', { userId: 'u1' })).resolves.toBe(true);
59+
60+
// A system context bypasses, mirroring the engine middleware's isSystem skip.
61+
const service = makeService({ canExport: async (_object, context) => context?.isSystem === true });
62+
await expect(service.canExport('deal', { isSystem: true })).resolves.toBe(true);
63+
await expect(service.canExport('deal', { userId: 'u1' })).resolves.toBe(false);
64+
});
65+
4866
it('getReadFilter: undefined means NO row restriction — the only thing it may mean', async () => {
4967
// A deny is expressed as a filter that matches nothing, never as `undefined`,
5068
// so a consumer can safely read `undefined` as "apply no filter".

0 commit comments

Comments
 (0)