import { describe, expect, it, vi } from 'vitest'; import { SocialhoseClient, SocialhoseError } from '../src/index'; const ok = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } }); describe('SocialhoseClient', () => { it('sends Api-Key auth, browser-like user-agent, and query params', async () => { const fetchMock = vi.fn(async () => ok({ count: 0, next: null, previous: null, results: [] })); const client = new SocialhoseClient({ apiKey: 'test-key', fetch: fetchMock, cacheTtlMs: 0 }); await client.getMentions({ page: 2, platforms: 'twitter', content_search: 'hospital' }); expect(fetchMock).toHaveBeenCalledTimes(1); const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; expect(url).toBe( 'https://socialhose.net/api/public/v1/mentions/?page=2&platforms=twitter&content_search=hospital', ); expect(init.headers).toMatchObject({ Authorization: 'Api-Key test-key', Accept: 'application/json', }); expect((init.headers as Record)['User-Agent']).toContain('Mozilla/5.0'); }); it('caches identical GET requests inside the configured TTL', async () => { const fetchMock = vi.fn(async () => ok({ count: 1, next: null, previous: null, results: [] })); const client = new SocialhoseClient({ apiKey: 'test-key', fetch: fetchMock, cacheTtlMs: 60_000 }); await client.getMentions({ page: 1 }); await client.getMentions({ page: 1 }); expect(fetchMock).toHaveBeenCalledTimes(1); }); it('retries rate limits and transient server failures', async () => { const fetchMock = vi .fn() .mockResolvedValueOnce(new Response('rate limited', { status: 429 })) .mockResolvedValueOnce(new Response('bad gateway', { status: 502 })) .mockResolvedValueOnce(ok({ results: [] })); const client = new SocialhoseClient({ apiKey: 'test-key', fetch: fetchMock, cacheTtlMs: 0, retryDelayMs: () => 0, }); const campaigns = await client.getCampaigns(); expect(campaigns).toEqual([]); expect(fetchMock).toHaveBeenCalledTimes(3); }); it('throws a structured SocialhoseError for non-ok responses', async () => { const fetchMock = vi.fn(async () => new Response('{"detail":"forbidden"}', { status: 403 })); const client = new SocialhoseClient({ apiKey: 'test-key', fetch: fetchMock, cacheTtlMs: 0 }); await expect(client.getCampaign('abc')).rejects.toMatchObject({ name: 'SocialhoseError', status: 403, path: '/campaigns/abc/', } satisfies Partial); }); it('normalizes mailing-list invite outcomes', async () => { const fetchMock = vi.fn(async () => ok({ detail: 'already invited' }, 409)); const client = new SocialhoseClient({ apiKey: 'test-key', fetch: fetchMock }); await expect(client.inviteMailingListMember('list-1', { email: 'a@example.com' })).resolves.toEqual({ outcome: 'already', detail: 'already invited', }); }); it('defers missing apiKey errors until the first request', async () => { const client = new SocialhoseClient({ apiKey: '', fetch: vi.fn() }); await expect(client.getCampaigns()).rejects.toThrow('Socialhose apiKey is required'); }); });