feat: add JavaScript Socialhose API SDK
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user