83 lines
3.2 KiB
TypeScript
83 lines
3.2 KiB
TypeScript
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<string, string>)['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<SocialhoseError>);
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
|