Skip to content

Commit dc0a93d

Browse files
committed
test(metadata-core,metadata-protocol): pin the moved predicate and the save/publish intake advisory
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 207c209 commit dc0a93d

3 files changed

Lines changed: 375 additions & 2 deletions

File tree

‎packages/metadata-core/src/anonymous-form-intake.test.ts‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import { describe, it, expect } from 'vitest';
44
import { SharingConfigSchema } from '@objectstack/spec/ui';
55
import {
66
anonymousFormIntakeCandidates,
7+
anonymousFormIntakePosture,
78
anonymousFormIntakeSlug,
89
anonymousFormIntakeSlugs,
10+
anonymousFormIntakeUnavailability,
11+
anonymousFormIntakeUnavailableMessage,
12+
anonymousFormIntakeUnavailableRemedy,
13+
anonymousFormObjectName,
14+
anonymousFormSharingPath,
915
publicFormSlug,
1016
} from './anonymous-form-intake.js';
1117

@@ -79,3 +85,102 @@ describe('anonymousFormIntakeCandidates / anonymousFormIntakeSlugs — the three
7985
expect(publicFormSlug('//forms/x')).toBe('x');
8086
});
8187
});
88+
89+
// [#21476] Whether an open form can take an anonymous submission on this
90+
// posture — the one predicate both anonymous doors, the admin read and the
91+
// runtime authoring gate's advisory read.
92+
describe('anonymousFormIntakeUnavailability — the intake-availability predicate', () => {
93+
/** The object as a served document carries it: the registry injects `organization_id`. */
94+
const served = (extra: Record<string, unknown> = {}) => ({
95+
name: 'inquiry',
96+
fields: { organization_id: { type: 'lookup', reference: 'sys_organization' }, email: { type: 'text' } },
97+
...extra,
98+
});
99+
/** The same object as a stored or pending body carries it: declared fields only. */
100+
const raw = (extra: Record<string, unknown> = {}) => ({ name: 'inquiry', fields: { email: { type: 'text' } }, ...extra });
101+
102+
it.each(['isolated', 'group'] as const)("'%s': a walled object is unavailable, naming the object, the posture and the column", (posture) => {
103+
expect(anonymousFormIntakeUnavailability('inquiry', posture, () => served()))
104+
.toEqual({ object: 'inquiry', posture, tenantField: 'organization_id' });
105+
});
106+
107+
it('judges the EFFECTIVE schema: a stored body with no declared organization_id is walled all the same', () => {
108+
expect(anonymousFormIntakeUnavailability('inquiry', 'isolated', () => raw()))
109+
.toEqual({ object: 'inquiry', posture: 'isolated', tenantField: 'organization_id' });
110+
});
111+
112+
it('a declared tenancy.tenantField the object really has is the column named', () => {
113+
const schema = served({ tenancy: { tenantField: 'company_id' }, fields: { company_id: { type: 'text' } } });
114+
expect(anonymousFormIntakeUnavailability('inquiry', 'isolated', () => schema)?.tenantField).toBe('company_id');
115+
});
116+
117+
it.each<[string, unknown]>([
118+
['tenancy: { enabled: false } (ADR-0066)', served({ tenancy: { enabled: false } })],
119+
['an object the universe does not hold', undefined],
120+
['an object with no fields record and no wall', { name: 'inquiry', systemFields: false }],
121+
])('CONTROL, walled posture — %s: available', (_label, schema) => {
122+
expect(anonymousFormIntakeUnavailability('inquiry', 'isolated', () => schema)).toBeNull();
123+
});
124+
125+
it.each<[string, 'single' | undefined]>([
126+
["the 'single' posture", 'single'],
127+
['no tenancy service (no posture)', undefined],
128+
])('CONTROL — %s: available, and the object is never read', (_label, posture) => {
129+
let reads = 0;
130+
expect(anonymousFormIntakeUnavailability('inquiry', posture, () => { reads += 1; return served(); })).toBeNull();
131+
expect(reads).toBe(0);
132+
});
133+
134+
it('an asynchronous reader gets a promise when a wall is in force, and null without reading otherwise', async () => {
135+
const walled = anonymousFormIntakeUnavailability('inquiry', 'group', async () => served());
136+
expect(walled).toBeInstanceOf(Promise);
137+
expect(await walled).toEqual({ object: 'inquiry', posture: 'group', tenantField: 'organization_id' });
138+
expect(anonymousFormIntakeUnavailability('inquiry', 'single', async () => served())).toBeNull();
139+
});
140+
});
141+
142+
describe('anonymousFormIntakePosture — the posture IN FORCE, as the tenancy service reports it', () => {
143+
it('reads `posture`, never `requestedPosture`: a degraded walled request is single', () => {
144+
expect(anonymousFormIntakePosture({ posture: 'single', requestedPosture: 'isolated' })).toBe('single');
145+
expect(anonymousFormIntakePosture({ posture: 'group' })).toBe('group');
146+
expect(anonymousFormIntakePosture({ posture: 'multi' })).toBe('isolated');
147+
});
148+
149+
it('no service, or no recognisable posture: undefined', () => {
150+
for (const tenancy of [undefined, null, {}, { posture: 'walled' }, 'isolated']) {
151+
expect(anonymousFormIntakePosture(tenancy)).toBeUndefined();
152+
}
153+
});
154+
});
155+
156+
describe('where the reason is located, and the reason itself', () => {
157+
it('anonymousFormSharingPath: form.sharing, formViews.KEY.sharing, config.sharing', () => {
158+
const view = {
159+
name: 'inquiry.contact',
160+
form: { sharing: { ...OPEN, publicLink: '/forms/nested' } },
161+
formViews: { contact: { sharing: { ...OPEN, publicLink: '/forms/a' } } },
162+
viewKind: 'form',
163+
config: { sharing: { ...OPEN, publicLink: '/forms/flat' } },
164+
};
165+
expect(anonymousFormIntakeCandidates(view).map((c) => anonymousFormSharingPath(view, c)))
166+
.toEqual(['form.sharing', 'formViews.contact.sharing', 'config.sharing']);
167+
});
168+
169+
it('anonymousFormObjectName: the form\'s own data.object first, then the view\'s', () => {
170+
const view = { object: 'v_obj', list: { data: { object: 'list_obj' } } };
171+
expect(anonymousFormObjectName(view, { data: { object: 'form_obj' } })).toBe('form_obj');
172+
expect(anonymousFormObjectName(view, {})).toBe('list_obj');
173+
expect(anonymousFormObjectName({ object: 'v_obj' }, {})).toBe('v_obj');
174+
expect(anonymousFormObjectName(undefined, undefined)).toBeUndefined();
175+
});
176+
177+
it('the message names the slug, the object, the column and the posture, and ends with the remedy', () => {
178+
const u = { object: 'inquiry', posture: 'isolated' as const, tenantField: 'organization_id' };
179+
const message = anonymousFormIntakeUnavailableMessage('contact-us', u);
180+
for (const named of ["'/forms/contact-us'", "'inquiry'", "'organization_id'", "'isolated'"]) {
181+
expect(message).toContain(named);
182+
}
183+
expect(anonymousFormIntakeUnavailableRemedy(u)).toContain('tenancy: { enabled: false }');
184+
expect(message.endsWith(` ${anonymousFormIntakeUnavailableRemedy(u)}`)).toBe(true);
185+
});
186+
});

‎packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts‎

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3030
// would close a dependency cycle turbo rejects outright — which is why all 26
3131
// of this package's (file, verb) pairs sat in the gate's DEBT ledger until
3232
// #5619 sank the two predicates into a package both sides already depend on.
33-
import { assertEngineDeleteDispatch, assertEngineUpdateDispatch, assertEngineFindOnePredicate } from '@objectstack/metadata-core';
33+
import {
34+
assertEngineDeleteDispatch,
35+
assertEngineUpdateDispatch,
36+
assertEngineFindOnePredicate,
37+
// [#21476] The reason the admin read of a view states — the advisory must carry the same bytes.
38+
anonymousFormIntakeUnavailableMessage,
39+
} from '@objectstack/metadata-core';
3440
// [#4716] The advisory-tier rule the Q2 fence test proves its body WOULD trip
3541
// — imported from the full barrel deliberately: this is a TEST, not the gate
3642
// (the gate itself may only reach the registry through `@objectstack/lint/runtime`,
@@ -39,7 +45,7 @@ import { validateSemanticRoles } from '@objectstack/lint';
3945
import { ObjectStackProtocolImplementation } from './protocol.js';
4046
import type { MetadataAuthoringChannel } from './protocol.js';
4147
import { SDUI_MANIFEST_SERVICE } from './index.js';
42-
import { stampHtmlPageRequires } from './runtime-authoring-gate.js';
48+
import { PUBLIC_FORM_INTAKE_UNAVAILABLE, stampHtmlPageRequires } from './runtime-authoring-gate.js';
4349

4450
/** The issue's body. Zod-valid: `approvers[].value` is just a string to the schema. */
4551
const brokenApprovalFlow = () => ({
@@ -1454,3 +1460,131 @@ describe('stored html page `requires` at load and at draft promotion (#20312)',
14541460
expect(storedPage(rows)?.requires).toEqual(['plugin-absent']);
14551461
});
14561462
});
1463+
1464+
/**
1465+
* [#21476] The public-form intake advisory, end to end through the two write
1466+
* doors: `saveMetaItem` (REST `PUT /meta/view/:name`) and the draft → active
1467+
* promotion `publishMetaItem` (REST `POST /meta/view/:name/publish`).
1468+
*
1469+
* The posture is read off a `tenancy` service in the protocol's own services
1470+
* table — the service, and the reader (`anonymousFormIntakePosture`), the
1471+
* anonymous form doors read. The rows pin both halves: on a walled posture in
1472+
* force the write SUCCEEDS with exactly one warning, located at the form's
1473+
* `sharing` and carrying the admin read's reason byte for byte; every control
1474+
* the doors serve the form on raises nothing, the degraded deployment included
1475+
* — whose REQUESTED posture (`OS_TENANCY_POSTURE`) walls while its in-force
1476+
* posture does not.
1477+
*/
1478+
describe('public-form intake advisory on save and publish (#21476)', () => {
1479+
const SLUG = 'contact-us';
1480+
const NAME = 'showcase_inquiry';
1481+
/** The showcase's contact form: a container whose public form is `formViews.contact`. */
1482+
const contactContainer = () => ({
1483+
list: { type: 'grid', data: { provider: 'object', object: NAME }, columns: [{ field: 'name' }] },
1484+
formViews: {
1485+
contact: {
1486+
type: 'simple',
1487+
data: { provider: 'object', object: NAME },
1488+
sections: [{ name: 'about', fields: [{ field: 'name' }, { field: 'email' }] }],
1489+
sharing: { enabled: true, allowAnonymous: true, publicLink: `/forms/${SLUG}` },
1490+
},
1491+
},
1492+
});
1493+
/** The bound object as the live registry holds it — `organization_id` injected at registration. */
1494+
const inquiry = (tenancyDisabled: boolean) => ({
1495+
name: NAME,
1496+
label: 'Inquiry',
1497+
...(tenancyDisabled ? { tenancy: { enabled: false } } : {}),
1498+
fields: {
1499+
organization_id: { type: 'lookup', reference: 'sys_organization' },
1500+
name: { type: 'text', label: 'Name' },
1501+
email: { type: 'email', label: 'Email' },
1502+
},
1503+
});
1504+
1505+
type Tenancy = 'isolated' | 'group' | 'degraded' | 'single' | 'no-service';
1506+
/** A `tenancy` service as plugin-auth registers it: `posture` is the posture IN FORCE. */
1507+
const tenancyService = (t: Exclude<Tenancy, 'no-service'>) => ({
1508+
posture: t === 'degraded' ? 'single' : t,
1509+
requestedPosture: t === 'degraded' ? 'isolated' : t,
1510+
defaultOrgId: async () => (t === 'single' ? 'org_alpha' : null),
1511+
});
1512+
1513+
function hostOn(tenancy: Tenancy, tenancyDisabled = false) {
1514+
const { engine, rows } = makeStubEngine();
1515+
engine.registry.listItems = (type: string) => (type === 'object' ? [inquiry(tenancyDisabled)] : []);
1516+
const services = new Map<string, unknown>(
1517+
tenancy === 'no-service' ? [] : [['tenancy', tenancyService(tenancy)]],
1518+
);
1519+
const protocol = new ObjectStackProtocolImplementation(engine, () => services, 'env_test') as any;
1520+
return { protocol, rows };
1521+
}
1522+
1523+
const put = (protocol: any, extra: Record<string, unknown> = {}) =>
1524+
protocol.saveMetaItem({ type: 'view', name: NAME, item: contactContainer(), ...extra });
1525+
const publish = async (protocol: any) => {
1526+
await expect(put(protocol, { mode: 'draft' })).resolves.toMatchObject({ success: true });
1527+
return protocol.publishMetaItem({ type: 'view', name: NAME });
1528+
};
1529+
const intake = (response: { advisories?: Array<{ rule: string }> }) =>
1530+
(response.advisories ?? []).filter((a) => a.rule === PUBLIC_FORM_INTAKE_UNAVAILABLE);
1531+
1532+
let warn: ReturnType<typeof vi.spyOn>;
1533+
const savedPosture = process.env.OS_TENANCY_POSTURE;
1534+
beforeEach(() => {
1535+
warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
1536+
delete process.env.OS_ALLOW_UNLINTED_METADATA_WRITES;
1537+
});
1538+
afterEach(() => {
1539+
warn.mockRestore();
1540+
if (savedPosture === undefined) delete process.env.OS_TENANCY_POSTURE;
1541+
else process.env.OS_TENANCY_POSTURE = savedPosture;
1542+
});
1543+
1544+
for (const posture of ['isolated', 'group'] as const) {
1545+
for (const [door, write] of [['PUT', put], ['publish', publish]] as const) {
1546+
it(`'${posture}' in force, walled object — ${door} succeeds with exactly one warning, the admin read's reason at the form's sharing`, async () => {
1547+
const { protocol, rows } = hostOn(posture);
1548+
const response = await write(protocol);
1549+
expect(response.success).toBe(true);
1550+
expect(response.advisories).toEqual([{
1551+
severity: 'warning',
1552+
rule: PUBLIC_FORM_INTAKE_UNAVAILABLE,
1553+
where: `view "${NAME}" · public form "/forms/${SLUG}"`,
1554+
path: 'views[0].formViews.contact.sharing',
1555+
message: anonymousFormIntakeUnavailableMessage(SLUG, {
1556+
object: NAME, posture, tenantField: 'organization_id',
1557+
}),
1558+
hint: expect.stringContaining('tenancy: { enabled: false }'),
1559+
}]);
1560+
// Never a refusal: the row landed active.
1561+
expect([...rows.values()].filter((r) => r.type === 'view' && r.state === 'active')).toHaveLength(1);
1562+
});
1563+
}
1564+
}
1565+
1566+
it.each<[string, Tenancy, boolean]>([
1567+
['walled posture, object declared tenancy: { enabled: false }', 'isolated', true],
1568+
["the 'single' posture", 'single', false],
1569+
['no tenancy service registered', 'no-service', false],
1570+
])('CONTROL — %s: PUT and publish raise no intake advisory', async (_label, tenancy, tenancyDisabled) => {
1571+
const { protocol } = hostOn(tenancy, tenancyDisabled);
1572+
const saved = await put(protocol);
1573+
expect(saved.success).toBe(true);
1574+
expect(intake(saved)).toEqual([]);
1575+
const published = await publish(protocol);
1576+
expect(published.success).toBe(true);
1577+
expect(intake(published)).toEqual([]);
1578+
});
1579+
1580+
it('CONTROL — a degraded walled deployment: the REQUESTED posture walls, the posture in force does not; the doors serve, so nothing is raised', async () => {
1581+
process.env.OS_TENANCY_POSTURE = 'isolated';
1582+
const { protocol } = hostOn('degraded');
1583+
const saved = await put(protocol);
1584+
expect(saved.success).toBe(true);
1585+
expect(intake(saved)).toEqual([]);
1586+
const published = await publish(protocol);
1587+
expect(published.success).toBe(true);
1588+
expect(intake(published)).toEqual([]);
1589+
});
1590+
});

0 commit comments

Comments
 (0)