import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { apiClient } from './api' import type { Task, CreateTaskRequest, UpdateTaskRequest } from '~/types/task' import { TaskStatus } from '~/types/task' // Mock fetch globally const mockFetch = vi.fn() global.fetch = mockFetch // Sample task data for testing const mockTask: Task = { id: '550e8400-e29b-41d4-a716-446655440000', title: 'Test Task', description: 'Test Description', status: TaskStatus.Todo, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', completed_at: null, } const mockTasks: Task[] = [ mockTask, { ...mockTask, id: '550e8400-e29b-41d4-a716-446655440001', title: 'Another Task', status: TaskStatus.Done, completed_at: '2023-01-01T01:00:00Z', }, ] describe('API Client', () => { beforeEach(() => { mockFetch.mockClear() // Silence console.log during tests vi.spyOn(console, 'log').mockImplementation(() => {}) vi.spyOn(console, 'error').mockImplementation(() => {}) }) afterEach(() => { vi.restoreAllMocks() }) describe('listTasks', () => { it('should fetch all tasks successfully', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: () => Promise.resolve(mockTasks), }) const result = await apiClient.listTasks() expect(mockFetch).toHaveBeenCalledWith('/api/tasks', { headers: { 'Content-Type': 'application/json' }, }) expect(result).toEqual(mockTasks) }) it('should handle empty task list', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: () => Promise.resolve([]), }) const result = await apiClient.listTasks() expect(result).toEqual([]) }) it('should throw error on failed request', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Internal Server Error', json: () => Promise.resolve({ message: 'Server error' }), }) await expect(apiClient.listTasks()).rejects.toThrow('Server error') }) }) describe('getTask', () => { it('should fetch single task successfully', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: () => Promise.resolve(mockTask), }) const result = await apiClient.getTask(mockTask.id) expect(mockFetch).toHaveBeenCalledWith(`/api/tasks/${mockTask.id}`, { headers: { 'Content-Type': 'application/json' }, }) expect(result).toEqual(mockTask) }) it('should handle task not found', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found', json: () => Promise.resolve({ message: 'Task not found' }), }) await expect(apiClient.getTask('nonexistent-id')).rejects.toThrow( 'Task not found' ) }) }) describe('createTask', () => { it('should create task successfully', async () => { const newTaskData: CreateTaskRequest = { title: 'New Task', description: 'New Description', } mockFetch.mockResolvedValueOnce({ ok: true, status: 201, statusText: 'Created', json: () => Promise.resolve(mockTask), }) const result = await apiClient.createTask(newTaskData) expect(mockFetch).toHaveBeenCalledWith('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newTaskData), }) expect(result).toEqual(mockTask) }) it('should handle validation errors', async () => { const invalidData: CreateTaskRequest = { title: '', } mockFetch.mockResolvedValueOnce({ ok: false, status: 422, statusText: 'Unprocessable Entity', json: () => Promise.resolve({ message: 'Title must not be empty' }), }) await expect(apiClient.createTask(invalidData)).rejects.toThrow( 'Title must not be empty' ) }) }) describe('updateTask', () => { it('should update task successfully', async () => { const updateData: UpdateTaskRequest = { title: 'Updated Task', status: TaskStatus.Done, } const updatedTask = { ...mockTask, ...updateData } mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: () => Promise.resolve(updatedTask), }) const result = await apiClient.updateTask(mockTask.id, updateData) expect(mockFetch).toHaveBeenCalledWith(`/api/tasks/${mockTask.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData), }) expect(result).toEqual(updatedTask) }) it('should handle partial updates', async () => { const updateData: UpdateTaskRequest = { status: TaskStatus.Done, } mockFetch.mockResolvedValueOnce({ ok: true, status: 200, statusText: 'OK', json: () => Promise.resolve({ ...mockTask, status: TaskStatus.Done }), }) const result = await apiClient.updateTask(mockTask.id, updateData) expect(result.status).toBe(TaskStatus.Done) }) }) describe('deleteTask', () => { it('should delete task successfully', async () => { mockFetch.mockResolvedValueOnce({ ok: true, status: 204, statusText: 'No Content', }) const result = await apiClient.deleteTask(mockTask.id) expect(result).toBeNull() expect(mockFetch).toHaveBeenCalledWith(`/api/tasks/${mockTask.id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }) }) it('should handle delete errors', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 404, statusText: 'Not Found', json: () => Promise.resolve({ message: 'Task not found' }), }) await expect(apiClient.deleteTask('nonexistent-id')).rejects.toThrow( 'Task not found' ) }) }) describe('Error Handling', () => { it('should handle network errors', async () => { mockFetch.mockRejectedValueOnce(new Error('Network error')) await expect(apiClient.listTasks()).rejects.toThrow('Network error') }) it('should handle malformed JSON responses', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Internal Server Error', json: () => Promise.reject(new Error('Invalid JSON')), }) await expect(apiClient.listTasks()).rejects.toThrow( 'HTTP 500: Internal Server Error' ) }) it('should handle responses with no error message', async () => { mockFetch.mockResolvedValueOnce({ ok: false, status: 400, statusText: 'Bad Request', json: () => Promise.resolve({}), }) await expect(apiClient.listTasks()).rejects.toThrow( 'HTTP 400: Bad Request' ) }) }) })