From 6c7e257c17c9ec1191c93c5acfbbacf4ed7f8f15 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 28 Aug 2026 17:39:38 +0200 Subject: [PATCH] test(plugins): cover the mock double and the child proxy for addEntryPhoto The handler had eight cases, its two doubles had none, which Sonar counted as new code without coverage. The mock is the more useful of the two to pin anyway: an importer developing against it should hit the same refusals it will hit in production, so the SVG, the missing extension, the empty payload and the size cap are all asserted against the double rather than only against the host. --- plugin-sdk/test/sdk.test.ts | 32 ++++++++++++++++++++ server/tests/unit/plugins/plugin-sdk.test.ts | 3 ++ 2 files changed, 35 insertions(+) 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' });