|
16 | 16 | * refused the submit outright (#6920) |
17 | 17 | * - `GET /forms/:slug/lookup/:field` answered 403 for every field (#3022) |
18 | 18 | * |
19 | | - * The first two clear on the stored-row path and are pinned below. ⚠️ The THIRD |
20 | | - * does not, and not because the fold missed it: the route's `publicPicker` |
21 | | - * opt-in is not a spec-declared key, so a form carrying one is refused 422 at |
22 | | - * `saveMetaItem` and never becomes a row. That is measured, and pinned as a |
23 | | - * boundary case rather than dropped — see the case for the reasoning. |
| 19 | + * The first two clear on the stored-row path and are pinned below. The THIRD |
| 20 | + * cleared later, in two steps: the fold reached its `sections` walk on this |
| 21 | + * card, and #7467 made the route's `publicPicker` opt-in spec-declarable (it |
| 22 | + * used to be refused 422 at `saveMetaItem`, so no stored row could carry one). |
| 23 | + * What was pinned here as a `BOUNDARY: STILL 403` case is now the flipped |
| 24 | + * pin below: a stored form carrying a picker gets a real lookup answer. |
24 | 25 | * |
25 | 26 | * ⛔ The fix is NOT `?? match.form?.groups` here. The #6926 guardrail stands: a |
26 | 27 | * lenient consumer is where AI-authored metadata errors hide, and it leaves the |
@@ -115,12 +116,11 @@ const DATA = { provider: 'object', object: 'lead' }; |
115 | 116 | /** |
116 | 117 | * The section every case declares. |
117 | 118 | * |
118 | | - * ⚠️ It deliberately carries NO `publicPicker`, and cannot: measured on this |
119 | | - * branch, `ViewMetadataSchema` refuses that key outright ("Unrecognized key(s) |
120 | | - * on this view/page schema: `publicPicker`", ADR-0089 D3a), so a form declaring |
121 | | - * one is a 422 at `saveMetaItem` and never becomes a stored row at all. See the |
122 | | - * lookup-route case below for what that means for the third degradation #7134 |
123 | | - * listed. |
| 119 | + * It carries NO `publicPicker`, which — since #7467 declared that key — is a |
| 120 | + * choice rather than a necessity (when this file was written the schema |
| 121 | + * refused the key outright, so no stored row could carry one). A picker-less |
| 122 | + * field keeps the lookup route at 403, which the flipped case below contrasts |
| 123 | + * against its own picker-carrying fixture. |
124 | 124 | */ |
125 | 125 | const SECTION = { |
126 | 126 | label: 'About you', |
@@ -183,9 +183,11 @@ function routesOver(storedView: any) { |
183 | 183 | return []; |
184 | 184 | }), |
185 | 185 | createData, |
186 | | - // The lookup route's search, once it gets past the 403. |
187 | | - queryData: vi.fn().mockResolvedValue({ records: [{ id: 'usr_1', name: 'Ada' }] }), |
188 | | - findData: vi.fn().mockResolvedValue({ records: [{ id: 'usr_1', name: 'Ada' }] }), |
| 186 | + // The lookup route's search, once it gets past the 403. The route reads |
| 187 | + // `result.data` (or `result.items`) — the flipped #7467 case below |
| 188 | + // asserts the projected rows, so the stub answers in that shape. |
| 189 | + queryData: vi.fn().mockResolvedValue({ data: [{ id: 'usr_1', name: 'Ada' }] }), |
| 190 | + findData: vi.fn().mockResolvedValue({ data: [{ id: 'usr_1', name: 'Ada' }] }), |
189 | 191 | }; |
190 | 192 | const rest = new RestServer(mockServer() as any, protocol, { api: { requireAuth: false } } as any); |
191 | 193 | (rest as any).resolveExecCtx = async () => ({ userId: 'test-user' }); |
@@ -229,31 +231,42 @@ describe('#7134 a Studio-saved form authored with `groups` no longer degrades on |
229 | 231 | expect(createData.mock.calls[0][0].data).toEqual({ company: 'Acme', owner: 'usr_1' }); |
230 | 232 | }); |
231 | 233 |
|
232 | | - it('BOUNDARY: the lookup route is STILL 403 — for a different reason, which is not this card\'s', async () => { |
233 | | - // #7134 listed `GET /forms/:slug/lookup/:field` answering 403 for every |
234 | | - // field as the third degradation. On the stored-row path that half does |
235 | | - // NOT clear, and the honest reason is worth pinning rather than |
236 | | - // quietly dropping. |
| 234 | + it('FLIPPED [#7467]: the lookup route answers — a stored form carrying a publicPicker gets real data', async () => { |
| 235 | + // This case was born as `BOUNDARY: the lookup route is STILL 403 — for |
| 236 | + // a different reason`: #7134 listed the lookup route's blanket 403 as |
| 237 | + // the third degradation, the fold reached its `sections` walk (the two |
| 238 | + // routes above are the same walk, and they changed), but the route's |
| 239 | + // `publicPicker` opt-in was declared in NO schema — `ViewMetadataSchema` |
| 240 | + // is strict, so a form carrying one was refused 422 by `saveMetaItem` |
| 241 | + // and could never reach a row. The original assertion said so and was |
| 242 | + // annotated to be revisited the day a spec-side home landed. |
237 | 243 | // |
238 | | - // The route's opt-in is `publicPicker` on the field declaration. That |
239 | | - // key is not declared anywhere in `packages/spec` (grepped: zero hits), |
240 | | - // and `ViewMetadataSchema` is strict — so a form carrying one is |
241 | | - // refused 422 by `saveMetaItem` and can never reach a row. The lookup |
242 | | - // route therefore answers 403 on the stored-row path both before and |
243 | | - // after this change: before because the fold left it no `sections` to |
244 | | - // walk, after because there is no picker to find in them. |
| 244 | + // #7467 landed it (maintainer ruling: declare — `FormFieldSchema. |
| 245 | + // publicPicker`, mirroring exactly what this route reads), so this is |
| 246 | + // the revisit: the SAME real write path now persists the picker, the |
| 247 | + // SAME real route handler gets past the 403, and the third degradation |
| 248 | + // clears end-to-end. `object` is declared on the picker because the |
| 249 | + // route's field-def fallback reads only legacy spellings — #7486. |
245 | 250 | // |
246 | | - // The fold DOES reach this route's section walk — the two routes above |
247 | | - // are the same walk, and they changed. What is missing is a spec-side |
248 | | - // home for `publicPicker`, which is a separate card (filed from this |
249 | | - // one; enforced-in-rest, undeclared-in-spec — the mirror of ADR-0049's |
250 | | - // usual direction). This assertion goes RED the day that lands, which |
251 | | - // is when someone should revisit it. |
252 | | - const { lookup } = routesOver(await persistedBody('lead.contact', studioForm('groups'))); |
| 251 | + // The picker-less half of the old pin is not lost: a stored form whose |
| 252 | + // field declares no picker still answers 403, pinned in |
| 253 | + // `public-form-lookup-picker.test.ts` (the opt-in stays an opt-in). |
| 254 | + const withPicker = { |
| 255 | + ...studioForm('groups'), |
| 256 | + config: { |
| 257 | + type: 'simple', data: DATA, sharing: SHARING, |
| 258 | + groups: [{ |
| 259 | + label: 'About you', |
| 260 | + fields: ['company', { field: 'owner', publicPicker: { displayFields: ['name'], object: 'sys_user' } }], |
| 261 | + }], |
| 262 | + }, |
| 263 | + }; |
| 264 | + const { lookup } = routesOver(await persistedBody('lead.contact', withPicker)); |
253 | 265 | const res = mockRes(); |
254 | 266 | await lookup.handler({ params: { slug: 'contact', field: 'owner' }, query: {} } as any, res); |
255 | | - expect(res.statusCode).toBe(403); |
256 | | - expect(res.body.code).toBe('LOOKUP_NOT_PUBLIC'); |
| 267 | + expect(res.statusCode).toBe(200); |
| 268 | + expect(res.body.data).toEqual([{ id: 'usr_1', name: 'Ada' }]); |
| 269 | + expect(res.body.displayFields).toEqual(['name']); |
257 | 270 | }); |
258 | 271 |
|
259 | 272 | it('the stored row itself is the canonical spelling — nothing here reads `groups`', async () => { |
|
0 commit comments