import { describe, it, expect, vi, beforeEach } from 'vitest' const { mockGetSession } = vi.hoisted(() => ({ mockGetSession: vi.fn(), })) vi.mock('@/lib/auth', () => ({ getSession: mockGetSession, })) const { mockUpsert, mockDeleteMany } = vi.hoisted(() => ({ mockUpsert: vi.fn(), mockDeleteMany: vi.fn(), })) vi.mock('@/lib/prisma', () => ({ prisma: { pushSubscription: { upsert: mockUpsert, deleteMany: mockDeleteMany, }, }, })) import { subscribeToPushAction, unsubscribeFromPushAction } from '@/actions/push' const VALID_INPUT = { endpoint: 'https://push.example.com/subscription/abc123', keys: { p256dh: 'aBcDeFgH', auth: 'xYzAbC' }, } const SESSION_USER = { userId: 'user-1', isDemo: false } const SESSION_DEMO = { userId: 'demo-1', isDemo: true } beforeEach(() => { vi.clearAllMocks() mockUpsert.mockResolvedValue({}) mockDeleteMany.mockResolvedValue({ count: 1 }) }) describe('subscribeToPushAction', () => { it('upserts subscription for authenticated user', async () => { mockGetSession.mockResolvedValue(SESSION_USER) await subscribeToPushAction(VALID_INPUT) expect(mockUpsert).toHaveBeenCalledWith( expect.objectContaining({ where: { endpoint: VALID_INPUT.endpoint }, create: expect.objectContaining({ user_id: 'user-1', endpoint: VALID_INPUT.endpoint }), }) ) }) it('is idempotent — calling twice upserts twice without error', async () => { mockGetSession.mockResolvedValue(SESSION_USER) await subscribeToPushAction(VALID_INPUT) await subscribeToPushAction(VALID_INPUT) expect(mockUpsert).toHaveBeenCalledTimes(2) }) it('returns without writing for demo user', async () => { mockGetSession.mockResolvedValue(SESSION_DEMO) await subscribeToPushAction(VALID_INPUT) expect(mockUpsert).not.toHaveBeenCalled() }) it('returns without writing when not authenticated', async () => { mockGetSession.mockResolvedValue({}) await subscribeToPushAction(VALID_INPUT) expect(mockUpsert).not.toHaveBeenCalled() }) it('returns without writing for invalid input', async () => { mockGetSession.mockResolvedValue(SESSION_USER) // @ts-expect-error intentionally invalid await subscribeToPushAction({ endpoint: 'not-a-url', keys: {} }) expect(mockUpsert).not.toHaveBeenCalled() }) }) describe('unsubscribeFromPushAction', () => { it('deletes subscription scoped to user_id', async () => { mockGetSession.mockResolvedValue(SESSION_USER) await unsubscribeFromPushAction({ endpoint: VALID_INPUT.endpoint }) expect(mockDeleteMany).toHaveBeenCalledWith({ where: { endpoint: VALID_INPUT.endpoint, user_id: 'user-1' }, }) }) it('does not touch subscriptions of other users', async () => { mockGetSession.mockResolvedValue({ userId: 'other-user', isDemo: false }) await unsubscribeFromPushAction({ endpoint: VALID_INPUT.endpoint }) expect(mockDeleteMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ user_id: 'other-user' }) }) ) }) it('returns without writing when not authenticated', async () => { mockGetSession.mockResolvedValue({}) await unsubscribeFromPushAction({ endpoint: VALID_INPUT.endpoint }) expect(mockDeleteMany).not.toHaveBeenCalled() }) })