Files
flyer-crawler.projectium.com/src/utils/apiResponse.test.ts
Torben Sorensen 6e36cc3b07
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m34s
logging + e2e test fixes
2026-01-12 19:10:29 -08:00

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();
});
});
});