Server-only push-lib met VAPID feature-gate, send naar alle subscriptions van een user, en automatische cleanup bij 404/410. Unit tests: success-pad, 410 verwijdert sub, 404 verwijdert sub, andere errors loggen zonder delete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
79 lines
2.5 KiB
TypeScript
79 lines
2.5 KiB
TypeScript
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()
|
|
})
|
|
})
|