import { afterEach, describe, expect, it, vi } from 'vitest'; import { ApiClientError, deleteNotification, deleteNotifications, getUnreadNotificationCount, listNotifications, } from '../src/lib/api'; const originalFetch = globalThis.fetch; function mockFetch(responder: (input: RequestInfo | URL, init?: RequestInit) => { status?: number; body?: unknown; statusText?: string; }) { const calls: { path: string; init?: RequestInit }[] = []; const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const path = typeof input === 'string' ? input : new URL(input.toString()).pathname; calls.push({ path, init }); const { status = 200, body = {}, statusText = 'OK' } = responder(input, init); return new Response(JSON.stringify(body), { status, statusText, headers: { 'content-type': 'application/json' }, }); }); globalThis.fetch = fetchMock as unknown as typeof fetch; return { calls, fetchMock }; } afterEach(() => { globalThis.fetch = originalFetch; }); describe('notifications api client', () => { it('listNotifications GETs /api/notifications with credentials', async () => { const { calls } = mockFetch(() => ({ body: { notifications: [{ id: 'n1', title: 'A', kind: 'promotion_approved', status: 'viewed' }] }, })); const result = await listNotifications(); expect(calls[0].path).toBe('/api/notifications'); expect(calls[0].init?.method).toBeUndefined(); expect(calls[0].init?.credentials).toBe('include'); expect(result.notifications).toHaveLength(1); }); it('getUnreadNotificationCount GETs the unread-count endpoint', async () => { const { calls } = mockFetch(() => ({ body: { count: 3 } })); const result = await getUnreadNotificationCount(); expect(calls[0].path).toBe('/api/notifications/unread-count'); expect(result.count).toBe(3); }); it('deleteNotification issues DELETE to /api/notifications/:id', async () => { const { calls } = mockFetch(() => ({ body: { ok: true } })); const result = await deleteNotification('n1'); expect(calls[0].path).toBe('/api/notifications/n1'); expect(calls[0].init?.method).toBe('DELETE'); expect(result.ok).toBe(true); }); it('deleteNotifications POSTs the ids array to the batch endpoint', async () => { const { calls } = mockFetch(() => ({ body: { ok: true, deleted: 2 } })); const result = await deleteNotifications(['n1', 'n2']); expect(calls[0].path).toBe('/api/notifications/delete-batch'); expect(calls[0].init?.method).toBe('POST'); const body = JSON.parse(calls[0].init?.body as string); expect(body).toEqual({ ids: ['n1', 'n2'] }); expect(result.deleted).toBe(2); }); it('encodes notification ids with special characters', async () => { const { calls } = mockFetch(() => ({ body: { ok: true } })); await deleteNotification('a/b'); expect(calls[0].path).toBe('/api/notifications/a%2Fb'); }); it('surfaces server errors as ApiClientError', async () => { mockFetch(() => ({ status: 401, body: { error: 'Authentication required' } })); await expect(listNotifications()).rejects.toMatchObject({ name: 'ApiClientError', status: 401, message: 'Authentication required', }); }); });