All checks were successful
continuous-integration/drone/push Build is passing
155 lines
5.3 KiB
TypeScript
155 lines
5.3 KiB
TypeScript
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.',
|
|
});
|
|
});
|
|
});
|