|
51 | 51 | // authz-row: anonymous-deny-packages |
52 | 52 | // authz-row: anonymous-deny-analytics |
53 | 53 | // authz-row: anonymous-deny-api-description |
| 54 | +// authz-row: anonymous-deny-user-work |
54 | 55 | // authz-row: anonymous-deny-i18n |
55 | 56 | // |
56 | 57 | // ── Why the translation faces are here (#22432) ──────────────────────────── |
@@ -180,6 +181,26 @@ const API_DESCRIPTION_ENDPOINTS: readonly ApiDescriptionEndpoint[] = [ |
180 | 181 | const RETIRED_VIEWER_PATH = '/docs'; |
181 | 182 | const NEVER_SERVED_PATH = '/this-route-does-not-exist'; |
182 | 183 |
|
| 184 | +// ── #30205 — a user's assigned work, counted and handed over (@objectstack/rest) |
| 185 | +// |
| 186 | +// Both handlers open with the floor, ahead of the query-parameter check and |
| 187 | +// any engine read. The ids are ones no user holds, so the member controls |
| 188 | +// read nothing and write nothing: the count answers an empty summary, and the |
| 189 | +// hand-over refuses the unknown target before any write. |
| 190 | +interface UserWorkFace { |
| 191 | + readonly face: string; |
| 192 | + readonly method: 'GET' | 'POST'; |
| 193 | + readonly path: string; |
| 194 | + readonly body?: unknown; |
| 195 | +} |
| 196 | +const USER_WORK_FACES: readonly UserWorkFace[] = [ |
| 197 | + { face: 'the assigned-work count', method: 'GET', path: '/security/users/usr_nobody_30205/assigned-work' }, |
| 198 | + { |
| 199 | + face: 'the assigned-work hand-over', method: 'POST', path: '/security/users/usr_nobody_30205/assigned-work/transfer', |
| 200 | + body: { toUserId: 'usr_nobody_either_30205' }, |
| 201 | + }, |
| 202 | +]; |
| 203 | + |
183 | 204 | // ── #22432 — the translation faces (dispatcher-mounted; runtime domains/i18n.ts) |
184 | 205 | // |
185 | 206 | // One handler body serves every face, so the floor is its first statement. The |
@@ -809,6 +830,40 @@ describe('showcase: anonymous posture is uniform across surfaces (#2567)', () => |
809 | 830 | expect(await adminRes.text(), 'the retired path answers the admin the ordinary not-found body').toBe(controlBody); |
810 | 831 | }); |
811 | 832 |
|
| 833 | + // ── a user's assigned work (@objectstack/rest) — #30205 ───────────────── |
| 834 | + // |
| 835 | + // The refusal is each handler's first act, so the anonymous body is the REST |
| 836 | + // seam's shared one, whole, and nothing is counted or moved. The member |
| 837 | + // controls are the teeth: with a session the count is served 200 and the |
| 838 | + // hand-over reaches its own target check (404), so both doors are mounted |
| 839 | + // on this boot and the 401 is the floor's answer. |
| 840 | + it.each(USER_WORK_FACES)('an anonymous caller on $face is denied (401) and served nothing', async (face) => { |
| 841 | + const r = await anon(face.method, face.path, face.body); |
| 842 | + expect(r.status, `${face.face}: an anonymous caller must be refused by the floor`).toBe(ANONYMOUS_DENY_STATUS); |
| 843 | + const body = await r.json(); |
| 844 | + expect(body, `${face.face}: the refusal is the REST seam's shared body, whole`).toEqual(ANONYMOUS_DENY_BODY); |
| 845 | + expect(body, `${face.face}: nothing of the work rides on the refusal`).not.toHaveProperty('data'); |
| 846 | + }); |
| 847 | + |
| 848 | + it('a signed-in member is served the assigned-work count (200) — the control', async () => { |
| 849 | + const r = await stack.apiAs(memberToken, 'GET', USER_WORK_FACES[0].path); |
| 850 | + expect(r.status, 'an authenticated member must clear the floor and be served the count').toBe(200); |
| 851 | + const body = await r.json(); |
| 852 | + expect(body.data, 'the served body is the count of a user who holds nothing').toMatchObject({ |
| 853 | + userId: 'usr_nobody_30205', total: 0, objects: [], |
| 854 | + }); |
| 855 | + }); |
| 856 | + |
| 857 | + it('a signed-in member reaches the hand-over\'s own target check (404), and nothing moves — the control', async () => { |
| 858 | + const face = USER_WORK_FACES[1]; |
| 859 | + const r = await stack.apiAs(memberToken, face.method, face.path, face.body); |
| 860 | + expect(r.status, 'an authenticated member must clear the floor and meet the target check').toBe(404); |
| 861 | + const body = await r.json(); |
| 862 | + expect(body.error?.details?.fields?.[0], 'the refusal names the unknown target').toMatchObject({ |
| 863 | + field: 'toUserId', code: 'reference_not_found', |
| 864 | + }); |
| 865 | + }); |
| 866 | + |
812 | 867 | // ── the translation faces (dispatcher-mounted; runtime domains/i18n.ts) — #22432 |
813 | 868 | // |
814 | 869 | // The same domain-wide shape as analytics: the floor is the handler's first |
|
0 commit comments