Skip to content

Commit 4d2b191

Browse files
committed
test(rest,dogfood): re-pin the public picker on a deployment without the guest set to the deny baseline
On a deployment that registers no guest set the picker's context resolves no permission set, so the engine refuses it at object admission: every listing, search and masked-only picker answers 403 PERMISSION_DENIED and serves nothing, for a visitor and a signed-in caller alike. The key the picker composes is unchanged and still never names the masked field; the guest-set and lifted-rule classes keep their pins. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2e1564f commit 4d2b191

2 files changed

Lines changed: 111 additions & 41 deletions

File tree

‎packages/qa/dogfood/test/public-picker-queryable-key.dogfood.test.ts‎

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22
//
33
// [#21062] A public lookup picker whose FIRST display field declares a masking
4-
// rule serves its rows, on a real boot, to a caller the rule applies to.
4+
// rule served its rows, on a real boot, to a caller the rule applies to.
55
//
66
// The picker searches and sorts by one key. It is the first display field the
77
// caller may query on, by the security service's published answer
@@ -10,13 +10,21 @@
1010
// searched or sorted on, so a picker keyed on it answered `403` to every such
1111
// caller.
1212
//
13-
// What is asserted, by class, for a visitor with no session on a deployment
14-
// that registers no profile for public forms: the scene is real (a system read
15-
// carries the stored values); the rows are sorted on the next queryable display
16-
// field, and the search matches it; the masked field is still served, masked;
17-
// a signed-in caller reaching the same door is served the same (the door builds
18-
// its own context); and a picker whose only display field is masked answers
19-
// the engine's refusal, `403 PERMISSION_DENIED`.
13+
// [#21079] On a deployment that registers no profile for public forms, the
14+
// picker's context resolves NO permission set, and an empty set list is the
15+
// ADR-0056 D2 deny baseline: the engine refuses that caller at object
16+
// admission, before any field guard. So on this fixture every picker answers
17+
// `403 PERMISSION_DENIED` and serves nothing — the key it composes is unchanged
18+
// (the unit pin `public-form-lookup-picker-queryable-key.test.ts` holds that),
19+
// but no row leaves the door. The picker is retired on its own card; until
20+
// then this is what it answers here.
21+
//
22+
// What is asserted, by class, for a visitor with no session on that
23+
// deployment: the scene is real (a system read carries the stored values); the
24+
// listing and the search are both refused at object admission, with no stored
25+
// value in the answer; a signed-in caller reaching the same door is refused the
26+
// same (the door builds its own context); and a picker whose only display field
27+
// is masked is refused with the same code and status.
2028
//
2129
// `bootStack` with the real `SecurityPlugin`, `ObjectQL`, SQL driver, REST and
2230
// auth layers. `@objectstack/rest` resolves to its BUILT output here (no source
@@ -38,7 +46,6 @@ const SEED = [
3846
{ name: 'Alpha Synthetic', [KEY]: 'SYNTH3CCC' },
3947
{ name: 'Charlie Synthetic', [KEY]: 'SYNTH2BBB' },
4048
];
41-
const BY_NAME = ['Alpha Synthetic', 'Bravo Synthetic', 'Charlie Synthetic'];
4249

4350
const PqkeyContact = ObjectSchema.create({
4451
name: CONTACT,
@@ -103,19 +110,19 @@ const pqkeyStack = defineStack({
103110
type Stack = Parameters<typeof bootStack>[0];
104111
type Row = Record<string, unknown>;
105112

106-
function expectServedMasked(rows: Row[]): void {
107-
const stored = new Map(SEED.map((r) => [r.name, r[KEY]]));
108-
for (const row of rows) {
109-
const value = row[KEY];
110-
expect(typeof value, 'the masked field is served, as a string').toBe('string');
111-
expect(value).not.toBe(stored.get(String(row.name)));
112-
expect(String(value)).toContain('*');
113-
}
113+
/** [#21079] The deny baseline's answer at the door: refused at object admission, nothing served. */
114+
async function expectRefusedAtAdmission(res: Response, what: string): Promise<void> {
115+
const text = await res.clone().text();
116+
expect(res.status, `${what}: ${text}`).toBe(403);
117+
const refusal = (await res.json()) as { code?: unknown; error?: { code?: unknown }; data?: unknown };
118+
expect(refusal.code ?? refusal.error?.code, what).toBe('PERMISSION_DENIED');
119+
expect(refusal.data, `${what}: no row is served`).toBeUndefined();
120+
for (const row of SEED) expect(text, `${what}: no stored value leaves the door`).not.toContain(String(row[KEY]));
114121
}
115122

116-
describe('[#21062] a public picker whose first display field is masked serves rows sorted on the next queryable one', () => {
123+
describe('[#21062] a public picker whose first display field is masked: [#21079] refused at object admission on a deployment without the public-form profile', () => {
117124
it(
118-
'to a visitor with no session and to a signed-in caller; a picker with no queryable display field is refused',
125+
'to a visitor with no session and to a signed-in caller; a picker with no queryable display field is refused the same',
119126
async () => {
120127
const stack = await bootStack(pqkeyStack as unknown as Stack);
121128
try {
@@ -130,24 +137,14 @@ describe('[#21062] a public picker whose first display field is masked serves ro
130137
['a visitor with no session', (path: string) => stack.api(path)],
131138
['a signed-in caller', (path: string) => stack.apiAs(token, 'GET', path)],
132139
] as const) {
133-
const listed = await call('/forms/pqkey-intake/lookup/contact');
134-
expect(listed.status, who).toBe(200);
135-
const rows = ((await listed.json()) as { data: Row[] }).data;
136-
expect(rows.map((r) => r.name), `${who}: sorted on the next queryable display field`).toEqual(BY_NAME);
137-
expectServedMasked(rows);
138-
139-
const searched = await call('/forms/pqkey-intake/lookup/contact?q=Charlie');
140-
expect(searched.status, who).toBe(200);
141-
const hits = ((await searched.json()) as { data: Row[] }).data;
142-
expect(hits.map((r) => r.name), `${who}: the search matches the next queryable display field`)
143-
.toEqual(['Charlie Synthetic']);
144-
expectServedMasked(hits);
140+
await expectRefusedAtAdmission(await call('/forms/pqkey-intake/lookup/contact'), `${who}: the listing`);
141+
await expectRefusedAtAdmission(await call('/forms/pqkey-intake/lookup/contact?q=Charlie'), `${who}: the search`);
145142
}
146143

147-
const refused = await stack.api('/forms/pqkey-intake/lookup/contact_masked_only');
148-
expect(refused.status).toBe(403);
149-
const refusal = (await refused.json()) as { code?: unknown; error?: { code?: unknown } };
150-
expect(refusal.code ?? refusal.error?.code).toBe('PERMISSION_DENIED');
144+
await expectRefusedAtAdmission(
145+
await stack.api('/forms/pqkey-intake/lookup/contact_masked_only'),
146+
'a picker with no queryable display field',
147+
);
151148
} finally {
152149
await stack.stop();
153150
}

‎packages/rest/src/public-form-lookup-picker-queryable-key.test.ts‎

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
* What is pinned, by caller class (the picker's own context — the route builds
1818
* it, the session is not read):
1919
*
20-
* - masked classes — the deployment registers no `guest_portal` set (the
21-
* context resolves no set), or registers one that does not hold the
22-
* capability the rule names: rows are sorted, and searched, on the next
23-
* queryable display field; the masked field is still served, masked; and no
24-
* request the engine receives names the masked field;
20+
* - masked class — the deployment registers a `guest_portal` set that does not
21+
* hold the capability the rule names: rows are sorted, and searched, on the
22+
* next queryable display field; the masked field is still served, masked;
23+
* and no request the engine receives names the masked field;
24+
* - [#21079] the deployment registers no `guest_portal` set, so the context
25+
* resolves no set: the ADR-0056 D2 deny baseline refuses every picker at
26+
* object admission (`403 PERMISSION_DENIED`), while the key it composes is
27+
* unchanged and still never names the masked field;
2528
* - a picker with no queryable display field answers the engine's own refusal
2629
* for those fields, and the engine is never asked;
2730
* - controls — a picker with no masked display field, and a caller the rule is
@@ -286,8 +289,78 @@ describe('[#21062] the fixture is authorable', () => {
286289
});
287290
});
288291

292+
/**
293+
* [#21079] The deployment registers no guest set, so the picker's context
294+
* resolves NO permission set — and an empty set list is the ADR-0056 D2 deny
295+
* baseline: the engine refuses that caller at object admission, before any
296+
* field guard, whatever the query names. The field answers are unchanged (the
297+
* masked field is readable and not queryable, and the key the picker composes
298+
* is still the next queryable display field), but nothing is served: every
299+
* picker on this deployment answers `403 PERMISSION_DENIED`. The picker itself
300+
* is retired on its own card; until then this is what it answers here.
301+
*/
302+
describe('[#21062] a picker whose first display field is masked for its caller — the deployment registers no guest set (the context resolves none): [#21079] refused at object admission', () => {
303+
let h: Harness;
304+
beforeAll(async () => { h = await boot([MEMBER_SET]); }, 60_000);
305+
afterAll(async () => { try { await h?.engine.destroy(); } catch { /* noop */ } });
306+
307+
/** The deny baseline's answer at the door: refused at object admission, nothing served. */
308+
function expectRefusedAtAdmission(res: { status: number; body: any }): void {
309+
expect({ status: res.status, code: res.body?.code }, JSON.stringify(res.body)).toEqual({ status: 403, code: 'PERMISSION_DENIED' });
310+
expect(res.body?.data, 'no row is served').toBeUndefined();
311+
for (const stored of Object.values(STORED_BY_ID)) expect(JSON.stringify(res.body)).not.toContain(stored);
312+
}
313+
314+
it('the premise: the security service answers the masked field readable and not queryable', async () => {
315+
const context = { permissions: ['guest_portal'], anonymous: true };
316+
expect(await h.security.getReadableFields(CONTACT, context)).toContain(MASKED);
317+
expect(await h.security.getQueryableFields(CONTACT, context)).not.toContain(MASKED);
318+
expect(await engineRefusal(h.engine, [MASKED])).toMatchObject({ status: 403, code: 'PERMISSION_DENIED' });
319+
});
320+
321+
it('[#21079] the picker whose first display field is masked is refused at object admission', async () => {
322+
expectRefusedAtAdmission(await h.lookup('c_first'));
323+
});
324+
325+
it('[#21079] its search is refused at object admission too', async () => {
326+
expectRefusedAtAdmission(await h.lookup('c_first', 'Brav'));
327+
});
328+
329+
it('never uses the masked field as a key', async () => {
330+
const before = h.finds().length;
331+
await h.lookup('c_first');
332+
await h.lookup('c_first', 'Brav');
333+
const asked = h.finds().slice(before);
334+
expect(asked).toHaveLength(2);
335+
for (const query of asked) {
336+
expect(queriedFields(query)).not.toContain(MASKED);
337+
expect(query.orderBy).toEqual([{ field: 'name', order: 'asc' }]);
338+
}
339+
});
340+
341+
it('[#21079] a picker with no queryable display field answers 403 PERMISSION_DENIED, as the engine does, and the engine is never asked', async () => {
342+
const reference = await engineRefusal(h.engine, PICKERS.c_only.displayFields);
343+
expect(reference).toMatchObject({ status: 403, code: 'PERMISSION_DENIED' });
344+
const before = h.finds().length;
345+
for (const q of [undefined, 'A']) {
346+
const res = await h.lookup('c_only', q);
347+
expectRefusedAtAdmission(res);
348+
expect({ status: res.status, code: res.body?.code }).toEqual({ status: reference!.status, code: reference!.code });
349+
}
350+
expect(h.finds().length - before, 'the engine was asked').toBe(0);
351+
});
352+
353+
it('[#21079] control: a picker with no masked display field still keys on its first display field, and is refused at object admission too', async () => {
354+
const before = h.finds().length;
355+
expectRefusedAtAdmission(await h.lookup('c_control'));
356+
expectRefusedAtAdmission(await h.lookup('c_control', 'Charl'));
357+
const asked = h.finds().slice(before);
358+
expect(asked).toHaveLength(2);
359+
for (const query of asked) expect(query.orderBy).toEqual([{ field: 'name', order: 'asc' }]);
360+
});
361+
});
362+
289363
for (const [label, sets] of [
290-
['the deployment registers no guest set (the context resolves none)', [MEMBER_SET]],
291364
['the guest set does not hold the capability the rule names', [MEMBER_SET, GUEST_SET]],
292365
] as const) {
293366
describe(`[#21062] a picker whose first display field is masked for its caller — ${label}`, () => {

0 commit comments

Comments
 (0)