// src/utils/apiResponse.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Response } from 'express'; import { sendSuccess, sendNoContent, calculatePagination, sendPaginated, sendError, sendMessage, ErrorCode, } from './apiResponse'; // Create a mock Express response function createMockResponse(): Response { const res = { status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), send: vi.fn().mockReturnThis(), } as unknown as Response; return res; } describe('apiResponse utilities', () => { let mockRes: Response; beforeEach(() => { mockRes = createMockResponse(); }); describe('sendSuccess', () => { it('should send success response with data and default status 200', () => { const data = { id: 1, name: 'Test' }; sendSuccess(mockRes, data); expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data, }); }); it('should send success response with custom status code', () => { const data = { id: 1 }; sendSuccess(mockRes, data, 201); expect(mockRes.status).toHaveBeenCalledWith(201); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data, }); }); it('should include meta when provided', () => { const data = { id: 1 }; const meta = { requestId: 'req-123', timestamp: '2024-01-15T12:00:00Z' }; sendSuccess(mockRes, data, 200, meta); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data, meta, }); }); it('should handle null data', () => { sendSuccess(mockRes, null); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: null, }); }); it('should handle array data', () => { const data = [{ id: 1 }, { id: 2 }]; sendSuccess(mockRes, data); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data, }); }); it('should handle empty object data', () => { sendSuccess(mockRes, {}); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: {}, }); }); }); describe('sendNoContent', () => { it('should send 204 status with no body', () => { sendNoContent(mockRes); expect(mockRes.status).toHaveBeenCalledWith(204); expect(mockRes.send).toHaveBeenCalledWith(); }); }); describe('calculatePagination', () => { it('should calculate pagination for first page', () => { const result = calculatePagination({ page: 1, limit: 10, total: 100 }); expect(result).toEqual({ page: 1, limit: 10, total: 100, totalPages: 10, hasNextPage: true, hasPrevPage: false, }); }); it('should calculate pagination for middle page', () => { const result = calculatePagination({ page: 5, limit: 10, total: 100 }); expect(result).toEqual({ page: 5, limit: 10, total: 100, totalPages: 10, hasNextPage: true, hasPrevPage: true, }); }); it('should calculate pagination for last page', () => { const result = calculatePagination({ page: 10, limit: 10, total: 100 }); expect(result).toEqual({ page: 10, limit: 10, total: 100, totalPages: 10, hasNextPage: false, hasPrevPage: true, }); }); it('should handle single page result', () => { const result = calculatePagination({ page: 1, limit: 10, total: 5 }); expect(result).toEqual({ page: 1, limit: 10, total: 5, totalPages: 1, hasNextPage: false, hasPrevPage: false, }); }); it('should handle empty results', () => { const result = calculatePagination({ page: 1, limit: 10, total: 0 }); expect(result).toEqual({ page: 1, limit: 10, total: 0, totalPages: 0, hasNextPage: false, hasPrevPage: false, }); }); it('should handle non-even page boundaries', () => { const result = calculatePagination({ page: 1, limit: 10, total: 25 }); expect(result).toEqual({ page: 1, limit: 10, total: 25, totalPages: 3, // ceil(25/10) = 3 hasNextPage: true, hasPrevPage: false, }); }); it('should handle page 2 of 3 with non-even total', () => { const result = calculatePagination({ page: 2, limit: 10, total: 25 }); expect(result).toEqual({ page: 2, limit: 10, total: 25, totalPages: 3, hasNextPage: true, hasPrevPage: true, }); }); it('should handle last page with non-even total', () => { const result = calculatePagination({ page: 3, limit: 10, total: 25 }); expect(result).toEqual({ page: 3, limit: 10, total: 25, totalPages: 3, hasNextPage: false, hasPrevPage: true, }); }); it('should handle limit of 1', () => { const result = calculatePagination({ page: 5, limit: 1, total: 10 }); expect(result).toEqual({ page: 5, limit: 1, total: 10, totalPages: 10, hasNextPage: true, hasPrevPage: true, }); }); it('should handle large limit with small total', () => { const result = calculatePagination({ page: 1, limit: 100, total: 5 }); expect(result).toEqual({ page: 1, limit: 100, total: 5, totalPages: 1, hasNextPage: false, hasPrevPage: false, }); }); }); describe('sendPaginated', () => { it('should send paginated response with data and pagination meta', () => { const data = [{ id: 1 }, { id: 2 }]; const pagination = { page: 1, limit: 10, total: 100 }; sendPaginated(mockRes, data, pagination); expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data, meta: { pagination: { page: 1, limit: 10, total: 100, totalPages: 10, hasNextPage: true, hasPrevPage: false, }, }, }); }); it('should include additional meta when provided', () => { const data = [{ id: 1 }]; const pagination = { page: 1, limit: 10, total: 1 }; const meta = { requestId: 'req-456' }; sendPaginated(mockRes, data, pagination, meta); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data, meta: { requestId: 'req-456', pagination: { page: 1, limit: 10, total: 1, totalPages: 1, hasNextPage: false, hasPrevPage: false, }, }, }); }); it('should handle empty array data', () => { const data: unknown[] = []; const pagination = { page: 1, limit: 10, total: 0 }; sendPaginated(mockRes, data, pagination); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: [], meta: { pagination: { page: 1, limit: 10, total: 0, totalPages: 0, hasNextPage: false, hasPrevPage: false, }, }, }); }); it('should always return status 200', () => { const data = [{ id: 1 }]; const pagination = { page: 1, limit: 10, total: 1 }; sendPaginated(mockRes, data, pagination); expect(mockRes.status).toHaveBeenCalledWith(200); }); }); describe('sendError', () => { it('should send error response with code and message', () => { sendError(mockRes, ErrorCode.VALIDATION_ERROR, 'Invalid input'); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: ErrorCode.VALIDATION_ERROR, message: 'Invalid input', }, }); }); it('should send error with custom status code', () => { sendError(mockRes, ErrorCode.NOT_FOUND, 'Resource not found', 404); expect(mockRes.status).toHaveBeenCalledWith(404); expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: ErrorCode.NOT_FOUND, message: 'Resource not found', }, }); }); it('should include details when provided', () => { const details = [ { field: 'email', message: 'Invalid email format' }, { field: 'password', message: 'Password too short' }, ]; sendError(mockRes, ErrorCode.VALIDATION_ERROR, 'Validation failed', 400, details); expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: ErrorCode.VALIDATION_ERROR, message: 'Validation failed', details, }, }); }); it('should include meta when provided', () => { const meta = { requestId: 'req-789', timestamp: '2024-01-15T12:00:00Z' }; sendError(mockRes, ErrorCode.INTERNAL_ERROR, 'Server error', 500, undefined, meta); expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: ErrorCode.INTERNAL_ERROR, message: 'Server error', }, meta, }); }); it('should include both details and meta when provided', () => { const details = { originalError: 'Database connection failed' }; const meta = { requestId: 'req-000' }; sendError(mockRes, ErrorCode.INTERNAL_ERROR, 'Database error', 500, details, meta); expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: ErrorCode.INTERNAL_ERROR, message: 'Database error', details, }, meta, }); }); it('should accept string error codes', () => { sendError(mockRes, 'CUSTOM_ERROR', 'Custom error message', 400); expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: 'CUSTOM_ERROR', message: 'Custom error message', }, }); }); it('should use default status 400 when not specified', () => { sendError(mockRes, ErrorCode.VALIDATION_ERROR, 'Error'); expect(mockRes.status).toHaveBeenCalledWith(400); }); it('should handle null details (not undefined)', () => { // null should be included as details, unlike undefined sendError(mockRes, ErrorCode.VALIDATION_ERROR, 'Error', 400, null); expect(mockRes.json).toHaveBeenCalledWith({ success: false, error: { code: ErrorCode.VALIDATION_ERROR, message: 'Error', details: null, }, }); }); }); describe('sendMessage', () => { it('should send success response with message', () => { sendMessage(mockRes, 'Operation completed successfully'); expect(mockRes.status).toHaveBeenCalledWith(200); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: { message: 'Operation completed successfully' }, }); }); it('should send message with custom status code', () => { sendMessage(mockRes, 'Resource created', 201); expect(mockRes.status).toHaveBeenCalledWith(201); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: { message: 'Resource created' }, }); }); it('should handle empty message', () => { sendMessage(mockRes, ''); expect(mockRes.json).toHaveBeenCalledWith({ success: true, data: { message: '' }, }); }); }); describe('ErrorCode re-export', () => { it('should export ErrorCode enum', () => { expect(ErrorCode).toBeDefined(); expect(ErrorCode.VALIDATION_ERROR).toBeDefined(); expect(ErrorCode.NOT_FOUND).toBeDefined(); expect(ErrorCode.INTERNAL_ERROR).toBeDefined(); }); }); });