UNPKG

qgenutils

Version:

A security-first Node.js utility library providing authentication, HTTP operations, URL processing, validation, datetime formatting, and template rendering. Designed as a lightweight alternative to heavy npm packages with comprehensive error handling and

257 lines (212 loc) 11.6 kB
// Unit tests for HTTP helpers covering header sanitization, content-length // calculations, and generic response handling to guarantee consistent behavior // for API proxying scenarios. const { calculateContentLength, buildCleanHeaders, getRequiredHeader, HEADERS_TO_REMOVE } = require('../../lib/http'); // include constant for immutability tests const { sendJsonResponse } = require('../../lib/response-utils'); describe('HTTP Utilities', () => { // confirms consistent proxy behavior describe('calculateContentLength', () => { // ensures payload sizes computed predictably // verifies should calculate length for string body test('should calculate length for string body', () => { expect(calculateContentLength('Hello World')).toBe('11'); // bytes count for string body }); // verifies should calculate length for object body test('should calculate length for object body', () => { const obj = { name: 'John', age: 30 }; const expected = Buffer.byteLength(JSON.stringify(obj), 'utf8').toString(); expect(calculateContentLength(obj)).toBe(expected); // verifies object size calculation }); // verifies should return "0" for empty string test('should return "0" for empty string', () => { expect(calculateContentLength('')).toBe('0'); // no content should return zero }); // verifies should return "0" for empty object test('should return "0" for empty object', () => { expect(calculateContentLength({})).toBe('0'); // empty object yields zero bytes }); // verifies should return "0" for null body test('should return "0" for null body', () => { expect(calculateContentLength(null)).toBe('0'); // null body produces zero }); // verifies should throw error for undefined body test('should throw error for undefined body', () => { expect(() => calculateContentLength(undefined)).toThrow('Body is undefined'); // undefined body should throw }); // verifies should handle UTF-8 characters correctly test('should handle UTF-8 characters correctly', () => { const utf8String = 'café'; const expected = Buffer.byteLength(utf8String, 'utf8').toString(); expect(calculateContentLength(utf8String)).toBe(expected); // verify multibyte chars counted correctly }); // verifies should handle complex nested objects test('should handle complex nested objects', () => { const complexObj = { user: { name: 'John', settings: { theme: 'dark' } }, data: [1, 2, 3] }; const expected = Buffer.byteLength(JSON.stringify(complexObj), 'utf8').toString(); expect(calculateContentLength(complexObj)).toBe(expected); // nested object should serialize consistently }); // verifies should calculate length for Buffer body test('should calculate length for Buffer body', () => { const buf = Buffer.from('abc'); expect(calculateContentLength(buf)).toBe(buf.length.toString()); // buffer length returned as string }); }); describe('buildCleanHeaders', () => { // guards against unsafe headers leaking const originalHeaders = { 'authorization': 'Bearer token123', 'content-type': 'application/json', 'host': 'example.com', 'x-target-url': 'https://api.example.com', 'cf-ray': '12345', 'user-agent': 'MyApp/1.0' }; // verifies should remove dangerous headers for GET request test('should remove dangerous headers for GET request', () => { const result = buildCleanHeaders(originalHeaders, 'GET', null); expect(result['authorization']).toBe('Bearer token123'); // auth header preserved expect(result['content-type']).toBe('application/json'); // content-type kept expect(result['user-agent']).toBe('MyApp/1.0'); // user-agent kept expect(result['host']).toBeUndefined(); // host removed for security expect(result['x-target-url']).toBeUndefined(); // upstream url stripped expect(result['cf-ray']).toBeUndefined(); // cf specific header removed expect(result['content-length']).toBeUndefined(); // not added for GET }); // verifies should remove dangerous headers and set content-length for POST with body test('should remove dangerous headers and set content-length for POST with body', () => { const body = { name: 'test' }; const result = buildCleanHeaders(originalHeaders, 'POST', body); expect(result['authorization']).toBe('Bearer token123'); // auth header preserved for POST expect(result['content-type']).toBe('application/json'); // content-type kept expect(result['content-length']).toBe(calculateContentLength(body)); // length header set from body expect(result['host']).toBeUndefined(); // host stripped expect(result['x-target-url']).toBeUndefined(); // remove forwarding header expect(result['cf-ray']).toBeUndefined(); // remove cf data }); // verifies should not set content-length for POST without body test('should not set content-length for POST without body', () => { const result = buildCleanHeaders(originalHeaders, 'POST', null); expect(result['content-length']).toBeUndefined(); // no body means no length header }); // verifies should remove content-length when body is empty object test('should remove content-length when body is empty object', () => { const input = { 'content-length': '10' }; const result = buildCleanHeaders(input, 'POST', {}); expect(result['content-length']).toBeUndefined(); // empty object should remove length }); // verifies should remove content-length when body is empty buffer test('should remove content-length when body is empty buffer', () => { const input = { 'content-length': '5' }; const result = buildCleanHeaders(input, 'POST', Buffer.alloc(0)); expect(result['content-length']).toBeUndefined(); // empty buffer should remove length }); // verifies should handle empty headers object test('should handle empty headers object', () => { const result = buildCleanHeaders({}, 'GET', null); expect(Object.keys(result)).toHaveLength(0); // result should stay empty }); // verifies should not mutate original headers test('should not mutate original headers', () => { const original = { ...originalHeaders }; buildCleanHeaders(originalHeaders, 'GET', null); expect(originalHeaders).toEqual(original); // verify immutability }); // verifies should default to GET when method is invalid test('should default to GET when method is invalid', () => { const result = buildCleanHeaders(originalHeaders, 123, null); expect(result['content-length']).toBeUndefined(); // invalid method defaults to GET expect(result['host']).toBeUndefined(); // host still removed }); }); describe('sendJsonResponse', () => { // checks generic JSON response helper let mockRes; beforeEach(() => { mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; }); // verifies should send JSON response with correct status test('should send JSON response with correct status', () => { const data = { message: 'Success' }; sendJsonResponse(mockRes, 200, data); expect(mockRes.status).toHaveBeenCalledWith(200); // status should match provided code expect(mockRes.json).toHaveBeenCalledWith(data); // response payload should be sent }); // verifies should handle error responses test('should handle error responses', () => { const errorData = { error: 'Not found' }; sendJsonResponse(mockRes, 404, errorData); expect(mockRes.status).toHaveBeenCalledWith(404); // error status is set expect(mockRes.json).toHaveBeenCalledWith(errorData); // error payload returned }); }); describe('getRequiredHeader', () => { // verifies header retrieval with fallback let mockReq, mockRes; beforeEach(() => { mockReq = { headers: { 'authorization': 'Bearer token123', 'content-type': 'application/json' } }; mockRes = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis() }; }); // verifies should return header value when present test('should return header value when present', () => { const result = getRequiredHeader(mockReq, mockRes, 'authorization', 401, 'Missing auth'); expect(result).toBe('Bearer token123'); // header retrieved in case-insensitive manner expect(mockRes.status).not.toHaveBeenCalled(); }); // verifies should send error response when header is missing test('should send error response when header is missing', () => { const result = getRequiredHeader(mockReq, mockRes, 'x-api-key', 401, 'Missing API key'); expect(result).toBeNull(); // missing header returns null expect(mockRes.status).toHaveBeenCalledWith(401); // sends unauthorized code expect(mockRes.json).toHaveBeenCalledWith({ error: 'Missing API key' }); // payload explains missing key }); // verifies should handle malformed request object test('should handle malformed request object', () => { const malformedReq = {}; const result = getRequiredHeader(malformedReq, mockRes, 'authorization', 401, 'Missing auth'); expect(result).toBeNull(); // missing headers should cause null return expect(mockRes.status).toHaveBeenCalledWith(401); // unauthorized status used for missing header }); // verifies should handle undefined headers test('should handle undefined headers', () => { const reqWithoutHeaders = { headers: undefined }; const result = getRequiredHeader(reqWithoutHeaders, mockRes, 'authorization', 401, 'Missing auth'); expect(result).toBeNull(); // undefined headers result in null expect(mockRes.status).toHaveBeenCalledWith(401); // still unauthorized }); // verifies should retrieve header regardless of case test('should retrieve header regardless of case', () => { const result = getRequiredHeader(mockReq, mockRes, 'Authorization', 401, 'Missing auth'); expect(result).toBe('Bearer token123'); // case-insensitive header check }); // verifies should handle errors during header processing test('should handle errors during header processing', () => { const req = { get headers() { throw new Error('Header access error'); } }; const res = { status: jest.fn().mockReturnThis(), json: jest.fn() }; const result = getRequiredHeader(req, res, 'authorization', 401, 'Missing auth'); expect(result).toBeNull(); // failure should return null expect(res.status).toHaveBeenCalledWith(500); // error status for processing issue expect(res.json).toHaveBeenCalledWith({ error: 'Internal server error' }); // generic error body }); }); describe('HEADERS_TO_REMOVE', () => { // ensures security list remains immutable // verifies should not change when modification is attempted test('should not change when modification is attempted', () => { const original = [...HEADERS_TO_REMOVE]; // capture original list for comparison expect(() => { HEADERS_TO_REMOVE.push('new-header'); }).toThrow(TypeError); // frozen array should throw on push expect(HEADERS_TO_REMOVE).toEqual(original); // array should remain unchanged }); }); });