process.env.VAPID_PUBLIC_KEY = 'BTestPublicKey_______________________________________________________________'; process.env.VAPID_PRIVATE_KEY = 'TestPrivateKey_______________________________'; process.env.VAPID_SUBJECT = 'mailto:test@vyndr.app'; const mockSendNotification = jest.fn(); const mockSetVapidDetails = jest.fn(); jest.mock('web-push', () => ({ setVapidDetails: (...args) => mockSetVapidDetails(...args), sendNotification: (...args) => mockSendNotification(...args), })); // Builds a fluent supabase mock where each chained call records intent and // the terminal promise yields {data, error}. Good enough for what webPush // actually does (.from().select().eq() / .delete().eq()). function makeSupabase({ rows = [], error = null } = {}) { const deleted = []; const builder = (table) => { const ctx = { table, filters: [] }; const proxy = { _ctx: ctx, select: () => proxy, eq: (col, val) => { ctx.filters.push([col, val]); return ctx.action === 'delete' ? Promise.resolve({ error: null }) : proxy; }, contains: () => proxy, delete: () => { ctx.action = 'delete'; deleted.push(ctx); return proxy; }, then: (resolve) => resolve({ data: rows, error }), }; return proxy; }; return { from: jest.fn().mockImplementation(builder), _deleted: deleted, }; } const mockSupabase = { current: makeSupabase() }; jest.mock('../../src/utils/supabase', () => ({ getSupabaseServiceClient: () => mockSupabase.current, })); const webPush = require('../../src/services/distribution/webPush'); beforeEach(() => { mockSendNotification.mockReset(); mockSetVapidDetails.mockReset(); mockSupabase.current = makeSupabase(); }); describe('webPush.configured', () => { test('returns true when both VAPID keys present', () => { expect(webPush.configured()).toBe(true); }); }); describe('webPush.sendPushToUser', () => { test('returns sent=0 when user has no subscriptions', async () => { mockSupabase.current = makeSupabase({ rows: [] }); const result = await webPush.sendPushToUser('user-1', { title: 'hi' }); expect(result).toMatchObject({ ok: true, sent: 0 }); expect(mockSendNotification).not.toHaveBeenCalled(); }); test('sends to every subscription and counts successes', async () => { mockSupabase.current = makeSupabase({ rows: [ { id: 'a', endpoint: 'https://a', keys_p256dh: 'p1', keys_auth: 'k1' }, { id: 'b', endpoint: 'https://b', keys_p256dh: 'p2', keys_auth: 'k2' }, ], }); mockSendNotification.mockResolvedValue({ statusCode: 201 }); const result = await webPush.sendPushToUser('user-1', { title: 'hi' }); expect(result.ok).toBe(true); expect(result.sent).toBe(2); expect(mockSendNotification).toHaveBeenCalledTimes(2); }); test('prunes subscription that returns 410 Gone', async () => { mockSupabase.current = makeSupabase({ rows: [{ id: 'dead', endpoint: 'https://dead', keys_p256dh: 'p', keys_auth: 'k' }], }); const err = Object.assign(new Error('Gone'), { statusCode: 410 }); mockSendNotification.mockRejectedValue(err); const result = await webPush.sendPushToUser('user-1', { title: 'hi' }); expect(result.pruned).toBe(1); expect(result.sent).toBe(0); expect(mockSupabase.current._deleted.length).toBeGreaterThan(0); }); test('non-410 failure does not prune, counts as failed', async () => { mockSupabase.current = makeSupabase({ rows: [{ id: 'flaky', endpoint: 'https://flaky', keys_p256dh: 'p', keys_auth: 'k' }], }); const err = Object.assign(new Error('boom'), { statusCode: 500 }); mockSendNotification.mockRejectedValue(err); const result = await webPush.sendPushToUser('user-1', { title: 'hi' }); expect(result.failed).toBe(1); expect(result.sent).toBe(0); expect(mockSupabase.current._deleted.length).toBe(0); }); });