362 lines
10 KiB
TypeScript
362 lines
10 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||
|
|
import { nextTick } from 'vue';
|
||
|
|
import type { FetchError } from 'ofetch';
|
||
|
|
import type { ApiError } from '~/types/api/error';
|
||
|
|
import { useApi } from './useApi';
|
||
|
|
|
||
|
|
// Mock dependencies
|
||
|
|
vi.mock('#app', () => ({
|
||
|
|
useRuntimeConfig: vi.fn(() => ({
|
||
|
|
public: {
|
||
|
|
apiBase: 'http://localhost:3100/api',
|
||
|
|
},
|
||
|
|
})),
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Mock $fetch globally
|
||
|
|
global.$fetch = vi.fn();
|
||
|
|
|
||
|
|
describe('useApi', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
vi.restoreAllMocks();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('GET requests', () => {
|
||
|
|
it('should make a GET request and populate data on success', async () => {
|
||
|
|
const mockData = { id: 1, name: 'Test' };
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get<typeof mockData>('/test');
|
||
|
|
|
||
|
|
// Should start loading
|
||
|
|
await nextTick();
|
||
|
|
expect(result.loading.value).toBe(false); // Immediate execution completes quickly
|
||
|
|
|
||
|
|
// Wait for the async operation
|
||
|
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
|
||
|
|
|
||
|
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||
|
|
baseURL: 'http://localhost:3100/api',
|
||
|
|
method: 'GET',
|
||
|
|
body: undefined,
|
||
|
|
});
|
||
|
|
expect(result.data.value).toEqual(mockData);
|
||
|
|
expect(result.error.value).toBeNull();
|
||
|
|
expect(result.loading.value).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle GET request with custom options', async () => {
|
||
|
|
const mockData = { result: 'success' };
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test', { headers: { 'X-Custom': 'header' } });
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
|
||
|
|
|
||
|
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||
|
|
baseURL: 'http://localhost:3100/api',
|
||
|
|
method: 'GET',
|
||
|
|
headers: { 'X-Custom': 'header' },
|
||
|
|
body: undefined,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should not execute immediately when immediate is false', async () => {
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test', {}, false);
|
||
|
|
|
||
|
|
expect($fetch).not.toHaveBeenCalled();
|
||
|
|
expect(result.data.value).toBeNull();
|
||
|
|
expect(result.loading.value).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should execute when run() is called manually', async () => {
|
||
|
|
const mockData = { manual: true };
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test', {}, false);
|
||
|
|
|
||
|
|
expect($fetch).not.toHaveBeenCalled();
|
||
|
|
|
||
|
|
await result.run();
|
||
|
|
|
||
|
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||
|
|
baseURL: 'http://localhost:3100/api',
|
||
|
|
method: 'GET',
|
||
|
|
body: undefined,
|
||
|
|
});
|
||
|
|
expect(result.data.value).toEqual(mockData);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('DELETE requests', () => {
|
||
|
|
it('should make a DELETE request', async () => {
|
||
|
|
const mockData = { deleted: true };
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.del<typeof mockData>('/test/1');
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
|
||
|
|
|
||
|
|
expect($fetch).toHaveBeenCalledWith('/test/1', {
|
||
|
|
baseURL: 'http://localhost:3100/api',
|
||
|
|
method: 'DELETE',
|
||
|
|
body: undefined,
|
||
|
|
});
|
||
|
|
expect(result.data.value).toEqual(mockData);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('POST requests', () => {
|
||
|
|
it('should make a POST request with body', async () => {
|
||
|
|
const mockResponse = { id: 1, created: true };
|
||
|
|
const requestBody = { name: 'New Item' };
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockResponse);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.post<typeof mockResponse, typeof requestBody>(
|
||
|
|
'/test',
|
||
|
|
{},
|
||
|
|
true,
|
||
|
|
requestBody,
|
||
|
|
);
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));
|
||
|
|
|
||
|
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||
|
|
baseURL: 'http://localhost:3100/api',
|
||
|
|
method: 'POST',
|
||
|
|
body: requestBody,
|
||
|
|
});
|
||
|
|
expect(result.data.value).toEqual(mockResponse);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should allow run() to be called with a different body', async () => {
|
||
|
|
const mockResponse = { success: true };
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockResponse);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.post<typeof mockResponse, { data: string }>('/test', {}, false);
|
||
|
|
|
||
|
|
const body = { data: 'runtime-data' };
|
||
|
|
await result.run(body);
|
||
|
|
|
||
|
|
expect($fetch).toHaveBeenCalledWith('/test', {
|
||
|
|
baseURL: 'http://localhost:3100/api',
|
||
|
|
method: 'POST',
|
||
|
|
body,
|
||
|
|
});
|
||
|
|
expect(result.data.value).toEqual(mockResponse);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('PUT requests', () => {
|
||
|
|
it('should make a PUT request with body', async () => {
|
||
|
|
const mockResponse = { updated: true };
|
||
|
|
const requestBody = { name: 'Updated Item' };
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockResponse);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.put<typeof mockResponse, typeof requestBody>(
|
||
|
|
'/test/1',
|
||
|
|
{},
|
||
|
|
true,
|
||
|
|
requestBody,
|
||
|
|
);
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));
|
||
|
|
|
||
|
|
expect($fetch).toHaveBeenCalledWith('/test/1', {
|
||
|
|
baseURL: 'http://localhost:3100/api',
|
||
|
|
method: 'PUT',
|
||
|
|
body: requestBody,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('PATCH requests', () => {
|
||
|
|
it('should make a PATCH request with body', async () => {
|
||
|
|
const mockResponse = { patched: true };
|
||
|
|
const requestBody = { field: 'value' };
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockResponse);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.patch<typeof mockResponse, typeof requestBody>(
|
||
|
|
'/test/1',
|
||
|
|
{},
|
||
|
|
true,
|
||
|
|
requestBody,
|
||
|
|
);
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));
|
||
|
|
|
||
|
|
expect($fetch).toHaveBeenCalledWith('/test/1', {
|
||
|
|
baseURL: 'http://localhost:3100/api',
|
||
|
|
method: 'PATCH',
|
||
|
|
body: requestBody,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Error handling', () => {
|
||
|
|
it('should handle fetch errors with ApiError response', async () => {
|
||
|
|
const apiError: ApiError = {
|
||
|
|
message: 'backend.errors.not_found',
|
||
|
|
success: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
const fetchError: Partial<FetchError> = {
|
||
|
|
message: 'Fetch Error',
|
||
|
|
response: {
|
||
|
|
_data: apiError,
|
||
|
|
} as any,
|
||
|
|
};
|
||
|
|
|
||
|
|
vi.mocked($fetch).mockRejectedValueOnce(fetchError);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test');
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.error.value).not.toBeNull());
|
||
|
|
|
||
|
|
expect(result.data.value).toBeNull();
|
||
|
|
expect(result.error.value).toEqual(apiError);
|
||
|
|
expect(result.loading.value).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle fetch errors without ApiError response', async () => {
|
||
|
|
const fetchError: Partial<FetchError> = {
|
||
|
|
message: 'Network Error',
|
||
|
|
response: undefined,
|
||
|
|
};
|
||
|
|
|
||
|
|
vi.mocked($fetch).mockRejectedValueOnce(fetchError);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test');
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.error.value).not.toBeNull());
|
||
|
|
|
||
|
|
expect(result.error.value).toEqual({
|
||
|
|
message: 'Network Error',
|
||
|
|
success: false,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should use default error message when fetch error has no message', async () => {
|
||
|
|
const fetchError: Partial<FetchError> = {
|
||
|
|
message: '',
|
||
|
|
};
|
||
|
|
|
||
|
|
vi.mocked($fetch).mockRejectedValueOnce(fetchError);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test');
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.error.value).not.toBeNull());
|
||
|
|
|
||
|
|
expect(result.error.value).toEqual({
|
||
|
|
message: 'backend.errors.unknown',
|
||
|
|
success: false,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should clear previous errors on new request', async () => {
|
||
|
|
const fetchError: Partial<FetchError> = {
|
||
|
|
message: 'First Error',
|
||
|
|
};
|
||
|
|
const mockData = { success: true };
|
||
|
|
|
||
|
|
// First request fails
|
||
|
|
vi.mocked($fetch).mockRejectedValueOnce(fetchError);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test', {}, false);
|
||
|
|
|
||
|
|
await result.run();
|
||
|
|
await vi.waitFor(() => expect(result.error.value).not.toBeNull());
|
||
|
|
expect(result.error.value?.message).toBe('First Error');
|
||
|
|
|
||
|
|
// Second request succeeds
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce(mockData);
|
||
|
|
await result.run();
|
||
|
|
|
||
|
|
await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
|
||
|
|
expect(result.error.value).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Loading state', () => {
|
||
|
|
it('should set loading to true during request', async () => {
|
||
|
|
let resolvePromise: (value: any) => void;
|
||
|
|
const promise = new Promise((resolve) => {
|
||
|
|
resolvePromise = resolve;
|
||
|
|
});
|
||
|
|
|
||
|
|
vi.mocked($fetch).mockReturnValueOnce(promise as any);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test', {}, false);
|
||
|
|
|
||
|
|
expect(result.loading.value).toBe(false);
|
||
|
|
|
||
|
|
const runPromise = result.run();
|
||
|
|
|
||
|
|
// Should be loading
|
||
|
|
await nextTick();
|
||
|
|
expect(result.loading.value).toBe(true);
|
||
|
|
|
||
|
|
// Resolve the request
|
||
|
|
resolvePromise!({ done: true });
|
||
|
|
await runPromise;
|
||
|
|
|
||
|
|
expect(result.loading.value).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should set loading to false after error', async () => {
|
||
|
|
let rejectPromise: (error: any) => void;
|
||
|
|
const promise = new Promise((_, reject) => {
|
||
|
|
rejectPromise = reject;
|
||
|
|
});
|
||
|
|
|
||
|
|
vi.mocked($fetch).mockReturnValueOnce(promise as any);
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test', {}, false);
|
||
|
|
|
||
|
|
const runPromise = result.run();
|
||
|
|
|
||
|
|
await nextTick();
|
||
|
|
expect(result.loading.value).toBe(true);
|
||
|
|
|
||
|
|
rejectPromise!({ message: 'Error' });
|
||
|
|
await runPromise;
|
||
|
|
|
||
|
|
expect(result.loading.value).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('Return type structure', () => {
|
||
|
|
it('should return QueryResult with correct structure', async () => {
|
||
|
|
vi.mocked($fetch).mockResolvedValueOnce({ test: 'data' });
|
||
|
|
|
||
|
|
const api = useApi();
|
||
|
|
const result = api.get('/test', {}, false);
|
||
|
|
|
||
|
|
expect(result).toHaveProperty('data');
|
||
|
|
expect(result).toHaveProperty('error');
|
||
|
|
expect(result).toHaveProperty('loading');
|
||
|
|
expect(result).toHaveProperty('run');
|
||
|
|
expect(typeof result.run).toBe('function');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|