This commit is contained in:
154
tests/api/createApiClient.test.ts
Normal file
154
tests/api/createApiClient.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ApiError, createApiClient } from '../../src/api/createApiClient';
|
||||
|
||||
describe('createApiClient', () => {
|
||||
it('sends json requests and returns parsed payload', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: vi.fn().mockResolvedValue({ ok: true }),
|
||||
} as unknown as Response);
|
||||
|
||||
const client = createApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
fetchImpl: fetchMock as typeof fetch,
|
||||
});
|
||||
|
||||
const result = await client.request<{ ok: boolean }>('/users', {
|
||||
method: 'POST',
|
||||
token: 'token-123',
|
||||
body: { hello: 'world' },
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/users', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer token-123',
|
||||
},
|
||||
body: JSON.stringify({ hello: 'world' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('maps api error payload through custom resolver', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
code: 'AUTH_UNAUTHORIZED',
|
||||
error: 'unauthorized',
|
||||
requestId: 'req-1',
|
||||
details: { reason: 'expired' },
|
||||
}),
|
||||
} as unknown as Response);
|
||||
const resolveError = vi.fn(() => 'Unauthorized access. Please sign in again.');
|
||||
|
||||
const client = createApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
fetchImpl: fetchMock as typeof fetch,
|
||||
resolveError,
|
||||
});
|
||||
|
||||
await expect(client.request('/users')).rejects.toMatchObject({
|
||||
name: 'ApiError',
|
||||
status: 401,
|
||||
code: 'AUTH_UNAUTHORIZED',
|
||||
requestId: 'req-1',
|
||||
details: { reason: 'expired' },
|
||||
rawMessage: 'unauthorized',
|
||||
message: 'Unauthorized access. Please sign in again.',
|
||||
});
|
||||
expect(resolveError).toHaveBeenCalledWith({
|
||||
code: 'AUTH_UNAUTHORIZED',
|
||||
status: 401,
|
||||
fallbackMessage: 'unauthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('infers an error code from status when payload has no code', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: vi.fn().mockResolvedValue({ error: 'missing resource' }),
|
||||
} as unknown as Response);
|
||||
const inferErrorCodeFromStatus = vi.fn((status?: number | null) =>
|
||||
status === 404 ? 'USER_NOT_FOUND' : undefined,
|
||||
);
|
||||
|
||||
const client = createApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
fetchImpl: fetchMock as typeof fetch,
|
||||
inferErrorCodeFromStatus,
|
||||
resolveError: ({ code }) => (code === 'USER_NOT_FOUND' ? 'User not found.' : 'Unknown'),
|
||||
});
|
||||
|
||||
await expect(client.request('/users/missing')).rejects.toMatchObject({
|
||||
code: 'USER_NOT_FOUND',
|
||||
message: 'User not found.',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to default messages when response is not valid json', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: vi.fn().mockRejectedValue(new Error('invalid json')),
|
||||
} as unknown as Response);
|
||||
|
||||
const client = createApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
fetchImpl: fetchMock as typeof fetch,
|
||||
});
|
||||
|
||||
await expect(client.request('/users')).rejects.toMatchObject({
|
||||
code: undefined,
|
||||
rawMessage: undefined,
|
||||
message: 'Request failed (500).',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses generic default message when status is unavailable', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: undefined,
|
||||
json: vi.fn().mockResolvedValue(null),
|
||||
} as unknown as Response);
|
||||
|
||||
const client = createApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
fetchImpl: fetchMock as typeof fetch,
|
||||
});
|
||||
|
||||
let thrown: unknown;
|
||||
try {
|
||||
await client.request('/users');
|
||||
} catch (err) {
|
||||
thrown = err;
|
||||
}
|
||||
|
||||
expect(thrown).toBeInstanceOf(ApiError);
|
||||
expect((thrown as ApiError).message).toBe('Request failed. Please try again.');
|
||||
});
|
||||
|
||||
it('uses raw fallback error text with the default resolver when present', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
error: 'Validation failed in backend.',
|
||||
}),
|
||||
} as unknown as Response);
|
||||
|
||||
const client = createApiClient({
|
||||
baseUrl: 'https://api.example.com',
|
||||
fetchImpl: fetchMock as typeof fetch,
|
||||
});
|
||||
|
||||
await expect(client.request('/users')).rejects.toMatchObject({
|
||||
message: 'Validation failed in backend.',
|
||||
rawMessage: 'Validation failed in backend.',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user