Skip to content

Commit d10b998

Browse files
committed
test(plugin-security,spec): pin the new engine double to the producer's dispatch predicates (#7414)
`check:engine-double-contract` caught the new fixture's `delete()`/`update()` being looser than `ObjectQL`'s — a fake that accepts call shapes the engine refuses collects greens the producer would not. Both verbs now open with `assertEngineDeleteDispatch` / `assertEngineUpdateDispatch` from `@objectstack/metadata-core`. Also corrects the flag on the catalog's placeholder case: reverse verification measured it going RED on a reverted catalog (it reads the ENTRY, not the rendering), so the comment claiming it cannot bite was wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BM1tNf5U3nEbHKR4fo5qVQ
1 parent 58023c7 commit d10b998

2 files changed

Lines changed: 32 additions & 7 deletions

File tree

packages/plugins/plugin-security/src/permission-denied-user-copy.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
*/
3939

4040
import { describe, it, expect, vi } from 'vitest';
41+
import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core';
4142
import { FileI18nAdapter } from '@objectstack/service-i18n';
4243
import { PermissionSetSchema } from '@objectstack/spec/security';
4344
import type { PermissionSet } from '@objectstack/spec/security';
@@ -126,8 +127,29 @@ function makeEngine() {
126127
return (await this.find(object, options))[0] ?? null;
127128
},
128129
async insert(object: string, data: any) { (tables[object] ??= []).push({ ...data }); return data; },
129-
async update(_object: string, data: any) { return data; },
130-
async delete() { return true; },
130+
// Both write verbs open with the PRODUCER's own dispatch predicate, never a
131+
// hand-mirrored guard: a fake looser than `ObjectQL` collects greens from
132+
// call shapes the engine would refuse. Nothing in this file reaches them
133+
// (the gate refuses first, which is the point), so they exist to keep the
134+
// double honest for whatever case is added next.
135+
async update(object: string, data: any, options?: any) {
136+
const dispatch = assertEngineUpdateDispatch(data, options);
137+
const rows = (tables[object] ??= []);
138+
const targets = dispatch.kind === 'by-id'
139+
? rows.filter((r) => r.id === dispatch.id)
140+
: rows.filter((r) => matches(r, options?.where));
141+
for (const r of targets) Object.assign(r, data);
142+
return dispatch.kind === 'by-id' ? (targets[0] ?? null) : targets.length;
143+
},
144+
async delete(object: string, options?: any) {
145+
const dispatch = assertEngineDeleteDispatch(options);
146+
const rows = (tables[object] ??= []);
147+
const targets = dispatch.kind === 'by-id'
148+
? rows.filter((r) => r.id === dispatch.id)
149+
: rows.filter((r) => matches(r, options?.where));
150+
tables[object] = rows.filter((r) => !targets.includes(r));
151+
return dispatch.kind === 'by-id' ? targets.length > 0 : targets.length;
152+
},
131153
};
132154
}
133155

packages/spec/src/system/operation-message.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,14 @@ describe('operation message catalog — permission_denied (#7414)', () => {
151151
});
152152

153153
it('ships no unfilled placeholder in any locale — the sentence takes no params', () => {
154-
// ⚠️ This case is HYGIENE, not a revert-detector: it also passes on a
155-
// catalog with the key removed entirely (the fallback is the bare
156-
// messageKey, which has no braces either). Its value is catching a
157-
// template that shipped a `{{name}}` / `{name}` nobody fills — the #7333
158-
// class of bug, where the two brace conventions in this repo are mixed up.
154+
// Asserts on the CATALOG ENTRY, not on the rendering, and that is the
155+
// difference between a guard and a decoration. Rendering a removed key
156+
// yields the bare messageKey — which has no braces either, so a
157+
// rendering-based version of this case would stay green on a catalog that
158+
// lost the key entirely. Reading the entry makes it bite twice: on a
159+
// missing locale (the entry is `undefined`) and on a template that shipped
160+
// a `{{name}}` / `{name}` nobody fills, which is the #7333 class of bug
161+
// where this repo's two brace conventions get mixed up.
159162
for (const [locale, catalog] of Object.entries(BUILTIN_OPERATION_MESSAGES)) {
160163
expect(catalog.permission_denied, `${locale} defines permission_denied`).toBeTypeOf('string');
161164
expect(catalog.permission_denied, `${locale} placeholder-free`).not.toMatch(/[{}]/);

0 commit comments

Comments
 (0)