All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m34s
470 lines
12 KiB
TypeScript
470 lines
12 KiB
TypeScript
// 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();
|
|
});
|
|
});
|
|
});
|