UNPKG

serverless

Version:

Serverless Framework - Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more

686 lines (640 loc) • 24.5 kB
'use strict'; const chai = require('chai'); const runServerless = require('../../../../../../../tests/utils/run-serverless'); const fixtures = require('../../../../../../../tests/fixtures'); chai.use(require('chai-as-promised')); const { expect } = chai; describe('HttpApiEvents', () => { after(fixtures.cleanup); it('Should not configure HTTP when events are not configured', () => runServerless({ config: { service: 'irrelevant', provider: 'aws' }, cliArgs: ['package'], }).then(({ serverless }) => { const cfResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; const naming = serverless.getProvider('aws').naming; expect(cfResources[naming.getHttpApiLogicalId()]).to.equal(); expect(cfResources[naming.getHttpApiStageLogicalId()]).to.equal(); })); describe('Specific endpoints', () => { let cfResources; let cfOutputs; let naming; before(() => runServerless({ cwd: fixtures.map.httpApi, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { ({ Resources: cfResources, Outputs: cfOutputs } = cfTemplate); naming = awsNaming; }) ); it('Should configure API resource', () => { const resource = cfResources[naming.getHttpApiLogicalId()]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Api'); expect(resource.Properties).to.have.property('Name'); expect(resource.Properties.ProtocolType).to.equal('HTTP'); }); it('Should not configure default route', () => { const resource = cfResources[naming.getHttpApiLogicalId()]; expect(resource.Properties).to.not.have.property('RouteKey'); expect(resource.Properties).to.not.have.property('Target'); }); it('Should not configure cors when not asked to', () => { const resource = cfResources[naming.getHttpApiLogicalId()]; expect(resource.Properties).to.not.have.property('CorsConfiguration'); }); it('Should not configure logs when not asked to', () => { expect(cfResources).to.not.have.property(naming.getHttpApiLogGroupLogicalId()); }); it('Should configure stage resource', () => { const resource = cfResources[naming.getHttpApiStageLogicalId()]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Stage'); expect(resource.Properties.StageName).to.equal('$default'); expect(resource.Properties.AutoDeploy).to.equal(true); }); it('Should configure output', () => { const output = cfOutputs.HttpApiUrl; expect(output).to.have.property('Value'); }); it('Should configure endpoint', () => { const routeKey = 'POST /some-post'; const resource = cfResources[naming.getHttpApiRouteLogicalId(routeKey)]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Route'); expect(resource.Properties.RouteKey).to.equal(routeKey); }); it('Should configure endpoint integration', () => { const resource = cfResources[naming.getHttpApiIntegrationLogicalId('foo')]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Integration'); expect(resource.Properties.IntegrationType).to.equal('AWS_PROXY'); expect(resource.Properties.PayloadFormatVersion).to.equal('1.0'); }); it('Should ensure higher timeout than function', () => { const resource = cfResources[naming.getHttpApiIntegrationLogicalId('foo')]; expect(resource.Properties.TimeoutInMillis).to.equal(6500); }); it('Should configure lambda permissions', () => { const resource = cfResources[naming.getLambdaHttpApiPermissionLogicalId('foo')]; expect(resource.Type).to.equal('AWS::Lambda::Permission'); expect(resource.Properties.Action).to.equal('lambda:InvokeFunction'); }); }); describe('Custom API name', () => { let cfHttpApi; before(() => fixtures .extend('httpApi', { provider: { httpApi: { name: 'TestHttpApi' } } }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { const { Resources } = cfTemplate; cfHttpApi = Resources[awsNaming.getHttpApiLogicalId()]; }) ) ); it('Should configure API name', () => { expect(cfHttpApi.Properties.Name).to.equal('TestHttpApi'); }); }); describe('Payload format version', () => { let cfHttpApiIntegration; before(() => fixtures.extend('httpApi', { provider: { httpApi: { payload: '2.0' } } }).then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate: { Resources } }) => { cfHttpApiIntegration = Resources[awsNaming.getHttpApiIntegrationLogicalId('foo')]; }) ) ); it('Should set payload format version', () => { expect(cfHttpApiIntegration.Type).to.equal('AWS::ApiGatewayV2::Integration'); expect(cfHttpApiIntegration.Properties.IntegrationType).to.equal('AWS_PROXY'); expect(cfHttpApiIntegration.Properties.PayloadFormatVersion).to.equal('2.0'); }); }); describe('Catch-all endpoints', () => { let cfResources; let cfOutputs; let naming; before(() => runServerless({ cwd: fixtures.map.httpApiCatchAll, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { ({ Resources: cfResources, Outputs: cfOutputs } = cfTemplate); naming = awsNaming; }) ); it('Should configure API resource', () => { const resource = cfResources[naming.getHttpApiLogicalId()]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Api'); expect(resource.Properties).to.have.property('Name'); expect(resource.Properties.ProtocolType).to.equal('HTTP'); }); it('Should not configure default route', () => { const resource = cfResources[naming.getHttpApiLogicalId()]; expect(resource.Properties).to.not.have.property('RouteKey'); expect(resource.Properties).to.not.have.property('Target'); }); it('Should configure default stage resource', () => { const resource = cfResources[naming.getHttpApiStageLogicalId()]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Stage'); expect(resource.Properties.StageName).to.equal('$default'); expect(resource.Properties.AutoDeploy).to.equal(true); }); it('Should configure output', () => { const output = cfOutputs.HttpApiUrl; expect(output).to.have.property('Value'); }); it('Should configure catch all endpoint', () => { const routeKey = '*'; const resource = cfResources[naming.getHttpApiRouteLogicalId(routeKey)]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Route'); expect(resource.Properties.RouteKey).to.equal('$default'); }); it('Should configure method catch all endpoint', () => { const routeKey = 'ANY /foo'; const resource = cfResources[naming.getHttpApiRouteLogicalId(routeKey)]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Route'); expect(resource.Properties.RouteKey).to.equal(routeKey); }); it('Should configure endpoint integration', () => { const resource = cfResources[naming.getHttpApiIntegrationLogicalId('other')]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Integration'); expect(resource.Properties.IntegrationType).to.equal('AWS_PROXY'); }); it('Should configure lambda permissions for global catch all target', () => { const resource = cfResources[naming.getLambdaHttpApiPermissionLogicalId('foo')]; expect(resource.Type).to.equal('AWS::Lambda::Permission'); expect(resource.Properties.Action).to.equal('lambda:InvokeFunction'); }); it('Should configure lambda permissions for path catch all target', () => { const resource = cfResources[naming.getLambdaHttpApiPermissionLogicalId('other')]; expect(resource.Type).to.equal('AWS::Lambda::Permission'); expect(resource.Properties.Action).to.equal('lambda:InvokeFunction'); }); }); describe('Cors', () => { let cfCors; describe('`true` configuration', () => { before(() => fixtures.extend('httpApi', { provider: { httpApi: { cors: true } } }).then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { cfCors = cfTemplate.Resources[awsNaming.getHttpApiLogicalId()].Properties.CorsConfiguration; }) ) ); it('Should not set AllowCredentials', () => expect(cfCors.AllowCredentials).to.equal()); it('Should include default set of headers at AllowHeaders', () => expect(cfCors.AllowHeaders).to.include('Content-Type')); it('Should include "OPTIONS" method at AllowMethods', () => expect(cfCors.AllowMethods).to.include('OPTIONS')); it('Should include used method at AllowMethods', () => { expect(cfCors.AllowMethods).to.include('GET'); expect(cfCors.AllowMethods).to.include('POST'); }); it('Should not include not used method at AllowMethods', () => { expect(cfCors.AllowMethods).to.not.include('PATCH'); expect(cfCors.AllowMethods).to.not.include('DELETE'); }); it('Should allow all origins at AllowOrigins', () => expect(cfCors.AllowOrigins).to.include('*')); it('Should not set ExposeHeaders', () => expect(cfCors.ExposeHeaders).to.equal()); it('Should not set MaxAge', () => expect(cfCors.MaxAge).to.equal()); }); describe('Object configuration #1', () => { before(() => fixtures .extend('httpApi', { provider: { httpApi: { cors: { allowedOrigins: 'https://serverless.com', exposedResponseHeaders: ['Content-Length', 'X-Kuma-Revision'], }, }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { cfCors = cfTemplate.Resources[awsNaming.getHttpApiLogicalId()].Properties.CorsConfiguration; }) ) ); it('Should not set AllowCredentials', () => expect(cfCors.AllowCredentials).to.equal()); it('Should include default set of headers at AllowHeaders', () => expect(cfCors.AllowHeaders).to.include('Content-Type')); it('Should include "OPTIONS" method at AllowMethods', () => expect(cfCors.AllowMethods).to.include('OPTIONS')); it('Should include used method at AllowMethods', () => { expect(cfCors.AllowMethods).to.include('GET'); expect(cfCors.AllowMethods).to.include('POST'); }); it('Should not include not used method at AllowMethods', () => { expect(cfCors.AllowMethods).to.not.include('PATCH'); expect(cfCors.AllowMethods).to.not.include('DELETE'); }); it('Should respect allowedOrigins', () => expect(cfCors.AllowOrigins).to.deep.equal(['https://serverless.com'])); it('Should respect exposedResponseHeaders', () => expect(cfCors.ExposeHeaders).to.deep.equal(['Content-Length', 'X-Kuma-Revision'])); it('Should not set MaxAge', () => expect(cfCors.MaxAge).to.equal()); }); describe('Object configuration #2', () => { before(() => fixtures .extend('httpApi', { provider: { httpApi: { cors: { allowCredentials: true, allowedHeaders: ['Authorization'], allowedMethods: ['GET'], maxAge: 300, }, }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { cfCors = cfTemplate.Resources[awsNaming.getHttpApiLogicalId()].Properties.CorsConfiguration; }) ) ); it('Should respect allowCredentials', () => expect(cfCors.AllowCredentials).to.equal(true)); it('Should respect allowedHeaders', () => expect(cfCors.AllowHeaders).to.deep.equal(['Authorization'])); it('Should respect allowedMethods', () => expect(cfCors.AllowMethods).to.deep.equal(['GET'])); it('Should fallback AllowOrigins to default', () => expect(cfCors.AllowOrigins).to.include('*')); it('Should not set ExposeHeaders', () => expect(cfCors.ExposeHeaders).to.equal()); it('Should respect maxAge', () => expect(cfCors.MaxAge).to.equal(300)); }); describe('With a catch-all route', () => { before(() => fixtures .extend('httpApiCatchAll', { provider: { httpApi: { cors: true, }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { cfCors = cfTemplate.Resources[awsNaming.getHttpApiLogicalId()].Properties.CorsConfiguration; }) ) ); it('Should respect all allowedMethods', () => expect(cfCors.AllowMethods.sort()).to.deep.equal( ['GET', 'POST', 'PUT', 'PATCH', 'OPTIONS', 'HEAD', 'DELETE'].sort() )); }); }); describe('Authorizers: JWT', () => { let cfResources; let naming; before(() => fixtures .extend('httpApi', { provider: { httpApi: { authorizers: { someAuthorizer: { identitySource: '$request.header.Authorization', issuerUrl: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx', audience: 'audiencexxx', }, }, }, }, functions: { foo: { events: [ { httpApi: { authorizer: { name: 'someAuthorizer', scopes: 'foo', }, }, }, ], }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { cfResources = cfTemplate.Resources; naming = awsNaming; }) ) ); it('Should configure authorizer resource', () => { expect(cfResources[naming.getHttpApiAuthorizerLogicalId('someAuthorizer')]).to.deep.equal({ Type: 'AWS::ApiGatewayV2::Authorizer', Properties: { ApiId: { Ref: naming.getHttpApiLogicalId() }, AuthorizerType: 'JWT', IdentitySource: ['$request.header.Authorization'], JwtConfiguration: { Audience: ['audiencexxx'], Issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx', }, Name: 'someAuthorizer', }, }); }); it('Should setup authorizer properties on an endpoint', () => { const routeResourceProps = cfResources[naming.getHttpApiRouteLogicalId('GET /foo')].Properties; expect(routeResourceProps.AuthorizationType).to.equal('JWT'); expect(routeResourceProps.AuthorizationScopes).to.deep.equal(['foo']); }); }); describe('External authorizers: JWT', () => { const apiId = 'external-http-api'; describe('correct configuration', () => { let cfResources; let naming; before(() => fixtures .extend('httpApi', { provider: { httpApi: { id: apiId } }, functions: { foo: { events: [ { httpApi: { authorizer: { id: 'externalAuthorizer', scopes: 'foo', }, }, }, ], }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { cfResources = cfTemplate.Resources; naming = awsNaming; }) ) ); it('Should setup authorizer properties on an endpoint', () => { const routeResourceProps = cfResources[naming.getHttpApiRouteLogicalId('GET /foo')].Properties; expect(routeResourceProps.AuthorizerId).to.equal('externalAuthorizer'); expect(routeResourceProps.AuthorizationType).to.equal('JWT'); expect(routeResourceProps.AuthorizationScopes).to.deep.equal(['foo']); }); }); describe('disallowed configurations', () => { it('Should not allow external authorizer without external httpApi', () => { return expect( fixtures .extend('httpApi', { functions: { foo: { events: [ { httpApi: { authorizer: { id: 'externalAuthorizer', scopes: 'foo', }, }, }, ], }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }) ) ).to.eventually.be.rejected.and.have.property( 'code', 'EXTERNAL_HTTP_API_AUTHORIZER_WITHOUT_EXTERNAL_HTTP_API' ); }); }); }); describe('Access logs', () => { let cfResources; let naming; before(() => fixtures .extend('httpApi', { provider: { logs: { httpApi: true, }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { cfResources = cfTemplate.Resources; naming = awsNaming; }) ) ); it('Should configure log group resource', () => { const resource = cfResources[naming.getHttpApiLogGroupLogicalId()]; expect(resource.Type).to.equal('AWS::Logs::LogGroup'); expect(resource.Properties).to.have.property('LogGroupName'); expect(resource.Properties).to.have.property('RetentionInDays'); }); it('Should setup logs format on stage', () => { const stageResourceProps = cfResources[naming.getHttpApiStageLogicalId()].Properties; expect(stageResourceProps.AccessLogSettings).to.have.property('Format'); }); }); describe('External HTTP API', () => { let cfResources; let cfOutputs; let naming; const apiId = 'external-api-id'; describe('correct configuration', () => { before(() => fixtures.extend('httpApi', { provider: { httpApi: { id: apiId } } }).then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { ({ Resources: cfResources, Outputs: cfOutputs } = cfTemplate); naming = awsNaming; }) ) ); it('Should not configure API resource', () => { expect(cfResources).to.not.have.property(naming.getHttpApiLogicalId()); }); it('Should not configure stage resource', () => { expect(cfResources).to.not.have.property(naming.getHttpApiStageLogicalId()); }); it('Should not configure output', () => { expect(cfOutputs).to.not.have.property('HttpApiUrl'); }); it('Should configure endpoint that attaches to external API', () => { const routeKey = 'POST /some-post'; const resource = cfResources[naming.getHttpApiRouteLogicalId(routeKey)]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Route'); expect(resource.Properties.RouteKey).to.equal(routeKey); expect(resource.Properties.ApiId).to.equal(apiId); }); it('Should configure endpoint integration', () => { const resource = cfResources[naming.getHttpApiIntegrationLogicalId('foo')]; expect(resource.Type).to.equal('AWS::ApiGatewayV2::Integration'); expect(resource.Properties.IntegrationType).to.equal('AWS_PROXY'); }); it('Should configure lambda permissions', () => { const resource = cfResources[naming.getLambdaHttpApiPermissionLogicalId('foo')]; expect(resource.Type).to.equal('AWS::Lambda::Permission'); expect(resource.Properties.Action).to.equal('lambda:InvokeFunction'); }); }); describe('disallowed configurations', () => { it('Should not allow defined cors rules', () => { return expect( fixtures .extend('httpApi', { provider: { httpApi: { id: apiId, cors: true, }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }) ) ).to.eventually.be.rejected.and.have.property('code', 'EXTERNAL_HTTP_API_CORS_CONFIG'); }); it('Should not allow defined authorizers', () => { return expect( fixtures .extend('httpApi', { provider: { httpApi: { id: apiId, authorizers: { someAuthorizer: { identitySource: '$request.header.Authorization', issuerUrl: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx', audience: 'audiencexxx', }, }, }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }) ) ).to.eventually.be.rejected.and.have.property( 'code', 'EXTERNAL_HTTP_API_AUTHORIZERS_CONFIG' ); }); it('Should not allow defined logs', () => { return expect( fixtures .extend('httpApi', { provider: { httpApi: { id: apiId, }, logs: { httpApi: true, }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }) ) ).to.eventually.be.rejected.and.have.property('code', 'EXTERNAL_HTTP_API_LOGS_CONFIG'); }); }); }); describe('Timeout', () => { let cfResources; let naming; before(() => fixtures .extend('httpApi', { provider: { httpApi: { timeout: 3 } }, functions: { other: { events: [ { httpApi: { timeout: 20.56, }, }, ], }, }, }) .then(fixturePath => runServerless({ cwd: fixturePath, cliArgs: ['package'], }).then(({ awsNaming, cfTemplate }) => { ({ Resources: cfResources } = cfTemplate); naming = awsNaming; }) ) ); it('Should support timeout set at endpoint', () => { const resource = cfResources[naming.getHttpApiIntegrationLogicalId('other')]; expect(resource.Properties.TimeoutInMillis).to.equal(20560); }); it('Should support globally set timeout', () => { const resource = cfResources[naming.getHttpApiIntegrationLogicalId('foo')]; expect(resource.Properties.TimeoutInMillis).to.equal(3000); }); }); });