From 24ad8a918b1192aef91139a83d4103322b0d8c3c Mon Sep 17 00:00:00 2001 From: junjie-w Date: Mon, 18 Nov 2024 22:26:08 +0100 Subject: [PATCH] fix: refactor tests and scripts --- .github/workflows/quality-checks.yml | 3 - README.md | 41 +- examples/README.md | 20 +- examples/package.json | 2 +- examples/src/docker-usage.js | 2 +- jest.config.js | 8 +- package-lock.json | 6 +- scripts/docker.sh | 2 +- scripts/help.sh | 69 ++-- src/{tests => __tests__}/api/echo.test.ts | 167 +++++--- .../integration/app.test.ts | 0 .../unit/middleware/errorHandler.test.ts | 242 +++++++++++ .../unit/middleware/jsonValidator.test.ts | 0 .../unit/middleware/notFoundHandler.test.ts | 0 .../unit/utils/logger.test.ts | 0 .../unit/utils/response.test.ts | 0 .../unit/utils/timing.test.ts | 0 src/app.ts | 1 - src/config/config.ts | 1 - .../unit/middleware/errorHandler.test.ts | 377 ------------------ src/types/index.ts | 1 - 21 files changed, 452 insertions(+), 490 deletions(-) rename src/{tests => __tests__}/api/echo.test.ts (51%) rename src/{tests => __tests__}/integration/app.test.ts (100%) create mode 100644 src/__tests__/unit/middleware/errorHandler.test.ts rename src/{tests => __tests__}/unit/middleware/jsonValidator.test.ts (100%) rename src/{tests => __tests__}/unit/middleware/notFoundHandler.test.ts (100%) rename src/{tests => __tests__}/unit/utils/logger.test.ts (100%) rename src/{tests => __tests__}/unit/utils/response.test.ts (100%) rename src/{tests => __tests__}/unit/utils/timing.test.ts (100%) delete mode 100644 src/tests/unit/middleware/errorHandler.test.ts diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 6ae77cd..cb2aed6 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -24,9 +24,6 @@ jobs: - name: Install dependencies run: npm ci - - name: Security audit - run: npm audit - - name: Run linting run: npm run lint diff --git a/README.md b/README.md index 022d8d7..176091b 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,14 @@ npm install @junjie-wu/echo-service docker pull junjiewu0/echo-service docker run -p 3000:3000 junjiewu0/echo-service +# For ARM-based machines (Apple Silicon, etc.) +docker pull --platform linux/amd64 junjiewu0/echo-service +docker run --platform linux/amd64 -p 3000:3000 junjiewu0/echo-service + # Using Docker Compose docker compose up -d -# Building Locally +# Build and Run Locally docker build -t echo-service . docker run -p 3000:3000 echo-service ``` @@ -56,17 +60,22 @@ import { createServer } from '@junjie-wu/echo-service'; const server = createServer(3000); ``` -### ๐Ÿงช Test the Service +### ๐Ÿงช Try it out ```bash # Check service health -http://localhost:3000/health +curl http://localhost:3000/health # Echo back request details -http://localhost:3000/echo +curl http://localhost:3000/echo + +# Echo with query parameters +curl "http://localhost:3000/echo?name=test" -# Supports all HTTP methods and parameters -http://localhost:3000/echo?name=test +# Echo with POST data +curl -X POST -H "Content-Type: application/json" \ + -d '{"message": "hello"}' \ + http://localhost:3000/echo ``` ### ๐Ÿ“‹ Examples @@ -94,15 +103,21 @@ npm run start:lib # Library usage ### Setup -1. Clone the repository: -```bash -git clone https://github.com/junjie-w/echo-service.git -cd echo-service -``` - -2. Install dependencies: ```bash +# Install dependencies npm install + +# Start development server +npm run dev + +# Run tests +npm test + +# Build for production +npm run build + +# Start production server +npm start ``` ### Commit Convention diff --git a/examples/README.md b/examples/README.md index d760a9a..9cd49a6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,9 +1,8 @@ # Echo Service Examples Examples of using the [Echo Service](https://github.com/junjie-w/echo-service) via [Docker image](https://hub.docker.com/r/junjiewu0/echo-service) and [NPM package](https://www.npmjs.com/package/@junjie-wu/echo-service). -Each example runs on a different port to avoid conflicts. -## ๐Ÿณ Docker Usage (Port 3003) +## ๐Ÿณ Docker Usage ### Using Container @@ -12,30 +11,30 @@ Each example runs on a different port to avoid conflicts. npm run start:docker # Stop container -npm run docker:stop +npm run stop:docker ``` ### Using Docker Compose ```bash # Start services -npm run compose:up +npm run start:docker-compose # View logs -npm run compose:logs +npm run logs:docker-compose # Stop services -npm run compose:down +npm run stop:docker-compose ``` -## ๐ŸŽฏ CLI Usage (Port 3002) +## ๐ŸŽฏ CLI Usage Using the package as a command-line tool: ```bash npm run start:cli ``` -## ๐Ÿ“ฆ Library Usage (Port 3001) +## ๐Ÿ“ฆ Library Usage Using the package as a library in your code: ```bash @@ -67,6 +66,7 @@ curl http://localhost:3001/echo ## โš ๏ธ Troubleshooting ### Port Already in Use + If you see "Port in use" error: ```bash # Check what's using the port @@ -76,8 +76,10 @@ lsof -i : kill -9 ``` -### Docker on Apple Silicon (M1/M2/M3) +### Docker on ARM-based machines (Apple Silicon, etc.) + The example automatically handles platform differences, but you can manually run: ```bash +docker pull --platform linux/amd64 junjiewu0/echo-service docker run --platform linux/amd64 -p 3003:3000 junjiewu0/echo-service ``` diff --git a/examples/package.json b/examples/package.json index 4b24009..c0b9025 100644 --- a/examples/package.json +++ b/examples/package.json @@ -8,7 +8,7 @@ "start:docker": "node src/docker-usage.js", "stop:docker": "docker stop echo-service-example && docker rm echo-service-example || true", "start:docker-compose": "docker compose up -d", - "stop:docker-compose": "docker-compose down", + "stop:docker-compose": "docker compose down", "logs:docker-compose": "docker compose logs -f" }, "dependencies": { diff --git a/examples/src/docker-usage.js b/examples/src/docker-usage.js index e1d6828..74cda7f 100644 --- a/examples/src/docker-usage.js +++ b/examples/src/docker-usage.js @@ -68,7 +68,7 @@ docker rm ${CONTAINER_NAME} Troubleshooting: 1. Make sure Docker is running 2. Check if port ${DOCKER_PORT} is available -3. For Apple Silicon Macs (M1/M2/M3), try manually: +3. For ARM-based machines (Apple Silicon, etc.), try manually: docker pull --platform linux/amd64 ${IMAGE_NAME}:${IMAGE_TAG} docker run --platform linux/amd64 -p ${DOCKER_PORT}:3000 ${IMAGE_NAME}:${IMAGE_TAG} diff --git a/jest.config.js b/jest.config.js index 01c855e..580aaaf 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,19 +18,19 @@ export default { { displayName: '๐Ÿงช UNIT', ...baseConfig, - testMatch: ['/src/tests/unit/**/*.test.ts'], + testMatch: ['/src/__tests__/unit/**/*.test.ts'], rootDir: '.' }, { displayName: '๐Ÿ”„ INTEGRATION', ...baseConfig, - testMatch: ['/src/tests/integration/**/*.test.ts'], + testMatch: ['/src/__tests__/integration/**/*.test.ts'], rootDir: '.' }, { displayName: '๐ŸŒ API', ...baseConfig, - testMatch: ['/src/tests/api/**/*.test.ts'], + testMatch: ['/src/__tests__/api/**/*.test.ts'], rootDir: '.' } ], @@ -44,7 +44,7 @@ export default { }, collectCoverageFrom: [ 'src/**/*.{ts,js}', - '!src/tests/**', + '!src/__tests__/**', '!src/**/*.d.ts', '!src/types/**', '!src/server.ts', diff --git a/package-lock.json b/package-lock.json index 6858d42..2211c07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4658,9 +4658,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", diff --git a/scripts/docker.sh b/scripts/docker.sh index e73ccf7..72cad66 100755 --- a/scripts/docker.sh +++ b/scripts/docker.sh @@ -109,7 +109,7 @@ help() { # Main execution main() { command="$1" - shift # Remove the first argument + shift if [[ " ${allowed_targets[@]} " =~ " ${command} " ]]; then $command "$@" else diff --git a/scripts/help.sh b/scripts/help.sh index a1a7c04..d44b154 100755 --- a/scripts/help.sh +++ b/scripts/help.sh @@ -6,33 +6,44 @@ Echo Service Commands Development ---------- -npm run dev # Start development server -npm start # Start production server - -Docker ------- -npm run docker:build # Build Docker image -npm run docker:start # Start Docker container -npm run docker:stop # Stop Docker container - -Package Development ------------------ -npm run cli # Run CLI locally -npm run link # Link package globally -npm run unlink # Unlink @junjie-wu/echo-service globally - -Global Usage ------------ -# Install globally -npm install -g @junjie-wu/echo-service - -# Run anywhere -echo-service - -# Or use npx without installing -npx @junjie-wu/echo-service - -Environment Variables -------------------- -PORT # Specify port (default: 3000) +npm run dev # Start development server with auto-reload (tsx watch) +npm run build # Build TypeScript project +npm start # Start production server +npm run prepare # Setup Husky git hooks + +Testing +------- +npm test # Run all tests +npm run test:watch # Run tests in watch mode +npm run test:coverage # Generate test coverage report +npm run test:ci # Run tests in CI mode with coverage +npm run test:unit # Run unit tests only +npm run test:integration # Run integration tests only +npm run test:api # Run API tests only +npm run start:test # Start test server +npm run stop:test # Stop test server + +Linting +------- +npm run lint # Run ESLint +npm run lint:fix # Fix ESLint issues automatically + +Docker Operations +--------------- +npm run docker:build # Build Docker image +npm run docker:start # Start Docker container +npm run docker:stop # Stop Docker container +npm run docker:logs # View container logs +npm run docker:clean # Remove container and image +npm run docker:shell # Open a shell in the container +npm run docker:info # Show container information +npm run docker:restart # Restart Docker container +npm run docker:help # Show all Docker commands + +CLI Development & Usage +----------------------- +npm run cli # Run CLI locally from dist/ +npm run link # Link package globally +npm run unlink # Unlink package globally +npx @junjie-wu/echo-service # Run CLI without installing " \ No newline at end of file diff --git a/src/tests/api/echo.test.ts b/src/__tests__/api/echo.test.ts similarity index 51% rename from src/tests/api/echo.test.ts rename to src/__tests__/api/echo.test.ts index b9a9a33..3da091b 100644 --- a/src/tests/api/echo.test.ts +++ b/src/__tests__/api/echo.test.ts @@ -25,7 +25,7 @@ const waitForServer = async (url: string, maxAttempts = 10): Promise => { }; describe('Echo Service E2E Tests', () => { - jest.setTimeout(30000); + jest.setTimeout(30000); beforeAll(async () => { if (!process.env.API_URL) { @@ -53,7 +53,7 @@ describe('Echo Service E2E Tests', () => { throw error; } } - }, 30000); + }, 30000); afterAll(async () => { if (!process.env.API_URL) { @@ -73,10 +73,10 @@ describe('Echo Service E2E Tests', () => { console.error('Failed to stop server:', error); } } - }, 10000); + }, 10000); describe('Basic API Health', () => { - it('should be healthy', async () => { + it('should have healthy status', async () => { const response = await request(API_URL) .get('/health') .expect(200); @@ -87,7 +87,7 @@ describe('Echo Service E2E Tests', () => { })); }); - it('should handle basic request-response cycle', async () => { + it('should handle basic echo request', async () => { const testData = { test: 'basic-cycle' }; const response = await request(API_URL) .post('/echo') @@ -99,33 +99,6 @@ describe('Echo Service E2E Tests', () => { }); }); - describe('API Reliability', () => { - it('should handle parallel requests', async () => { - const requests = Array(5).fill(null).map((_, index) => - request(API_URL) - .post('/echo') - .send({ test: `parallel-${index}` }) - ); - - const responses = await Promise.all(requests); - responses.forEach((response, index) => { - expect(response.status).toBe(200); - expect(response.body.requestEcho.body.test).toBe(`parallel-${index}`); - }); - }); - - // Your existing performance test can stay here - it('should maintain response time under threshold', async () => { - const startTime = Date.now(); - await request(API_URL) - .get('/echo') - .expect(200); - - const responseTime = Date.now() - startTime; - expect(responseTime).toBeLessThan(200); - }); - }); - describe('Error Handling', () => { it('should handle invalid JSON gracefully', async () => { const response = await request(API_URL) @@ -153,29 +126,131 @@ describe('Echo Service E2E Tests', () => { }); }); - describe('External API Tests', () => { - it('should handle high load', async () => { - const requests = Array(10).fill(null).map(() => + describe('Performance and Load Testing', () => { + const PERFORMANCE_THRESHOLD = 200; + + it('should handle parallel requests with consistent results', async () => { + const NUM_PARALLEL = 5; + const requests = Array(NUM_PARALLEL).fill(null).map((_, index) => request(API_URL) .post('/echo') - .send({ test: 'load' }) + .send({ test: `parallel-${index}` }) ); - + + const startTime = Date.now(); const responses = await Promise.all(requests); - responses.forEach(response => { + const totalTime = Date.now() - startTime; + + responses.forEach((response, index) => { expect(response.status).toBe(200); + expect(response.body.requestEcho.body.test).toBe(`parallel-${index}`); }); - }, 10000); - it('should maintain response time under threshold', async () => { + expect(totalTime).toBeLessThan(PERFORMANCE_THRESHOLD * 2); + + console.log('Parallel requests completed in:', totalTime, 'ms'); + }); + + it('should handle different HTTP methods concurrently', async () => { + type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch'; + const methods: HttpMethod[] = ['get', 'post', 'put', 'delete', 'patch']; + const startTime = Date.now(); - await request(API_URL) - .get('/echo') - .expect(200); + const responses = await Promise.all(methods.map(method => { + const req = request(API_URL); + switch (method) { + case 'get': + return req.get('/echo'); + case 'post': + return req.post('/echo').send({ test: 'post' }); + case 'put': + return req.put('/echo').send({ test: 'put' }); + case 'delete': + return req.delete('/echo'); + case 'patch': + return req.patch('/echo').send({ test: 'patch' }); + } + })); + + const totalTime = Date.now() - startTime; + + expect(responses.every(r => r.status === 200)).toBe(true); + expect(totalTime).toBeLessThan(PERFORMANCE_THRESHOLD * 2); + + console.log('Mixed methods test completed in:', totalTime, 'ms'); + }); + + it('should maintain consistent response time under load', async () => { + const NUM_REQUESTS = 10; + const requests = Array(NUM_REQUESTS).fill(null).map(async (_, index) => { + const startTime = Date.now(); + const response = await request(API_URL) + .post('/echo') + .send({ test: `load-${index}` }); + + return { + status: response.status, + responseTime: Date.now() - startTime + }; + }); - const responseTime = Date.now() - startTime; - expect(responseTime).toBeLessThan(200); - }, 10000); + const results = await Promise.all(requests); + + const stats = results.reduce((acc, result) => ({ + totalTime: acc.totalTime + result.responseTime, + maxTime: Math.max(acc.maxTime, result.responseTime), + minTime: Math.min(acc.minTime, result.responseTime), + successCount: acc.successCount + (result.status === 200 ? 1 : 0) + }), { + totalTime: 0, + maxTime: -Infinity, + minTime: Infinity, + successCount: 0 + }); + + expect(stats.successCount).toBe(NUM_REQUESTS); + expect(stats.maxTime).toBeLessThan(PERFORMANCE_THRESHOLD); + + console.log('Load test results:', { + averageResponseTime: Math.round(stats.totalTime / NUM_REQUESTS), + maxResponseTime: stats.maxTime, + minResponseTime: stats.minTime, + successRate: `${(stats.successCount / NUM_REQUESTS) * 100}%` + }); + }, 10000); + + it('should handle varying payload sizes efficiently', async () => { + const payloadSizes = [1, 10, 100, 1000]; + const results = await Promise.all( + payloadSizes.map(async (size) => { + const startTime = Date.now(); + const response = await request(API_URL) + .post('/echo') + .send({ + array: Array(size).fill('test'), + timestamp: Date.now() + }); + + return { + size, + responseTime: Date.now() - startTime, + status: response.status + }; + }) + ); + + results.forEach(result => { + expect(result.status).toBe(200); + expect(result.responseTime).toBeLessThan(PERFORMANCE_THRESHOLD * 2); + }); + + console.log('Payload size test results:', + results.map(r => ({ + size: r.size, + responseTime: r.responseTime + })) + ); + }); }); }); diff --git a/src/tests/integration/app.test.ts b/src/__tests__/integration/app.test.ts similarity index 100% rename from src/tests/integration/app.test.ts rename to src/__tests__/integration/app.test.ts diff --git a/src/__tests__/unit/middleware/errorHandler.test.ts b/src/__tests__/unit/middleware/errorHandler.test.ts new file mode 100644 index 0000000..f9cf32b --- /dev/null +++ b/src/__tests__/unit/middleware/errorHandler.test.ts @@ -0,0 +1,242 @@ +import { NextFunction, Request, Response } from 'express'; + +import { errorHandler } from '../../../middleware/errorHandler.js'; +import { HttpError } from '../../../types/index.js'; +import { logger } from '../../../utils/logger.js'; + +jest.mock('../../../utils/logger.js'); + +describe('Error Handler Middleware', () => { + let mockRequest: Partial; + let mockResponse: Partial; + let nextFunction: NextFunction; + const mockDate = '2024-01-01T00:00:00.000Z'; + const originalEnv = process.env.NODE_ENV; + + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date(mockDate)); + + mockRequest = { + method: 'GET', + url: '/test', + path: '/test', + headers: {} + }; + + mockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn() + }; + + nextFunction = jest.fn(); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.useRealTimers(); + process.env.NODE_ENV = originalEnv; + }); + + describe('Environment-specific Error Handling', () => { + describe('Development Environment', () => { + beforeEach(() => { + process.env.NODE_ENV = 'development'; + }); + + it('should show original message for 500 HttpError', () => { + const error = new HttpError(500, 'Original 500 error message'); + errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: 'Original 500 error message', + timestamp: mockDate, + path: '/test', + requestId: undefined + } + }); + }); + + it('should show original message for non-500 HttpError', () => { + const error = new HttpError(400, 'Bad Request Message'); + errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.status).toHaveBeenCalledWith(400); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: 'Bad Request Message', + timestamp: mockDate, + path: '/test', + requestId: undefined + } + }); + }); + }); + + describe('Production Environment', () => { + beforeEach(() => { + process.env.NODE_ENV = 'production'; + }); + + it('should show "Internal Server Error" for 500 HttpError', () => { + const error = new HttpError(500, 'Original 500 error message'); + errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: 'Internal Server Error', + timestamp: mockDate, + path: '/test', + requestId: undefined + } + }); + }); + + it('should show original message for client errors (4xx)', () => { + const error = new HttpError(400, 'Bad Request Message'); + errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.status).toHaveBeenCalledWith(400); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: 'Bad Request Message', + timestamp: mockDate, + path: '/test', + requestId: undefined + } + }); + }); + + it('should show original message for server errors other than 500', () => { + const error = new HttpError(501, 'Not Implemented'); + errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.status).toHaveBeenCalledWith(501); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: 'Not Implemented', + timestamp: mockDate, + path: '/test', + requestId: undefined + } + }); + }); + }); + }); + + describe('Request ID Handling', () => { + it('should include request ID when available', () => { + const requestId = 'test-request-id'; + const requestWithId = { + ...mockRequest, + headers: { 'x-request-id': requestId } + }; + + const error = new Error('Test error'); + errorHandler(error, requestWithId as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: error.message, + timestamp: mockDate, + path: '/test', + requestId + } + }); + + expect(logger.error).toHaveBeenCalledWith({ + err: error, + method: 'GET', + url: '/test', + requestId + }); + }); + + it('should handle malformed request IDs', () => { + const requestWithMalformedId = { + ...mockRequest, + headers: { 'x-request-id': ['multiple', 'ids'] } + }; + + const error = new Error('Test error'); + errorHandler(error, requestWithMalformedId as unknown as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: error.message, + timestamp: mockDate, + path: '/test', + requestId: 'multiple,ids' + } + }); + }); + }); + + describe('Special Error Types', () => { + it('should handle SyntaxError with body property', () => { + const syntaxError = new SyntaxError('Invalid JSON'); + (syntaxError as SyntaxError & { body: string }).body = '{"invalid": "json"'; + + errorHandler(syntaxError, mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.status).toHaveBeenCalledWith(400); + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: 'Invalid JSON format', + timestamp: mockDate, + path: '/test', + requestId: undefined + } + }); + }); + + it('should handle custom error types', () => { + process.env.NODE_ENV = 'production'; + + class CustomError extends Error { + constructor(message: string, public code: string) { + super(message); + this.name = 'CustomError'; + } + } + + const customError = new CustomError('Custom error', 'CUSTOM_ERROR'); + errorHandler(customError, mockRequest as Request, mockResponse as Response, nextFunction); + + expect(logger.error).toHaveBeenCalledWith({ + err: customError, + method: 'GET', + url: '/test', + requestId: 'unknown' + }); + + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: 'Internal Server Error', + timestamp: mockDate, + path: '/test', + requestId: undefined + } + }); + }); + + it('should handle errors with undefined messages', () => { + process.env.NODE_ENV = 'development'; + const errorWithoutMessage = new Error(); + + errorHandler(errorWithoutMessage, mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockResponse.json).toHaveBeenCalledWith({ + error: { + message: '', + timestamp: mockDate, + path: '/test', + requestId: undefined + } + }); + }); + }); +}); diff --git a/src/tests/unit/middleware/jsonValidator.test.ts b/src/__tests__/unit/middleware/jsonValidator.test.ts similarity index 100% rename from src/tests/unit/middleware/jsonValidator.test.ts rename to src/__tests__/unit/middleware/jsonValidator.test.ts diff --git a/src/tests/unit/middleware/notFoundHandler.test.ts b/src/__tests__/unit/middleware/notFoundHandler.test.ts similarity index 100% rename from src/tests/unit/middleware/notFoundHandler.test.ts rename to src/__tests__/unit/middleware/notFoundHandler.test.ts diff --git a/src/tests/unit/utils/logger.test.ts b/src/__tests__/unit/utils/logger.test.ts similarity index 100% rename from src/tests/unit/utils/logger.test.ts rename to src/__tests__/unit/utils/logger.test.ts diff --git a/src/tests/unit/utils/response.test.ts b/src/__tests__/unit/utils/response.test.ts similarity index 100% rename from src/tests/unit/utils/response.test.ts rename to src/__tests__/unit/utils/response.test.ts diff --git a/src/tests/unit/utils/timing.test.ts b/src/__tests__/unit/utils/timing.test.ts similarity index 100% rename from src/tests/unit/utils/timing.test.ts rename to src/__tests__/unit/utils/timing.test.ts diff --git a/src/app.ts b/src/app.ts index 3f2ca51..7a0ee57 100644 --- a/src/app.ts +++ b/src/app.ts @@ -52,7 +52,6 @@ app.all('/echo', (req, res, next) => { }, serviceInfo: { sourceCode: SERVICE_INFO.SOURCE_CODE, - version: config.VERSION, environment: config.NODE_ENV } }; diff --git a/src/config/config.ts b/src/config/config.ts index 0b5b22a..06a965c 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -8,5 +8,4 @@ export const SERVICE_INFO = { export const config = { NODE_ENV: process.env.NODE_ENV || 'development', PORT: process.env.PORT || '3000', - VERSION: process.env.npm_package_version || '1.0.0' } as const; diff --git a/src/tests/unit/middleware/errorHandler.test.ts b/src/tests/unit/middleware/errorHandler.test.ts deleted file mode 100644 index 8e5ce11..0000000 --- a/src/tests/unit/middleware/errorHandler.test.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { NextFunction, Request, Response } from 'express'; - -import { errorHandler } from '../../../middleware/errorHandler.js'; -import { HttpError } from '../../../types/index.js'; -import { logger } from '../../../utils/logger.js'; - -jest.mock('../../../utils/logger.js'); - -describe('errorHandler', () => { - let mockRequest: Partial; - let mockResponse: Partial; - let nextFunction: NextFunction; - const mockDate = '2024-01-01T00:00:00.000Z'; - const originalEnv = process.env.NODE_ENV; - - beforeEach(() => { - jest.useFakeTimers(); - jest.setSystemTime(new Date(mockDate)); - - mockRequest = { - method: 'GET', - url: '/test', - path: '/test', - headers: {} - }; - - mockResponse = { - status: jest.fn().mockReturnThis(), - json: jest.fn() - }; - - nextFunction = jest.fn(); - jest.clearAllMocks(); - }); - - afterEach(() => { - jest.useRealTimers(); - process.env.NODE_ENV = originalEnv; - }); - - // it('should handle errors in production mode', () => { - // process.env.NODE_ENV = 'production'; - // const error = new Error('Test error'); - - // errorHandler( - // error, - // mockRequest as Request, - // mockResponse as Response, - // nextFunction - // ); - - // expect(mockResponse.status).toHaveBeenCalledWith(500); - // expect(mockResponse.json).toHaveBeenCalledWith({ - // error: { - // message: 'Internal Server Error', - // timestamp: mockDate, - // path: '/test', - // requestId: undefined - // } - // }); - - // expect(logger.error).toHaveBeenCalledWith({ - // err: error, - // method: 'GET', - // url: '/test', - // requestId: 'unknown' - // }); - // }); - - // it('should handle errors in development mode', () => { - // process.env.NODE_ENV = 'development'; - // const error = new Error('Test error'); - - // errorHandler( - // error, - // mockRequest as Request, - // mockResponse as Response, - // nextFunction - // ); - - // expect(logger.error).toHaveBeenCalledWith({ - // err: error, - // method: 'GET', - // url: '/test', - // requestId: 'unknown' - // }); - - // expect(mockResponse.json).toHaveBeenCalledWith({ - // error: { - // message: error.message, - // timestamp: mockDate, - // path: '/test', - // requestId: undefined - // } - // }); - // }); - describe('HttpError handling in production mode', () => { - beforeEach(() => { - process.env.NODE_ENV = 'production'; - }); - - it('should show "Internal Server Error" for 500 HttpError', () => { - const error = new HttpError(500, 'Original 500 error message'); - - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.status).toHaveBeenCalledWith(500); - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: 'Internal Server Error', - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - - it('should show original message for non-500 HttpError', () => { - const error = new HttpError(400, 'Bad Request Message'); - - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.status).toHaveBeenCalledWith(400); - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: 'Bad Request Message', // Original message should be shown - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - - it('should show original message for 501 HttpError', () => { - const error = new HttpError(501, 'Not Implemented'); - - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.status).toHaveBeenCalledWith(501); - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: 'Not Implemented', // Original message should be shown - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - }); - - describe('HttpError handling in development mode', () => { - beforeEach(() => { - process.env.NODE_ENV = 'development'; - }); - - it('should always show original message for 500 HttpError', () => { - const error = new HttpError(500, 'Original 500 error message'); - - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.status).toHaveBeenCalledWith(500); - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: 'Original 500 error message', - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - - it('should show original message for non-500 HttpError', () => { - const error = new HttpError(400, 'Bad Request Message'); - - errorHandler( - error, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.status).toHaveBeenCalledWith(400); - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: 'Bad Request Message', - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - }); - - it('should include request ID when available', () => { - const requestId = 'test-request-id'; - const requestWithId = { - ...mockRequest, - headers: { - 'x-request-id': requestId - } - }; - - const error = new Error('Test error'); - - errorHandler( - error, - requestWithId as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: error.message, - timestamp: mockDate, - path: '/test', - requestId: requestId - } - }); - - expect(logger.error).toHaveBeenCalledWith({ - err: error, - method: 'GET', - url: '/test', - requestId: requestId - }); - }); - - describe('edge cases', () => { - it('should handle errors with undefined messages', () => { - const errorWithoutMessage = new Error(); - process.env.NODE_ENV = 'development'; - - errorHandler( - errorWithoutMessage, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: '', - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - - it('should handle malformed request IDs', () => { - const requestWithMalformedId = { - ...mockRequest, - headers: { - 'x-request-id': ['multiple', 'ids'] - } - }; - - const error = new Error('Test error'); - - errorHandler( - error, - requestWithMalformedId as unknown as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: error.message, - timestamp: mockDate, - path: '/test', - requestId: 'multiple,ids' - } - }); - }); - - it('should handle SyntaxError with body property', () => { - const syntaxError = new SyntaxError('Invalid JSON'); - (syntaxError as SyntaxError & { body: string }).body = '{"invalid": "json"'; - - errorHandler( - syntaxError, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.status).toHaveBeenCalledWith(400); - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: 'Invalid JSON format', - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - it('should handle HttpError in production mode', () => { - process.env.NODE_ENV = 'production'; - const httpError = new HttpError(404, 'Not Found'); - - errorHandler( - httpError, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(mockResponse.status).toHaveBeenCalledWith(404); - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: httpError.message, - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - - it('should handle errors with custom properties', () => { - process.env.NODE_ENV = 'production'; - - class CustomError extends Error { - constructor(message: string, public code: string) { - super(message); - this.name = 'CustomError'; - } - } - - const customError = new CustomError('Custom error', 'CUSTOM_ERROR'); - - errorHandler( - customError, - mockRequest as Request, - mockResponse as Response, - nextFunction - ); - - expect(logger.error).toHaveBeenCalledWith({ - err: customError, - method: 'GET', - url: '/test', - requestId: 'unknown' - }); - - expect(mockResponse.json).toHaveBeenCalledWith({ - error: { - message: 'Internal Server Error', - timestamp: mockDate, - path: '/test', - requestId: undefined - } - }); - }); - }); -}); diff --git a/src/types/index.ts b/src/types/index.ts index 286731f..0f9c149 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -27,7 +27,6 @@ export interface ServerInfo { export interface ServiceInfo { sourceCode: string; - version: string; environment: string; }