import { jest, expect as jestExpect } from '@jest/globals' import { APIGatewayEvent, Context } from 'aws-lambda' import { expect } from 'chai' import Request, { HttpMethod } from '../../src/API/Request.js' import Logger from '../../src/Logger/Logger.js' function newReq(event: any, context?: any) { const logger = new Logger( { logLevel: 'DEBUG', }, '123-456' ) return new Request( (event), context ? context : {}, logger ) } describe('Request querystring', () => { test('Null query string', () => { const r = newReq({ queryStringParameters: null, }) expect(r.containsQueryParam('123')).to.be.false expect(r.getQueryParam('123')).to.be.null }) test('Empty query string', () => { const r = newReq({ queryStringParameters: {}, }) expect(r.containsQueryParam('123')).to.be.false expect(r.getQueryParam('123')).to.be.null }) test('Valid query string', () => { const v = { '123': 'abc' } const r = newReq({ queryStringParameters: v, }) expect(r.containsQueryParam('123')).to.be.true expect(r.getQueryParam('123')).to.be.equals('abc') expect(r.getQueryParams()).to.be.deep.equal(v) }) test('Valid query string number', () => { const r = newReq({ queryStringParameters: { '123': 456 }, }) expect(r.containsQueryParam('123')).to.be.true expect(r.getQueryParam('123')).to.be.equals(456) }) }) describe('Request headers', () => { test('Null headers', () => { const r = newReq({ headers: null }) expect(r.getHeader('123')).to.be.null }) test('Empty headers', () => { const r = newReq({ headers: {} }) expect(r.getHeader('123')).to.be.null }) test('Valid headers', () => { const r = newReq({ headers: { '123': 'abc' } }) expect(r.getHeader('123')).to.be.equals('abc') }) test('Auth headers', () => { const r = newReq({ headers: { Authorization: 'abc' } }) expect(r.getAuthorizationHeader()).to.be.equals('abc') }) }) describe('Request context', () => { test('Null context', () => { const r = newReq({ requestContext: null }) expect(r.getContextParam('123')).to.be.null }) test('Empty context', () => { const r = newReq({ requestContext: {} }) expect(r.getContextParam('123')).to.be.null }) test('Valid context', () => { const r = newReq({ requestContext: { '123': 'abc' }, }) expect(r.getContextParam('123')).to.be.equals('abc') }) }) describe('Request path params', () => { test('Null path params', () => { const r = newReq({ pathParameters: null }) expect(r.containsPathParam('123')).to.be.false expect(r.getPathParam('123')).to.be.null expect(r.getPathParams()).to.be.null }) test('Empty path params', () => { const r = newReq({ pathParameters: {} }) expect(r.containsPathParam('123')).to.be.false expect(r.getPathParam('123')).to.be.null expect(r.getPathParams()).to.be.deep.equal({}) }) test('Valid path params', () => { const v = { '123': 'abc' } const r = newReq({ pathParameters: v, }) expect(r.containsPathParam('123')).to.be.true expect(r.getPathParam('123')).to.be.equals('abc') expect(r.getPathParams()).to.be.deep.equal(v) }) test('Valid path param number', () => { const v = { '123': 456 } const r = newReq({ pathParameters: v, }) expect(r.containsPathParam('123')).to.be.true expect(r.getPathParam('123')).to.be.equals(456) expect(r.getPathParams()).to.be.deep.equal(v) }) test('Fix path params', () => { const v = { '123': 'abc' } const r = newReq({ pathParameters: null }) r.setFixedPathParams( Object.keys(v).map(k => ({ name: k })), ['/'].concat(Object.values(v)) ) expect(r.containsPathParam('123')).to.be.true expect(r.getPathParam('123')).to.be.equals('abc') expect(r.getPathParams()).to.be.deep.equal(v) }) }) describe('Request body', () => { test('Null body', () => { const r = newReq({ body: null }) expect(r.getBody()).to.be.null }) test('Empty body', () => { const r = newReq({ body: {} }) expect(r.getBody()).to.be.deep.equals({}) }) test('Empty string body', () => { const r = newReq({ body: '{}' }) expect(r.getBody()).to.be.deep.equals({}) }) test('Broken json string body', () => { const r = newReq({ body: '{name": "Joe"}' }) expect(r.getBody()).to.be.equals('{name": "Joe"}') }) test('Valid string body', () => { const r = newReq({ body: '{"name": "Joe"}' }) expect(r.getBody()).to.be.deep.equals({ name: 'Joe', }) }) test('Valid json body', () => { const r = newReq({ body: { name: 'Joe' } }) expect(r.getBody()).to.be.deep.equals({ name: 'Joe', }) }) }) describe('Request health check path detection', () => { test('/ is NOT treated as a health check path', () => { const r = newReq({ path: '/' }) expect(r.isHealthCheckPath()).to.be.false }) test('/health is a health check path', () => { const r = newReq({ path: '/health' }) expect(r.isHealthCheckPath()).to.be.true }) test('/ping is a health check path', () => { const r = newReq({ path: '/ping' }) expect(r.isHealthCheckPath()).to.be.true }) test('/api/users is NOT a health check path', () => { const r = newReq({ path: '/api/users' }) expect(r.isHealthCheckPath()).to.be.false }) }) describe('Request body logging', () => { test('Object body logs full object, not just keys summary', () => { const logger = new Logger({ logLevel: 'DEBUG' }, '123-456') const infoSpy = jest.spyOn(logger, 'info') new Request( { path: '/api/users', body: { name: 'Joe', role: 'admin' } }, {}, logger ) jestExpect(infoSpy).toHaveBeenCalledWith( 'Request:', jestExpect.objectContaining({ body: jestExpect.objectContaining({ name: 'Joe', role: 'admin' }), }) ) infoSpy.mockRestore() }) test('Object body with sensitive keys are redacted by logger', () => { const logger = new Logger({ logLevel: 'DEBUG', sensitiveFilteringKeywords: true }, '123-456') const infoSpy = jest.spyOn(logger, 'info') new Request( { path: '/api/users', body: { name: 'Joe', password: 'secret' } }, {}, logger ) jestExpect(infoSpy).toHaveBeenCalledWith( 'Request:', jestExpect.objectContaining({ body: jestExpect.objectContaining({ name: 'Joe', password: 'secret' }), }) ) infoSpy.mockRestore() }) }) describe('Request path/method', () => { test('Null path/method', () => { const r = newReq({ path: null, httpMethod: null, }) expect(r.getPath()).to.be.null expect(r.getMethod.bind(r)).to.throw('Invalid HTTP method: null') }) test('Empty path/method', () => { const r = newReq({ path: '', httpMethod: '', }) expect(r.getPath()).to.be.a('string') expect(r.getPath()).to.be.equals('') expect(r.getMethod.bind(r)).to.throw('Invalid HTTP method: ') }) test('Valid method/path', () => { const r = newReq({ path: '/root', httpMethod: HttpMethod.GET, }) expect(r.getPath()).to.be.a('string') expect(r.getPath()).to.be.equals('/root') expect(r.getMethod()).to.be.a('string') expect(r.getMethod()).to.be.equals(HttpMethod.GET) }) test('Case insensitive method', () => { const r = newReq({ path: '/root', httpMethod: 'poST', }) expect(r.getMethod()).to.be.equals(HttpMethod.POST) }) }) describe('Request ID', () => { test('From context', () => { const r = newReq({}, { awsRequestId: '123' }) expect(r.getRequestID()).to.not.be.null expect(r.getRequestID()).to.be.equals('123') }) test('From event', () => { const r = newReq({ requestContext: { requestId: '123' }, }) expect(r.getRequestID()).to.not.be.null expect(r.getRequestID()).to.be.equals('123') }) test('unknown', () => { const r = newReq({}) expect(r.getRequestID()).to.not.be.null expect(r.getRequestID()).to.be.equals('unknown') }) }) describe('Request origin IP', () => { test('From context', () => { const r = newReq({ requestContext: { identity: { sourceIp: '127.0.0.1' }, }, }) expect(r.getOriginIP()).to.not.be.null expect(r.getOriginIP()).to.be.equals('127.0.0.1') }) test('From header', () => { const r = newReq({ headers: { 'X-Forwarded-For': '127.0.0.1', }, }) expect(r.getOriginIP()).to.not.be.null expect(r.getOriginIP()).to.be.equals('127.0.0.1') }) test('unknown', () => { const r = newReq({}) expect(r.getOriginIP()).to.not.be.null expect(r.getOriginIP()).to.be.equals('unknown') }) }) export {}