diff --git a/lib/serverless/api-gateway.js b/lib/serverless/api-gateway.js index 7813ba920a..14746441cb 100644 --- a/lib/serverless/api-gateway.js +++ b/lib/serverless/api-gateway.js @@ -106,38 +106,45 @@ function isLambdaProxyEvent(event) { return isGatewayV1Event(event) || isGatewayV2Event(event) } -function isGatewayV1Event(event) { - let result = false - - if (event?.version === '1.0') { - result = true - } else if ( - typeof event?.path === 'string' && - (event.headers ?? event.multiValueHeaders) && - typeof event?.httpMethod === 'string' - // eslint-disable-next-line sonarjs/no-duplicated-branches - ) { - result = true - } +const v1Keys = [ + 'body', + 'headers', + 'httpMethod', + 'isBase64Encoded', + 'multiValueHeaders', + 'multiValueQueryStringParameters', + 'path', + 'pathParameters', + 'queryStringParameters', + 'requestContext', + 'resource', + 'stageVariables', + 'version' +].join(',') - return result +function isGatewayV1Event(event) { + const keys = Object.keys(event).sort().join(',') + return keys === v1Keys && event?.version === '1.0' } -function isGatewayV2Event(event) { - let result = false - - if (event?.version === '2.0') { - result = true - } else if ( - typeof event?.requestContext?.http?.path === 'string' && - Object.prototype.toString.call(event.headers) === '[object Object]' && - typeof event?.requestContext?.http?.method === 'string' - // eslint-disable-next-line sonarjs/no-duplicated-branches - ) { - result = true - } +const v2Keys = [ + 'body', + 'cookies', + 'headers', + 'isBase64Encoded', + 'pathParameters', + 'queryStringParameters', + 'rawPath', + 'rawQueryString', + 'requestContext', + 'routeKey', + 'stageVariables', + 'version' +].join(',') - return result +function isGatewayV2Event(event) { + const keys = Object.keys(event).sort().join(',') + return keys === v2Keys && event?.version === '2.0' } /** @@ -155,5 +162,7 @@ module.exports = { LambdaProxyWebRequest, LambdaProxyWebResponse, isLambdaProxyEvent, - isValidLambdaProxyResponse + isValidLambdaProxyResponse, + isGatewayV1Event, + isGatewayV2Event } diff --git a/test/unit/serverless/api-gateway-v2.test.js b/test/unit/serverless/api-gateway-v2.test.js index c5045ed4ed..65ff3635e0 100644 --- a/test/unit/serverless/api-gateway-v2.test.js +++ b/test/unit/serverless/api-gateway-v2.test.js @@ -13,73 +13,7 @@ const AwsLambda = require('../../../lib/serverless/aws-lambda') const ATTR_DEST = require('../../../lib/config/attribute-filter').DESTINATIONS -// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html -const v2Event = { - version: '2.0', - routeKey: '$default', - rawPath: '/my/path', - rawQueryString: 'parameter1=value1¶meter1=value2¶meter2=value', - cookies: ['cookie1', 'cookie2'], - headers: { - header1: 'value1', - header2: 'value1,value2', - accept: 'application/json' - }, - queryStringParameters: { - parameter1: 'value1,value2', - parameter2: 'value', - name: 'me', - team: 'node agent' - }, - requestContext: { - accountId: '123456789012', - apiId: 'api-id', - authentication: { - clientCert: { - clientCertPem: 'CERT_CONTENT', - subjectDN: 'www.example.com', - issuerDN: 'Example issuer', - serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1', - validity: { - notBefore: 'May 28 12:30:02 2019 GMT', - notAfter: 'Aug 5 09:36:04 2021 GMT' - } - } - }, - authorizer: { - jwt: { - claims: { - claim1: 'value1', - claim2: 'value2' - }, - scopes: ['scope1', 'scope2'] - } - }, - domainName: 'id.execute-api.us-east-1.amazonaws.com', - domainPrefix: 'id', - http: { - method: 'POST', - path: '/my/path', - protocol: 'HTTP/1.1', - sourceIp: '192.0.2.1', - userAgent: 'agent' - }, - requestId: 'id', - routeKey: '$default', - stage: '$default', - time: '12/Mar/2020:19:03:58 +0000', - timeEpoch: 1583348638390 - }, - body: 'Hello from Lambda', - pathParameters: { - parameter1: 'value1' - }, - isBase64Encoded: false, - stageVariables: { - stageVariable1: 'value1', - stageVariable2: 'value2' - } -} +const { gatewayV2Event: v2Event } = require('./fixtures') tap.beforeEach((t) => { // This env var suppresses console output we don't need to inspect. diff --git a/test/unit/serverless/fixtures.js b/test/unit/serverless/fixtures.js new file mode 100644 index 0000000000..55fbaa81d7 --- /dev/null +++ b/test/unit/serverless/fixtures.js @@ -0,0 +1,181 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html +const gatewayV1Event = { + version: '1.0', + resource: '/my/path', + path: '/my/path', + httpMethod: 'GET', + headers: { + header1: 'value1', + header2: 'value2' + }, + multiValueHeaders: { + header1: ['value1'], + header2: ['value1', 'value2'] + }, + queryStringParameters: { + parameter1: 'value1', + parameter2: 'value' + }, + multiValueQueryStringParameters: { + parameter1: ['value1', 'value2'], + parameter2: ['value'] + }, + requestContext: { + accountId: '123456789012', + apiId: 'id', + authorizer: { + claims: null, + scopes: null + }, + domainName: 'id.execute-api.us-east-1.amazonaws.com', + domainPrefix: 'id', + extendedRequestId: 'request-id', + httpMethod: 'GET', + identity: { + accessKey: null, + accountId: null, + caller: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: '192.0.2.1', + user: null, + userAgent: 'user-agent', + userArn: null, + clientCert: { + clientCertPem: 'CERT_CONTENT', + subjectDN: 'www.example.com', + issuerDN: 'Example issuer', + serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1', + validity: { + notBefore: 'May 28 12:30:02 2019 GMT', + notAfter: 'Aug 5 09:36:04 2021 GMT' + } + } + }, + path: '/my/path', + protocol: 'HTTP/1.1', + requestId: 'id=', + requestTime: '04/Mar/2020:19:15:17 +0000', + requestTimeEpoch: 1583349317135, + resourceId: null, + resourcePath: '/my/path', + stage: '$default' + }, + pathParameters: null, + stageVariables: null, + body: 'Hello from Lambda!', + isBase64Encoded: false +} + +// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html +const gatewayV2Event = { + version: '2.0', + routeKey: '$default', + rawPath: '/my/path', + rawQueryString: 'parameter1=value1¶meter1=value2¶meter2=value', + cookies: ['cookie1', 'cookie2'], + headers: { + header1: 'value1', + header2: 'value1,value2', + accept: 'application/json' + }, + queryStringParameters: { + parameter1: 'value1,value2', + parameter2: 'value', + name: 'me', + team: 'node agent' + }, + requestContext: { + accountId: '123456789012', + apiId: 'api-id', + authentication: { + clientCert: { + clientCertPem: 'CERT_CONTENT', + subjectDN: 'www.example.com', + issuerDN: 'Example issuer', + serialNumber: 'a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1', + validity: { + notBefore: 'May 28 12:30:02 2019 GMT', + notAfter: 'Aug 5 09:36:04 2021 GMT' + } + } + }, + authorizer: { + jwt: { + claims: { + claim1: 'value1', + claim2: 'value2' + }, + scopes: ['scope1', 'scope2'] + } + }, + domainName: 'id.execute-api.us-east-1.amazonaws.com', + domainPrefix: 'id', + http: { + method: 'POST', + path: '/my/path', + protocol: 'HTTP/1.1', + sourceIp: '192.0.2.1', + userAgent: 'agent' + }, + requestId: 'id', + routeKey: '$default', + stage: '$default', + time: '12/Mar/2020:19:03:58 +0000', + timeEpoch: 1583348638390 + }, + body: 'Hello from Lambda', + pathParameters: { + parameter1: 'value1' + }, + isBase64Encoded: false, + stageVariables: { + stageVariable1: 'value1', + stageVariable2: 'value2' + } +} + +// Event used when one Lambda directly invokes another Lambda. +// https://docs.aws.amazon.com/lambda/latest/dg/invocation-async-retain-records.html#invocation-async-destinations +const lambaV1InvocationEvent = { + version: '1.0', + timestamp: '2019-11-14T18:16:05.568Z', + requestContext: { + requestId: 'e4b46cbf-b738-xmpl-8880-a18cdf61200e', + functionArn: 'arn:aws:lambda:us-east-2:123456789012:function:my-function:$LATEST', + condition: 'RetriesExhausted', + approximateInvokeCount: 3 + }, + requestPayload: { + ORDER_IDS: [ + '9e07af03-ce31-4ff3-xmpl-36dce652cb4f', + '637de236-e7b2-464e-xmpl-baf57f86bb53', + 'a81ddca6-2c35-45c7-xmpl-c3a03a31ed15' + ] + }, + responseContext: { + statusCode: 200, + executedVersion: '$LATEST', + functionError: 'Unhandled' + }, + responsePayload: { + errorMessage: + 'RequestId: e4b46cbf-b738-xmpl-8880-a18cdf61200e Process exited before completing request' + } +} + +module.exports = { + gatewayV1Event, + gatewayV2Event, + lambaV1InvocationEvent +} diff --git a/test/unit/serverless/lambda-sample-events.js b/test/unit/serverless/lambda-sample-events.js index db0fc13d2c..bc1469860f 100644 --- a/test/unit/serverless/lambda-sample-events.js +++ b/test/unit/serverless/lambda-sample-events.js @@ -260,7 +260,10 @@ const cloudFormationCreateRequestEvent = { } const apiGatewayProxyEvent = { + version: '1.0', + resource: '/{proxy+}', path: '/test/hello', + httpMethod: 'GET', headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate, lzma, sdch, br', @@ -280,9 +283,12 @@ const apiGatewayProxyEvent = { 'X-Forwarded-Port': '443', 'X-Forwarded-Proto': 'https' }, - pathParameters: { - proxy: 'hello' + multiValueHeaders: null, + queryStringParameters: { + name: 'me', + team: 'node agent' }, + multiValueQueryStringParameters: null, requestContext: { accountId: '123456789012', resourceId: 'us4z18', @@ -305,15 +311,14 @@ const apiGatewayProxyEvent = { httpMethod: 'GET', apiId: 'wt6mne2s9k' }, - resource: '/{proxy+}', - httpMethod: 'GET', - queryStringParameters: { - name: 'me', - team: 'node agent' + pathParameters: { + proxy: 'hello' }, stageVariables: { stageVarName: 'stageVarValue' - } + }, + body: null, + isBase64Encoded: false } const cloudWatchLogsEvent = { @@ -490,17 +495,11 @@ const sesEvent = { } const albEventWithMultiValueParameters = { - requestContext: { - elb: { - targetGroupArn: - 'arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a' - } - }, - httpMethod: 'GET', + version: '1.0', + resource: '/lambda', path: '/lambda', - multiValueQueryStringParameters: { - query: ['1234ABCD', 'other'] - }, + httpMethod: 'GET', + headers: null, multiValueHeaders: { 'accept': [ 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' @@ -523,6 +522,18 @@ const albEventWithMultiValueParameters = { 'cookie-name=cookie-other-value' ] }, + queryStringParameters: null, + multiValueQueryStringParameters: { + query: ['1234ABCD', 'other'] + }, + requestContext: { + elb: { + targetGroupArn: + 'arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a' + } + }, + pathParameters: null, + stageVariables: null, body: '', isBase64Encoded: false } diff --git a/test/unit/serverless/utils.test.js b/test/unit/serverless/utils.test.js new file mode 100644 index 0000000000..40ba30fda2 --- /dev/null +++ b/test/unit/serverless/utils.test.js @@ -0,0 +1,24 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { isGatewayV1Event, isGatewayV2Event } = require('../../../lib/serverless/api-gateway') +const { gatewayV1Event, gatewayV2Event, lambaV1InvocationEvent } = require('./fixtures') + +test('isGatewayV1Event', () => { + assert.equal(isGatewayV1Event(gatewayV1Event), true) + assert.equal(isGatewayV1Event(gatewayV2Event), false) + assert.equal(isGatewayV1Event(lambaV1InvocationEvent), false) +}) + +test('isGatewayV2Event', () => { + assert.equal(isGatewayV2Event(gatewayV1Event), false) + assert.equal(isGatewayV2Event(gatewayV2Event), true) + assert.equal(isGatewayV2Event(lambaV1InvocationEvent), false) +})