|
33 | 33 | // - list-teams and get-full-organization's teams answer an external the |
34 | 34 | // teams it sits on; list-invitations answers it its own invitations. |
35 | 35 | // |
| 36 | +// [#25549] A share to an external on a `public_read_write` object: |
| 37 | +// |
| 38 | +// - the record's owner shares one invoice with an external over the REST |
| 39 | +// share door and gets 201; the external then reads that invoice, and no |
| 40 | +// other, where before it read none; |
| 41 | +// - the same share to an ordinary member is refused 422 |
| 42 | +// `SHARING_NOT_ENABLED`: on a public object no gate consults it for staff. |
| 43 | +// |
36 | 44 | // Fixtures are synthetic. ⚠️ No test title states a value. |
37 | 45 |
|
38 | 46 | import { describe, it, expect, beforeAll, afterAll } from 'vitest'; |
@@ -179,12 +187,12 @@ describe('[#25233] an external organization member reads its own identity rows a |
179 | 187 | // same read on both objects, and neither owns any of the rows. |
180 | 188 | const portalLines = await catalogPermissionSet(ql, portalLinesSet.name); |
181 | 189 | expect(portalLines, 'fixture invoice permission set served by the security catalog').toBeTruthy(); |
182 | | - for (const who of ['supplierD', 'member'] as const) { |
| 190 | + for (const who of ['supplierD', 'member', 'supplierB'] as const) { |
183 | 191 | await ql.insert('sys_user_permission_set', { user_id: uid[who], permission_set: portalLines.name, organization_id: orgId }, { context: SYS }); |
184 | 192 | } |
185 | 193 | const account = await ql.insert('showcase_account', { name: 'Account 25233', status: 'active', organization_id: orgId }, { context: SYS }); |
186 | 194 | const product = await ql.insert('showcase_product', { name: 'Product 25233', organization_id: orgId }, { context: SYS }); |
187 | | - for (const key of ['shared', 'other'] as const) { |
| 195 | + for (const key of ['shared', 'other', 'grant'] as const) { |
188 | 196 | const inv = await ql.insert(MASTER, { |
189 | 197 | name: `INV-25233-${key}`, account: String(account.id), status: 'draft', owner_id: uid.owner, organization_id: orgId, |
190 | 198 | }, { context: SYS }); |
@@ -362,6 +370,35 @@ describe('[#25233] an external organization member reads its own identity rows a |
362 | 370 | expect((await auth('supplierB', `list-invitations?organizationId=${orgId}`)).body).toEqual([]); |
363 | 371 | }); |
364 | 372 |
|
| 373 | + it('[#25549] the owner shares one public_read_write invoice with an external, who then reads it and no other', async () => { |
| 374 | + // Before: the floored external owns no invoice and holds no share, so it reads none. |
| 375 | + const before = await ids('supplierB', `/data/${MASTER}?limit=200`); |
| 376 | + expect(before.status).toBe(200); |
| 377 | + expect(before.ids).toEqual([]); |
| 378 | + |
| 379 | + const res = await stack.apiAs(token.owner, 'POST', `/data/${MASTER}/${row.inv_grant}/shares`, { |
| 380 | + recipientId: uid.supplierB, accessLevel: 'read', |
| 381 | + }); |
| 382 | + expect(res.status).toBe(201); |
| 383 | + const share = await res.json() as Row; |
| 384 | + expect(share).toMatchObject({ object_name: MASTER, record_id: row.inv_grant, recipient_id: uid.supplierB, organization_id: orgId }); |
| 385 | + |
| 386 | + const after = await ids('supplierB', `/data/${MASTER}?limit=200`); |
| 387 | + expect(after.status).toBe(200); |
| 388 | + expect(after.ids).toEqual([row.inv_grant]); |
| 389 | + }); |
| 390 | + |
| 391 | + it('[#25549] the same share to an ordinary member is refused 422 SHARING_NOT_ENABLED', async () => { |
| 392 | + const res = await stack.apiAs(token.owner, 'POST', `/data/${MASTER}/${row.inv_grant}/shares`, { |
| 393 | + recipientId: uid.member, accessLevel: 'read', |
| 394 | + }); |
| 395 | + expect(res.status).toBe(422); |
| 396 | + const body = await res.json() as { error?: { code?: string; message?: string } }; |
| 397 | + expect(body.error?.code).toBe('SHARING_NOT_ENABLED'); |
| 398 | + const rows = await findRows(ql, 'sys_record_share', { object_name: MASTER, record_id: row.inv_grant, recipient_id: uid.member }); |
| 399 | + expect(rows).toHaveLength(0); |
| 400 | + }); |
| 401 | + |
365 | 402 | it('E5: an owner cannot give a member external beside another role', async () => { |
366 | 403 | const [row] = await findRows(ql, 'sys_member', { user_id: uid.member, organization_id: orgId }, 1); |
367 | 404 | const res = await stack.apiAs(token.owner, 'POST', '/auth/organization/update-member-role', { |
|
0 commit comments