Files
heygo/tests/api.notifications-ui.test.ts
2026-06-20 21:07:56 +10:00

99 lines
3.2 KiB
TypeScript

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',
});
});
});