From 8c19e499e93f119b28b8b293129affce886ea9dc Mon Sep 17 00:00:00 2001 From: lucor Date: Thu, 30 Apr 2026 20:10:11 +0200 Subject: [PATCH] fix(hive): type service worker runtime deps and add E2E envelope test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This gives the service worker's runtime deps an explicit type so the compiler validates the handler wiring instead of trusting an untyped object. The fetch dependency is now also explicit: it delegates to self.fetch rather than falling through to an ambient global. It also adds a test for the E2E envelope path — where a push payload contains a beebuzz envelope with an attachment token, the worker fetches the ciphertext, decrypts it, and shows the notification. This documents the flow and protects it from regressions. --- web/apps/hive/src/sw-runtime.test.ts | 59 ++++++++++++++++++++++++++++ web/apps/hive/src/sw.ts | 7 ++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/web/apps/hive/src/sw-runtime.test.ts b/web/apps/hive/src/sw-runtime.test.ts index aa1d7bd..03db904 100644 --- a/web/apps/hive/src/sw-runtime.test.ts +++ b/web/apps/hive/src/sw-runtime.test.ts @@ -313,6 +313,65 @@ describe('service worker runtime', () => { expect(deps.saveNotification).not.toHaveBeenCalled(); }); + it('resolves an E2E envelope by fetching and decrypting the stored payload', async () => { + const envelopeBytes = new TextEncoder().encode( + JSON.stringify({ + beebuzz: { + id: 'n-e2e-1', + token: 'attachment-token', + sent_at: '2026-04-20T13:30:00.000Z' + } + }) + ); + const buffer = envelopeBytes.buffer.slice( + envelopeBytes.byteOffset, + envelopeBytes.byteOffset + envelopeBytes.byteLength + ); + const fetchSpy = vi.fn(() => + Promise.resolve( + new Response('ciphertext', { + status: 200 + }) + ) + ); + const deps = createDeps({ + fetch: fetchSpy, + decryptPayload: vi.fn(() => + Promise.resolve( + JSON.stringify({ + title: 'Encrypted', + body: 'Secret message', + topic: 'alerts' + }) + ) + ) + }); + const event: PushEventLike = { + data: { arrayBuffer: () => buffer }, + waitUntil: () => {} + }; + + await handlePushEvent(deps, event); + + expect(fetchSpy).toHaveBeenCalledWith( + 'https://api.beebuzz.test/v1/attachments/attachment-token' + ); + expect(deps.saveNotification).toHaveBeenCalledWith({ + id: 'n-e2e-1', + title: 'Encrypted', + body: 'Secret message', + topic: 'alerts', + sentAt: '2026-04-20T13:30:00.000Z', + topicId: undefined, + attachment: undefined, + priority: undefined + }); + expect(deps.showNotification).toHaveBeenCalledWith( + 'Encrypted', + expect.objectContaining({ body: 'Secret message' }) + ); + }); + it('tells the user to re-pair when the device key is missing', async () => { const ageHeader = new TextEncoder().encode('age-encryption.org/v1\ncorrupted-data'); const buffer = ageHeader.buffer.slice( diff --git a/web/apps/hive/src/sw.ts b/web/apps/hive/src/sw.ts index fbb0b89..0333aee 100644 --- a/web/apps/hive/src/sw.ts +++ b/web/apps/hive/src/sw.ts @@ -7,7 +7,8 @@ import { handleNotificationClickEvent, handlePushEvent, handlePushSubscriptionChangeEvent, - type NotificationAttachmentEnvelope + type NotificationAttachmentEnvelope, + type ServiceWorkerRuntimeDeps } from './sw-runtime'; declare const self: ServiceWorkerGlobalScope; @@ -69,7 +70,7 @@ function saveNotificationToStorage(input: { ); } -const runtimeDeps = { +const runtimeDeps: ServiceWorkerRuntimeDeps = { debug: DEBUG, locationOrigin: self.location.origin, beebuzzDomain: BEEBUZZ_DOMAIN, @@ -87,7 +88,7 @@ const runtimeDeps = { skipWaiting: () => self.skipWaiting(), getPushSubscription: () => self.registration.pushManager.getSubscription(), decryptPayload, - fetch + fetch: (input, init) => self.fetch(input, init) }; self.addEventListener('push', (event: PushEvent) => {