import { describe, it, expect, vi, beforeEach } from 'vitest' vi.mock('server-only', () => ({})) const { mockSendNotification } = vi.hoisted(() => ({ mockSendNotification: vi.fn(), })) vi.mock('web-push', () => ({ default: { setVapidDetails: vi.fn(), sendNotification: mockSendNotification, }, })) vi.mock('@/lib/env', () => ({ env: { NEXT_PUBLIC_VAPID_PUBLIC_KEY: 'pk', VAPID_PRIVATE_KEY: 'sk', VAPID_SUBJECT: 'mailto:test@example.com', }, })) const { mockPushSubscription } = vi.hoisted(() => ({ mockPushSubscription: { findMany: vi.fn(), update: vi.fn(), delete: vi.fn(), }, })) vi.mock('@/lib/prisma', () => ({ prisma: { pushSubscription: mockPushSubscription }, })) import { sendPushToUser } from '@/lib/push-server' const SUB = { id: 'sub-1', endpoint: 'https://push.example.com/1', p256dh: 'p256dh', auth: 'auth' } const PAYLOAD = { title: 'Test', body: 'Body', url: '/test' } beforeEach(() => { vi.clearAllMocks() mockPushSubscription.findMany.mockResolvedValue([SUB]) mockPushSubscription.update.mockResolvedValue(SUB) mockPushSubscription.delete.mockResolvedValue(SUB) }) describe('sendPushToUser', () => { it('sends notification and updates last_used_at on success', async () => { mockSendNotification.mockResolvedValue({ statusCode: 201 }) await sendPushToUser('user-1', PAYLOAD) expect(mockSendNotification).toHaveBeenCalledOnce() expect(mockPushSubscription.update).toHaveBeenCalledWith({ where: { id: SUB.id }, data: { last_used_at: expect.any(Date) }, }) }) it('deletes subscription on 410 (expired)', async () => { mockSendNotification.mockRejectedValue({ statusCode: 410 }) await sendPushToUser('user-1', PAYLOAD) expect(mockPushSubscription.delete).toHaveBeenCalledWith({ where: { id: SUB.id } }) expect(mockPushSubscription.update).not.toHaveBeenCalled() }) it('deletes subscription on 404 (not found)', async () => { mockSendNotification.mockRejectedValue({ statusCode: 404 }) await sendPushToUser('user-1', PAYLOAD) expect(mockPushSubscription.delete).toHaveBeenCalledWith({ where: { id: SUB.id } }) }) it('logs error but does not delete on other error status', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) mockSendNotification.mockRejectedValue({ statusCode: 500 }) await sendPushToUser('user-1', PAYLOAD) expect(mockPushSubscription.delete).not.toHaveBeenCalled() expect(consoleSpy).toHaveBeenCalled() consoleSpy.mockRestore() }) })