diff --git a/plugin-sdk/test/sdk.test.ts b/plugin-sdk/test/sdk.test.ts index 15b4d2832..2799c4e7b 100644 --- a/plugin-sdk/test/sdk.test.ts +++ b/plugin-sdk/test/sdk.test.ts @@ -288,6 +288,38 @@ describe('createMockHost', () => { await expect(ungranted.ctx.journal.createJourney({ title: 'x' })).rejects.toThrow(/PERMISSION_DENIED/); }); + it('journal.addEntryPhoto refuses in the mock what the host refuses in production', async () => { + const png = Buffer.from('89504e470d0a1a0a', 'hex').toString('base64'); + const host = createMockHost({ + grants: ['db:write:journal'], + actingUserId: 42, + journals: [{ id: 1, title: 'Japan' }], + journalEntries: [{ id: 5, journey_id: 1, entry_date: '2027-04-01' }], + }); + + const photo = await host.ctx.journal.addEntryPhoto(5, { name: 'tokyo.jpg', content_base64: png, caption: 'Arrival' }); + expect(photo).toMatchObject({ entry_id: 5, caption: 'Arrival' }); + + // An importer that hits one of these in the mock would hit it in production too, + // which is the whole point of the double refusing the same things. + await expect(host.ctx.journal.addEntryPhoto(999, { name: 'a.jpg', content_base64: png })) + .rejects.toThrow(/RESOURCE_FORBIDDEN/); + await expect(host.ctx.journal.addEntryPhoto(5, { name: 'map.svg', content_base64: png })) + .rejects.toThrow(/not an allowed image type/); + await expect(host.ctx.journal.addEntryPhoto(5, { name: 'noext', content_base64: png })) + .rejects.toThrow(/not an allowed image type/); + await expect(host.ctx.journal.addEntryPhoto(5, { name: '', content_base64: png })) + .rejects.toThrow(/name is required/); + await expect(host.ctx.journal.addEntryPhoto(5, { name: 'a.jpg', content_base64: '' })) + .rejects.toThrow(/content_base64 is required/); + await expect(host.ctx.journal.addEntryPhoto(5, { name: 'a.jpg', content_base64: 'A'.repeat(15 * 1024 * 1024) })) + .rejects.toThrow(/10MB plugin upload cap/); + + const ungranted = createMockHost({ grants: [], actingUserId: 42, journalEntries: [{ id: 5, journey_id: 1 }] }); + await expect(ungranted.ctx.journal.addEntryPhoto(5, { name: 'a.jpg', content_base64: png })) + .rejects.toThrow(/PERMISSION_DENIED/); + }); + it('creates a trip for the acting user and serves rates + collab reads against the grants', async () => { const { ctx } = createMockHost({ grants: ['db:create:trips', 'rates:read', 'db:read:collab'], diff --git a/server/tests/unit/plugins/plugin-sdk.test.ts b/server/tests/unit/plugins/plugin-sdk.test.ts index 687aca973..2c0316035 100644 --- a/server/tests/unit/plugins/plugin-sdk.test.ts +++ b/server/tests/unit/plugins/plugin-sdk.test.ts @@ -29,6 +29,9 @@ describe('createPluginContext', () => { await ctx.trips.getById(1); expect(rpc).toHaveBeenCalledWith('trips.getById', { tripId: 1, _inv: 'inv-1' }); + await ctx.journal.addEntryPhoto(5, { name: 'a.jpg', content_base64: 'eA==' }); + expect(rpc).toHaveBeenCalledWith('journal.addEntryPhoto', { entryId: 5, input: { name: 'a.jpg', content_base64: 'eA==' }, _inv: 'inv-1' }); + await ctx.ws.broadcastToTrip(1, 'ping', { a: 1 }); // carries _inv so the host can bind the acting user — without it the broadcast was refused expect(rpc).toHaveBeenCalledWith('ws.broadcastToTrip', { tripId: 1, event: 'ping', data: { a: 1 }, _inv: 'inv-1' });