diff --git a/docs/providers/aws/guide/functions.md b/docs/providers/aws/guide/functions.md index 0205deeda..76c5c6f3d 100644 --- a/docs/providers/aws/guide/functions.md +++ b/docs/providers/aws/guide/functions.md @@ -200,6 +200,91 @@ provider: See the documentation about [IAM](./iam.md) for function level IAM roles. +## Lambda Function URLs + +A [Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-urls.html) is a simple solution to create HTTP endpoints with AWS Lambda. Function URLs are ideal for getting started with AWS Lambda, or for single-function applications like webhooks or APIs built with web frameworks. + +You can create a function URL via the `url` property in the function configuration in `serverless.yml`. By setting `url` to `true`, as shown below, the URL will be public without CORS configuration. + +```yaml +functions: + func: + handler: index.handler + url: true +``` + +Alternatively, you can configure it as an object with the `authorizer` and/or `cors` properties. The `authorizer` property can be set to `aws_iam` to enable AWS IAM authorization on your function URL. + +```yaml +functions: + func: + handler: index.handler + url: + authorizer: aws_iam +``` + +When using IAM authorization, the URL will only accept HTTP requests with AWS credentials allowing `lambda:InvokeFunctionUrl` (similar to [API Gateway IAM authentication](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control-iam.html)). + +You can also configure [CORS headers](https://developer.mozilla.org/docs/Web/HTTP/CORS) so that your function URL can be called from other domains in browsers. Setting `cors` to `true` will allow all domains via the following CORS headers: + +```yaml +functions: + func: + handler: index.handler + url: + cors: true +``` + +| Header | Value | +| :--------------------------- | :----------------------------------------------------------------------- | +| Access-Control-Allow-Origin | \* | +| Access-Control-Allow-Headers | Content-Type, X-Amz-Date, Authorization, X-Api-Key, X-Amz-Security-Token | +| Access-Control-Allow-Methods | \* | + +You can also additionally adjust your CORS configuration by setting `allowedOrigins`, `allowedHeaders`, `allowedMethods`, `allowCredentials`, `exposedResponseHeaders`, and `maxAge` properties as shown in example below. + +```yaml +functions: + func: + handler: index.handler + url: + cors: + allowedOrigins: + - https://url1.com + - https://url2.com + allowedHeaders: + - Content-Type + - Authorization + allowedMethods: + - GET + allowCredentials: true + exposedResponseHeaders: + - Special-Response-Header + maxAge: 6000 # In seconds +``` + +In the table below you can find how the `cors` properties map to CORS headers + +| Configuration property | CORS Header | +| :--------------------- | :------------------------------- | +| allowedOrigins | Access-Control-Allow-Origin | +| allowedHeaders | Access-Control-Allow-Headers | +| allowedMethods | Access-Control-Allow-Methods | +| allowCredentials | Access-Control-Allow-Credentials | +| exposedResponseHeaders | Access-Control-Expose-Headers | +| maxAge | Access-Control-Max-Age | + +It is also possible to remove the values in CORS configuration that are set by default by setting them to `null` instead. + +```yaml +functions: + func: + handler: index.handler + url: + cors: + allowedHeaders: null +``` + ## Referencing container image as a target Alternatively lambda environment can be configured through docker images. Image published to AWS ECR registry can be referenced as lambda source (check [AWS Lambda – Container Image Support](https://aws.amazon.com/blogs/aws/new-for-aws-lambda-container-image-support/)). In addition, you can also define your own images that will be built locally and uploaded to AWS ECR registry. diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md index 1e79137ae..721caf1fd 100644 --- a/docs/providers/aws/guide/serverless.yml.md +++ b/docs/providers/aws/guide/serverless.yml.md @@ -645,6 +645,21 @@ functions: subnetIds: - subnetId1 - subnetId2 + # Lambda URL definition for this function, optional + # Can be defined as `true` which will create URL without authorizer and cors settings + url: + authorizer: 'aws_iam' # Authorizer used for calls to Lambda URL + cors: # CORS configuration for Lambda URL, can also be defined as `true` with default CORS configuration + allowedOrigins: + - * + allowedHeaders: + - Authorization + allowedMethods: + - GET + allowCredentials: true + exposedResponseHeaders: + - SomeHeader + maxAge: 3600 # Packaging rules specific to this function package: # Directories and files to include in the deployed package diff --git a/lib/plugins/aws/info/display.js b/lib/plugins/aws/info/display.js index e1b347b22..af3b0938e 100644 --- a/lib/plugins/aws/info/display.js +++ b/lib/plugins/aws/info/display.js @@ -88,6 +88,17 @@ module.exports = { outputSectionItems.push(`CloudFront - ${info.cloudFront}`); } + const functionsWithUrls = this.gatheredData.info.functions.filter((fn) => fn.url); + + for (const functionWithUrl of functionsWithUrls) { + if (outputSectionItems.length === 0 && functionsWithUrls.length === 1) { + // In this situation we want to skip displaying function name as there is only one URL in whole service + outputSectionItems.push(functionWithUrl.url); + } else { + outputSectionItems.push(`${functionWithUrl.name}: ${functionWithUrl.url}`); + } + } + if (outputSectionItems.length > 1) { this.serverless.serviceOutputs.set('endpoints', outputSectionItems); } else if (outputSectionItems.length) { diff --git a/lib/plugins/aws/info/get-stack-info.js b/lib/plugins/aws/info/get-stack-info.js index dc1ccbb1c..1ee22b257 100644 --- a/lib/plugins/aws/info/get-stack-info.js +++ b/lib/plugins/aws/info/get-stack-info.js @@ -75,6 +75,13 @@ module.exports = { functionInfo.name = func; functionInfo.deployedName = functionObj.name; functionInfo.artifactSize = functionObj.artifactSize; + const functionUrlOutput = outputs.find( + (output) => + output.OutputKey === this.provider.naming.getLambdaFunctionUrlOutputLogicalId(func) + ); + if (functionUrlOutput) { + functionInfo.url = functionUrlOutput.OutputValue; + } this.gatheredData.info.functions.push(functionInfo); }); diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js index e19b46852..8c06ddcee 100644 --- a/lib/plugins/aws/lib/naming.js +++ b/lib/plugins/aws/lib/naming.js @@ -150,6 +150,9 @@ module.exports = { getLambdaLogicalId(functionName) { return `${this.getNormalizedFunctionName(functionName)}LambdaFunction`; }, + getLambdaFunctionUrlLogicalId(functionName) { + return `${this.getNormalizedFunctionName(functionName)}LambdaFunctionUrl`; + }, getLambdaEventConfigLogicalId(functionName) { return `${this.getNormalizedFunctionName(functionName)}LambdaEvConf`; }, @@ -186,6 +189,9 @@ module.exports = { getLambdaProvisionedConcurrencyAliasName() { return 'provisioned'; }, + getLambdaFunctionUrlOutputLogicalId(functionName) { + return `${this.getNormalizedFunctionName(functionName)}LambdaFunctionUrl`; + }, getLambdaVersionOutputLogicalId(functionName) { return `${this.getLambdaLogicalId(functionName)}QualifiedArn`; }, @@ -516,6 +522,9 @@ module.exports = { getLambdaHttpApiPermissionLogicalId(functionName) { return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionHttpApi`; }, + getLambdaFnUrlPermissionLogicalId(functionName) { + return `${this.getNormalizedFunctionName(functionName)}LambdaPermissionFnUrl`; + }, getLambdaAuthorizerHttpApiPermissionLogicalId(authorizerName) { return `${this.getNormalizedResourceName(authorizerName)}LambdaAuthorizerPermissionHttpApi`; }, diff --git a/lib/plugins/aws/package/compile/functions.js b/lib/plugins/aws/package/compile/functions.js index 505a1266e..0a5c7f187 100644 --- a/lib/plugins/aws/package/compile/functions.js +++ b/lib/plugins/aws/package/compile/functions.js @@ -9,9 +9,22 @@ const path = require('path'); const ServerlessError = require('../../../../serverless-error'); const deepSortObjectByKey = require('../../../../utils/deep-sort-object-by-key'); const getHashForFilePath = require('../lib/get-hash-for-file-path'); +const resolveLambdaTarget = require('../../utils/resolve-lambda-target'); const parseS3URI = require('../../utils/parse-s3-uri'); const { log } = require('@serverless/utils/log'); +const defaultCors = { + allowedOrigins: ['*'], + allowedHeaders: [ + 'Content-Type', + 'X-Amz-Date', + 'Authorization', + 'X-Api-Key', + 'X-Amz-Security-Token', + ], + allowedMethods: ['*'], +}; + class AwsCompileFunctions { constructor(serverless, options) { this.serverless = serverless; @@ -593,9 +606,94 @@ class AwsCompileFunctions { } } + this.compileFunctionUrl(functionName); this.compileFunctionEventInvokeConfig(functionName); } + compileFunctionUrl(functionName) { + const functionObject = this.serverless.service.getFunction(functionName); + const cfTemplate = this.serverless.service.provider.compiledCloudFormationTemplate; + const { url } = functionObject; + + if (!url) return; + + let auth = 'NONE'; + let cors = null; + if (url.authorizer === 'aws_iam') { + auth = 'AWS_IAM'; + } + + if (url.cors) { + cors = Object.assign({}, defaultCors); + + if (url.cors.allowedOrigins) { + cors.allowedOrigins = _.uniq(url.cors.allowedOrigins); + } else if (url.cors.allowedOrigins === null) { + delete cors.allowedOrigins; + } + + if (url.cors.allowedHeaders) { + cors.allowedHeaders = _.uniq(url.cors.allowedHeaders); + } else if (url.cors.allowedHeaders === null) { + delete cors.allowedHeaders; + } + + if (url.cors.allowedMethods) { + cors.allowedMethods = _.uniq(url.cors.allowedMethods); + } else if (url.cors.allowedMethods === null) { + delete cors.allowedMethods; + } + + if (url.cors.allowCredentials) cors.allowCredentials = true; + + if (url.cors.exposedResponseHeaders) { + cors.exposedResponseHeaders = _.uniq(url.cors.exposedResponseHeaders); + } + + cors.maxAge = url.cors.maxAge; + } + + const urlResource = { + Type: 'AWS::Lambda::Url', + Properties: { + AuthType: auth, + TargetFunctionArn: resolveLambdaTarget(functionName, functionObject), + }, + }; + + if (cors) { + urlResource.Properties.Cors = { + AllowCredentials: cors.allowCredentials, + AllowHeaders: cors.allowedHeaders && Array.from(cors.allowedHeaders), + AllowMethods: cors.allowedMethods && Array.from(cors.allowedMethods), + AllowOrigins: cors.allowedOrigins && Array.from(cors.allowedOrigins), + ExposeHeaders: cors.exposedResponseHeaders && Array.from(cors.exposedResponseHeaders), + MaxAge: cors.maxAge, + }; + } + + const logicalId = this.provider.naming.getLambdaFunctionUrlLogicalId(functionName); + cfTemplate.Resources[logicalId] = urlResource; + cfTemplate.Outputs[this.provider.naming.getLambdaFunctionUrlOutputLogicalId(functionName)] = { + Description: 'Lambda Function URL', + Value: { + 'Fn::GetAtt': [logicalId, 'FunctionUrl'], + }, + }; + + if (auth === 'NONE') { + cfTemplate.Resources[this.provider.naming.getLambdaFnUrlPermissionLogicalId(functionName)] = { + Type: 'AWS::Lambda::Permission', + Properties: { + FunctionName: resolveLambdaTarget(functionName, functionObject), + Action: 'lambda:InvokeFunctionUrl', + Principal: '*', + FunctionUrlAuthType: auth, + }, + }; + } + } + compileFunctionEventInvokeConfig(functionName) { const functionObject = this.serverless.service.getFunction(functionName); const { destinations, maximumEventAge, maximumRetryAttempts } = functionObject; diff --git a/lib/plugins/aws/provider.js b/lib/plugins/aws/provider.js index 4620f2f95..1a5ef46b3 100644 --- a/lib/plugins/aws/provider.js +++ b/lib/plugins/aws/provider.js @@ -1332,6 +1332,55 @@ class AwsProvider { tags: { $ref: '#/definitions/awsResourceTags' }, timeout: { $ref: '#/definitions/awsLambdaTimeout' }, tracing: { $ref: '#/definitions/awsLambdaTracing' }, + url: { + anyOf: [ + { type: 'boolean' }, + { + type: 'object', + properties: { + authorizer: { type: 'string', enum: ['aws_iam'] }, + cors: { + anyOf: [ + { type: 'boolean' }, + { + type: 'object', + properties: { + allowCredentials: { type: 'boolean' }, + allowedHeaders: { + type: 'array', + minItems: 1, + maxItems: 100, + items: { type: 'string' }, + }, + allowedMethods: { + type: 'array', + minItems: 1, + maxItems: 6, + items: { type: 'string' }, + }, + allowedOrigins: { + type: 'array', + minItems: 1, + maxItems: 100, + items: { type: 'string' }, + }, + exposedResponseHeaders: { + type: 'array', + minItems: 1, + maxItems: 100, + items: { type: 'string' }, + }, + maxAge: { type: 'integer', minimum: 0 }, + }, + additionalProperties: false, + }, + ], + }, + }, + additionalProperties: false, + }, + ], + }, versionFunction: { $ref: '#/definitions/awsLambdaVersioning' }, vpc: { $ref: '#/definitions/awsLambdaVpcConfig' }, httpApi: { diff --git a/lib/utils/telemetry/generate-payload.js b/lib/utils/telemetry/generate-payload.js index c757fb025..ccef0f5a2 100644 --- a/lib/utils/telemetry/generate-payload.js +++ b/lib/utils/telemetry/generate-payload.js @@ -99,6 +99,7 @@ const getServiceConfig = ({ configuration, options, variableSources }) => { })(); return { + url: Boolean(functionConfig.url), runtime: functionRuntime, events: functionEvents.map((eventConfig) => ({ type: isObject(eventConfig) ? Object.keys(eventConfig)[0] || null : null, diff --git a/package.json b/package.json index ea90b85be..53ce82e47 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@serverless/eslint-config": "^4.0.0", "@serverless/test": "^10.0.2", "adm-zip": "^0.5.9", + "aws4": "^1.11.0", "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "cos-nodejs-sdk-v5": "^2.11.6", diff --git a/test/integration/aws/function-url.test.js b/test/integration/aws/function-url.test.js new file mode 100644 index 000000000..396f7b731 --- /dev/null +++ b/test/integration/aws/function-url.test.js @@ -0,0 +1,113 @@ +'use strict'; + +const { expect } = require('chai'); +const awsRequest = require('@serverless/test/aws-request'); +const CloudFormationService = require('aws-sdk').CloudFormation; +const fixtures = require('../../fixtures/programmatic'); +const aws4 = require('aws4'); +const url = require('url'); + +const { deployService, removeService, fetch } = require('../../utils/integration'); + +describe('test/integration/aws/function-url.test.js', function () { + this.timeout(1000 * 60 * 10); // Involves time-taking deploys + let stackName; + let serviceDir; + let basicEndpoint; + let otherEndpoint; + let authedEndpoint; + const stage = 'dev'; + + before(async () => { + const serviceData = await fixtures.setup('function', { + configExt: { + functions: { + basic: { + url: true, + }, + other: { + url: { + cors: { + exposedResponseHeaders: ['x-foo'], + allowCredentials: true, + allowedMethods: ['GET'], + }, + }, + }, + authed: { + handler: 'basic.handler', + url: { + authorizer: 'aws_iam', + }, + }, + }, + }, + }); + ({ servicePath: serviceDir } = serviceData); + const serviceName = serviceData.serviceConfig.service; + stackName = `${serviceName}-${stage}`; + await deployService(serviceDir); + const describeStacksResponse = await awsRequest(CloudFormationService, 'describeStacks', { + StackName: stackName, + }); + basicEndpoint = describeStacksResponse.Stacks[0].Outputs.find( + (output) => output.OutputKey === 'BasicLambdaFunctionUrl' + ).OutputValue; + otherEndpoint = describeStacksResponse.Stacks[0].Outputs.find( + (output) => output.OutputKey === 'OtherLambdaFunctionUrl' + ).OutputValue; + authedEndpoint = describeStacksResponse.Stacks[0].Outputs.find( + (output) => output.OutputKey === 'AuthedLambdaFunctionUrl' + ).OutputValue; + }); + + after(async () => { + if (!serviceDir) return; + await removeService(serviceDir); + }); + + it('should return valid response from Lambda URL', async () => { + const expectedMessage = 'Basic'; + + const response = await fetch(basicEndpoint, { method: 'GET' }); + const jsonResponse = await response.json(); + expect(jsonResponse.message).to.equal(expectedMessage); + }); + + it('should return valid response from Lambda URL with authorizer with valid signature', async () => { + const expectedMessage = 'Basic'; + const signedParams = aws4.sign( + { + service: 'lambda', + region: 'us-east-1', + method: 'GET', + host: url.parse(authedEndpoint).hostname, + }, + { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + } + ); + + const response = await fetch(authedEndpoint, signedParams); + const jsonResponse = await response.json(); + expect(jsonResponse.message).to.equal(expectedMessage); + }); + + it('should return invalid response from Lambda URL with authorizer without passed signature', async () => { + const expectedMessage = 'Forbidden'; + const response = await fetch(authedEndpoint, { method: 'GET' }); + const jsonResponse = await response.json(); + expect(jsonResponse.Message).to.equal(expectedMessage); + }); + + it('should return expected CORS headers from Lambda URL', async () => { + const response = await fetch(otherEndpoint, { + method: 'GET', + headers: { Origin: 'https://serverless.com' }, + }); + const headers = response.headers; + expect(headers.get('access-control-expose-headers')).to.equal('x-foo'); + expect(headers.get('access-control-allow-credentials')).to.equal('true'); + }); +}); diff --git a/test/unit/lib/plugins/aws/info/display.test.js b/test/unit/lib/plugins/aws/info/display.test.js index 8e42db590..11da62e69 100644 --- a/test/unit/lib/plugins/aws/info/display.test.js +++ b/test/unit/lib/plugins/aws/info/display.test.js @@ -43,6 +43,10 @@ describe('test/unit/lib/plugins/aws/info/display.test.js', () => { OutputKey: 'LayerLambdaLayerQualifiedArn', OutputValue: 'arn:aws:lambda:us-east-1:00000000:layer:layer:1', }, + { + OutputKey: 'WithUrlLambdaFunctionUrl', + OutputValue: 'https://sub.lambda.com', + }, ], }, ], @@ -68,6 +72,12 @@ describe('test/unit/lib/plugins/aws/info/display.test.js', () => { ], }, }, + functions: { + withUrl: { + handler: 'index.handler', + url: true, + }, + }, layers: { layer: { path: 'layer', @@ -90,6 +100,7 @@ describe('test/unit/lib/plugins/aws/info/display.test.js', () => { 'GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/foo', 'POST - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/some-post', 'GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/bar/{marko}', + 'withUrl: https://sub.lambda.com', ]); }); it('should register functions section', () => { @@ -97,6 +108,7 @@ describe('test/unit/lib/plugins/aws/info/display.test.js', () => { `minimal: ${serviceName}-dev-minimal`, `foo: ${serviceName}-dev-foo`, `other: ${serviceName}-dev-other`, + `withUrl: ${serviceName}-dev-withUrl`, ]); }); it('should register layers section', () => { diff --git a/test/unit/lib/plugins/aws/lib/naming.test.js b/test/unit/lib/plugins/aws/lib/naming.test.js index f72e3a433..a3574fc83 100644 --- a/test/unit/lib/plugins/aws/lib/naming.test.js +++ b/test/unit/lib/plugins/aws/lib/naming.test.js @@ -1044,6 +1044,14 @@ describe('#naming()', () => { }); }); + describe('#getLambdaFnUrlPermissionLogicalId()', () => { + it('should normalize the name and append correct suffix', () => { + expect(sdk.naming.getLambdaFnUrlPermissionLogicalId('fnName')).to.equal( + 'FnNameLambdaPermissionFnUrl' + ); + }); + }); + describe('#getHttpApiName()', () => { it('should return the composition of service & stage name if custom name not provided and shouldStartNameWithService is true', () => { serverless.service.service = 'myService'; diff --git a/test/unit/lib/plugins/aws/package/compile/functions.test.js b/test/unit/lib/plugins/aws/package/compile/functions.test.js index ff21dd5c7..4d509be04 100644 --- a/test/unit/lib/plugins/aws/package/compile/functions.test.js +++ b/test/unit/lib/plugins/aws/package/compile/functions.test.js @@ -1577,6 +1577,7 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { describe('Function properties', () => { let cfResources; + let cfOutputs; let naming; let serverless; let serviceConfig; @@ -1689,6 +1690,28 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { handler: 'index.handler', ephemeralStorageSize: 1024, }, + fnUrl: { + handler: 'target.handler', + url: true, + }, + fnUrlWithAuthAndCors: { + handler: 'target.handler', + url: { + authorizer: 'aws_iam', + cors: { + maxAge: 3600, + }, + }, + }, + fnUrlNullifyDefaultCorsValue: { + handler: 'target.handler', + url: { + authorizer: 'aws_iam', + cors: { + allowedHeaders: null, + }, + }, + }, }, resources: { Resources: { @@ -1708,6 +1731,7 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { }, }); cfResources = cfTemplate.Resources; + cfOutputs = cfTemplate.Outputs; naming = awsNaming; serverless = serverlessInstance; serviceConfig = fixtureData.serviceConfig; @@ -1888,6 +1912,89 @@ describe('lib/plugins/aws/package/compile/functions/index.test.js', () => { // https://github.com/serverless/serverless/blob/d8527d8b57e7e5f0b94ba704d9f53adb34298d99/lib/plugins/aws/package/compile/functions/index.test.js#L2381-L2397 }); + it('should support `functions[].url` set to `true`', () => { + expect(cfResources[naming.getLambdaFunctionUrlLogicalId('fnUrl')].Properties).to.deep.equal({ + AuthType: 'NONE', + TargetFunctionArn: { + 'Fn::GetAtt': [naming.getLambdaLogicalId('fnUrl'), 'Arn'], + }, + }); + expect(cfOutputs[naming.getLambdaFunctionUrlLogicalId('fnUrl')].Value).to.deep.equal({ + 'Fn::GetAtt': [naming.getLambdaFunctionUrlOutputLogicalId('fnUrl'), 'FunctionUrl'], + }); + + expect( + cfResources[naming.getLambdaFnUrlPermissionLogicalId('fnUrl')].Properties + ).to.deep.equal({ + Action: 'lambda:InvokeFunctionUrl', + FunctionName: { + 'Fn::GetAtt': ['FnUrlLambdaFunction', 'Arn'], + }, + FunctionUrlAuthType: 'NONE', + Principal: '*', + }); + }); + + it('should support `functions[].url` set to an object with authorizer and cors', () => { + expect( + cfResources[naming.getLambdaFunctionUrlLogicalId('fnUrlWithAuthAndCors')].Properties + ).to.deep.equal({ + AuthType: 'AWS_IAM', + TargetFunctionArn: { + 'Fn::GetAtt': [naming.getLambdaLogicalId('fnUrlWithAuthAndCors'), 'Arn'], + }, + Cors: { + AllowMethods: ['*'], + AllowOrigins: ['*'], + AllowHeaders: [ + 'Content-Type', + 'X-Amz-Date', + 'Authorization', + 'X-Api-Key', + 'X-Amz-Security-Token', + ], + MaxAge: 3600, + AllowCredentials: undefined, + ExposeHeaders: undefined, + }, + }); + expect( + cfOutputs[naming.getLambdaFunctionUrlLogicalId('fnUrlWithAuthAndCors')].Value + ).to.deep.equal({ + 'Fn::GetAtt': [ + naming.getLambdaFunctionUrlOutputLogicalId('fnUrlWithAuthAndCors'), + 'FunctionUrl', + ], + }); + }); + + it('should support nullifying default cors value with `null` for `functions[].url`', () => { + expect( + cfResources[naming.getLambdaFunctionUrlLogicalId('fnUrlNullifyDefaultCorsValue')].Properties + ).to.deep.equal({ + AuthType: 'AWS_IAM', + TargetFunctionArn: { + 'Fn::GetAtt': [naming.getLambdaLogicalId('fnUrlNullifyDefaultCorsValue'), 'Arn'], + }, + Cors: { + AllowMethods: ['*'], + AllowOrigins: ['*'], + AllowHeaders: undefined, + MaxAge: undefined, + AllowCredentials: undefined, + ExposeHeaders: undefined, + }, + }); + expect( + cfOutputs[naming.getLambdaFunctionUrlLogicalId('fnUrlNullifyDefaultCorsValue')].Value + ).to.deep.equal({ + 'Fn::GetAtt': [ + naming.getLambdaFunctionUrlOutputLogicalId('fnUrlNullifyDefaultCorsValue'), + 'FunctionUrl', + ], + }); + }); + it('should support `functions[].architecture`', () => { expect( cfResources[naming.getLambdaLogicalId('fnArch')].Properties.Architectures diff --git a/test/unit/lib/utils/telemetry/generate-payload.test.js b/test/unit/lib/utils/telemetry/generate-payload.test.js index 803402291..31825f639 100644 --- a/test/unit/lib/utils/telemetry/generate-payload.test.js +++ b/test/unit/lib/utils/telemetry/generate-payload.test.js @@ -53,6 +53,10 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => { image: '000000000000.dkr.ecr.sa-east-1.amazonaws.com/test-lambda-docker@sha256:6bb600b4d6e1d7cf521097177dd0c4e9ea373edb91984a505333be8ac9455d38', }, + withUrl: { + handler: 'index.handler', + url: true, + }, }, resources: { Resources: { @@ -148,11 +152,12 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => { }, plugins: [], functions: [ - { runtime: 'nodejs14.x', events: [{ type: 'httpApi' }, { type: 'httpApi' }] }, - { runtime: 'nodejs14.x', events: [{ type: 'httpApi' }] }, - { runtime: 'nodejs14.x', events: [] }, - { runtime: 'nodejs14.x', events: [] }, - { runtime: '$containerimage', events: [] }, + { runtime: 'nodejs14.x', events: [{ type: 'httpApi' }, { type: 'httpApi' }], url: false }, + { runtime: 'nodejs14.x', events: [{ type: 'httpApi' }], url: false }, + { runtime: 'nodejs14.x', events: [], url: false }, + { runtime: 'nodejs14.x', events: [], url: false }, + { runtime: '$containerimage', events: [], url: false }, + { runtime: 'nodejs14.x', events: [], url: true }, ], resources: { general: ['AWS::Logs::LogGroup', 'AWS::S3::Bucket', 'Custom'], @@ -217,8 +222,8 @@ describe('test/unit/lib/utils/telemetry/generatePayload.test.js', () => { }, plugins: ['./custom-provider'], functions: [ - { runtime: 'foo', events: [{ type: 'someEvent' }] }, - { runtime: 'bar', events: [] }, + { runtime: 'foo', events: [{ type: 'someEvent' }], url: false }, + { runtime: 'bar', events: [], url: false }, ], resources: undefined, variableSources: [],