| | import { handleJsonParseError } from './json'; |
| | import type { Request, Response, NextFunction } from 'express'; |
| |
|
| | describe('handleJsonParseError', () => { |
| | let req: Partial<Request>; |
| | let res: Partial<Response>; |
| | let next: NextFunction; |
| | let jsonSpy: jest.Mock; |
| | let statusSpy: jest.Mock; |
| |
|
| | beforeEach(() => { |
| | req = { |
| | path: '/api/test', |
| | method: 'POST', |
| | ip: '127.0.0.1', |
| | }; |
| |
|
| | jsonSpy = jest.fn(); |
| | statusSpy = jest.fn().mockReturnValue({ json: jsonSpy }); |
| |
|
| | res = { |
| | status: statusSpy, |
| | json: jsonSpy, |
| | }; |
| |
|
| | next = jest.fn(); |
| | }); |
| |
|
| | describe('JSON parse errors', () => { |
| | it('should handle JSON SyntaxError with 400 status', () => { |
| | const err = new SyntaxError('Unexpected token < in JSON at position 0') as SyntaxError & { |
| | status?: number; |
| | body?: unknown; |
| | }; |
| | err.status = 400; |
| | err.body = {}; |
| |
|
| | handleJsonParseError(err, req as Request, res as Response, next); |
| |
|
| | expect(statusSpy).toHaveBeenCalledWith(400); |
| | expect(jsonSpy).toHaveBeenCalledWith({ |
| | error: 'Invalid JSON format', |
| | message: 'The request body contains malformed JSON', |
| | }); |
| | expect(next).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should not reflect user input in error message', () => { |
| | const maliciousInput = '<script>alert("xss")</script>'; |
| | const err = new SyntaxError( |
| | `Unexpected token < in JSON at position 0: ${maliciousInput}`, |
| | ) as SyntaxError & { |
| | status?: number; |
| | body?: unknown; |
| | }; |
| | err.status = 400; |
| | err.body = maliciousInput; |
| |
|
| | handleJsonParseError(err, req as Request, res as Response, next); |
| |
|
| | expect(statusSpy).toHaveBeenCalledWith(400); |
| | const errorResponse = jsonSpy.mock.calls[0][0]; |
| | expect(errorResponse.message).not.toContain(maliciousInput); |
| | expect(errorResponse.message).toBe('The request body contains malformed JSON'); |
| | expect(next).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should handle JSON parse error with HTML tags in body', () => { |
| | const err = new SyntaxError('Invalid JSON') as SyntaxError & { |
| | status?: number; |
| | body?: unknown; |
| | }; |
| | err.status = 400; |
| | err.body = '<html><body><h1>XSS</h1></body></html>'; |
| |
|
| | handleJsonParseError(err, req as Request, res as Response, next); |
| |
|
| | expect(statusSpy).toHaveBeenCalledWith(400); |
| | const errorResponse = jsonSpy.mock.calls[0][0]; |
| | expect(errorResponse.message).not.toContain('<html>'); |
| | expect(errorResponse.message).not.toContain('<script>'); |
| | expect(next).not.toHaveBeenCalled(); |
| | }); |
| | }); |
| |
|
| | describe('non-JSON errors', () => { |
| | it('should pass through non-SyntaxError errors', () => { |
| | const err = new Error('Some other error'); |
| |
|
| | handleJsonParseError(err, req as Request, res as Response, next); |
| |
|
| | expect(next).toHaveBeenCalledWith(err); |
| | expect(statusSpy).not.toHaveBeenCalled(); |
| | expect(jsonSpy).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should pass through SyntaxError without status 400', () => { |
| | const err = new SyntaxError('Some syntax error') as SyntaxError & { status?: number }; |
| | err.status = 500; |
| |
|
| | handleJsonParseError(err, req as Request, res as Response, next); |
| |
|
| | expect(next).toHaveBeenCalledWith(err); |
| | expect(statusSpy).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should pass through SyntaxError without body property', () => { |
| | const err = new SyntaxError('Some syntax error') as SyntaxError & { status?: number }; |
| | err.status = 400; |
| |
|
| | handleJsonParseError(err, req as Request, res as Response, next); |
| |
|
| | expect(next).toHaveBeenCalledWith(err); |
| | expect(statusSpy).not.toHaveBeenCalled(); |
| | }); |
| |
|
| | it('should pass through TypeError', () => { |
| | const err = new TypeError('Type error'); |
| |
|
| | handleJsonParseError(err, req as Request, res as Response, next); |
| |
|
| | expect(next).toHaveBeenCalledWith(err); |
| | expect(statusSpy).not.toHaveBeenCalled(); |
| | }); |
| | }); |
| |
|
| | describe('security verification', () => { |
| | it('should return generic error message for all JSON parse errors', () => { |
| | const testCases = [ |
| | 'Unexpected token < in JSON', |
| | 'Unexpected end of JSON input', |
| | 'Invalid or unexpected token', |
| | '<script>alert(1)</script>', |
| | '"><img src=x onerror=alert(1)>', |
| | ]; |
| |
|
| | testCases.forEach((errorMsg) => { |
| | const err = new SyntaxError(errorMsg) as SyntaxError & { |
| | status?: number; |
| | body?: unknown; |
| | }; |
| | err.status = 400; |
| | err.body = errorMsg; |
| |
|
| | jsonSpy.mockClear(); |
| | statusSpy.mockClear(); |
| | (next as jest.Mock).mockClear(); |
| |
|
| | handleJsonParseError(err, req as Request, res as Response, next); |
| |
|
| | const errorResponse = jsonSpy.mock.calls[0][0]; |
| | |
| | expect(errorResponse.message).toBe('The request body contains malformed JSON'); |
| | expect(errorResponse.error).toBe('Invalid JSON format'); |
| | }); |
| | }); |
| | }); |
| | }); |
| |
|