'use strict' const { expect } = require('chai') const log = require('log').get('serverless:test') const awsRequest = require('@serverless/test/aws-request') const CloudFormationService = require('aws-sdk').CloudFormation const CognitoIdentityServiceProviderService = require('aws-sdk').CognitoIdentityServiceProvider const ApiGatewayV2Service = require('aws-sdk').ApiGatewayV2 const fixtures = require('../../fixtures/programmatic') const { confirmCloudWatchLogs } = require('../../utils/misc') const { deployService, removeService, fetch, } = require('../../utils/integration') describe('HTTP API Integration Test', function () { this.timeout(1000 * 60 * 20) // Involves time-taking deploys let endpoint let stackName let serviceDir const stage = 'dev' const resolveEndpoint = async () => { const result = await awsRequest(CloudFormationService, 'describeStacks', { StackName: stackName, }) const endpointOutput = result.Stacks[0].Outputs.find( (output) => output.OutputKey === 'HttpApiUrl', ).OutputValue endpoint = endpointOutput.match( /https:\/\/.+\.execute-api\..+\.amazonaws\.com/, )[0] } describe('Specific endpoints', () => { let poolId let clientId const userName = 'test-http-api' const userPassword = 'razDwa3!' before(async () => { poolId = ( await awsRequest( CognitoIdentityServiceProviderService, 'createUserPool', { PoolName: `test-http-api-${process.hrtime()[1]}`, }, ) ).UserPool.Id ;[clientId] = await Promise.all([ awsRequest( CognitoIdentityServiceProviderService, 'createUserPoolClient', { ClientName: 'test-http-api', UserPoolId: poolId, ExplicitAuthFlows: [ 'ALLOW_USER_PASSWORD_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH', ], PreventUserExistenceErrors: 'ENABLED', }, ).then((result) => result.UserPoolClient.ClientId), awsRequest(CognitoIdentityServiceProviderService, 'adminCreateUser', { UserPoolId: poolId, Username: userName, }).then(() => awsRequest( CognitoIdentityServiceProviderService, 'adminSetUserPassword', { UserPoolId: poolId, Username: userName, Password: userPassword, Permanent: true, }, ), ), ]) const serviceData = await fixtures.setup('http-api', { configExt: { provider: { httpApi: { cors: { exposedResponseHeaders: 'X-foo' }, authorizers: { someAuthorizer: { identitySource: '$request.header.Authorization', issuerUrl: `https://cognito-idp.us-east-1.amazonaws.com/${poolId}`, audience: clientId, }, simpleCustomLambdaAuthorizer: { type: 'request', functionName: 'simpleCustomAuthorizer', enableSimpleResponses: true, }, standardCustomLambdaAuthorizer: { type: 'request', functionName: 'standardCustomAuthorizer', }, }, }, logs: { httpApi: true }, }, functions: { foo: { events: [ { httpApi: { authorizer: 'someAuthorizer', }, }, ], }, other: { timeout: 1, }, behindSimpleCustomAuthorizer: { handler: 'index.handler', events: [ { httpApi: { method: 'GET', path: '/behind-simple-authorizer', authorizer: 'simpleCustomLambdaAuthorizer', }, }, ], }, behindStandardCustomAuthorizer: { handler: 'index.handler', events: [ { httpApi: { method: 'GET', path: '/behind-standard-authorizer', authorizer: 'standardCustomLambdaAuthorizer', }, }, ], }, }, }, }) ;({ servicePath: serviceDir } = serviceData) const serviceName = serviceData.serviceConfig.service stackName = `${serviceName}-${stage}` await deployService(serviceDir) return resolveEndpoint() }) after(async () => { await awsRequest( CognitoIdentityServiceProviderService, 'deleteUserPool', { UserPoolId: poolId, }, ) if (!serviceDir) return await removeService(serviceDir) }) it('should expose an accessible POST HTTP endpoint', async () => { const testEndpoint = `${endpoint}/some-post` const response = await fetch(testEndpoint, { method: 'POST' }) const json = await response.json() expect(json).to.deep.equal({ method: 'POST', path: '/some-post' }) }) it('should expose an accessible paramed GET HTTP endpoint', async () => { const testEndpoint = `${endpoint}/bar/whatever` const response = await fetch(testEndpoint, { method: 'GET' }) const json = await response.json() expect(json).to.deep.equal({ method: 'GET', path: '/bar/whatever' }) }) it('should return 404 on not supported method', async () => { const testEndpoint = `${endpoint}/foo` const response = await fetch(testEndpoint, { method: 'POST' }) expect(response.status).to.equal(404) }) it('should return 404 on not configured path', async () => { const testEndpoint = `${endpoint}/not-configured` const response = await fetch(testEndpoint, { method: 'GET' }) expect(response.status).to.equal(404) }) it('should respect timeout settings', async () => { const testEndpoint = `${endpoint}/bar/timeout` const response = await fetch(testEndpoint, { method: 'GET' }) expect(response.status).to.equal(500) }) it('should support CORS when indicated', async () => { const testEndpoint = `${endpoint}/bar/whatever` const response = await fetch(testEndpoint, { method: 'GET', headers: { Origin: 'https://serverless.com' }, }) expect(response.headers.get('access-control-allow-origin')).to.equal('*') expect(response.headers.get('access-control-expose-headers')).to.equal( 'x-foo', ) }) it('should expose a GET HTTP endpoint backed by JWT authorization', async () => { const testEndpoint = `${endpoint}/foo` const responseUnauthorized = await fetch(testEndpoint, { method: 'GET', }) expect(responseUnauthorized.status).to.equal(401) const token = ( await awsRequest( CognitoIdentityServiceProviderService, 'initiateAuth', { AuthFlow: 'USER_PASSWORD_AUTH', AuthParameters: { USERNAME: userName, PASSWORD: userPassword }, ClientId: clientId, }, ) ).AuthenticationResult.IdToken const responseAuthorized = await fetch(testEndpoint, { method: 'GET', headers: { Authorization: token }, }) const json = await responseAuthorized.json() expect(json).to.deep.equal({ method: 'GET', path: '/foo' }) }) it('should expose a GET HTTP endpoint backed by simple custom request authorization', async () => { const testEndpoint = `${endpoint}/behind-simple-authorizer` const responseUnauthorized = await fetch(testEndpoint, { method: 'GET', }) expect(responseUnauthorized.status).to.equal(403) const responseAuthorized = await fetch(testEndpoint, { method: 'GET', headers: { Authorization: 'secretToken' }, }) const json = await responseAuthorized.json() expect(json).to.deep.equal({ method: 'GET', path: '/behind-simple-authorizer', }) }) it('should expose a GET HTTP endpoint backed by standard custom request authorization', async () => { const testEndpoint = `${endpoint}/behind-standard-authorizer` const responseUnauthorized = await fetch(testEndpoint, { method: 'GET', }) expect(responseUnauthorized.status).to.equal(403) const responseAuthorized = await fetch(testEndpoint, { method: 'GET', headers: { Authorization: 'secretToken' }, }) const json = await responseAuthorized.json() expect(json).to.deep.equal({ method: 'GET', path: '/behind-standard-authorizer', }) }) it('should expose access logs when configured to', () => confirmCloudWatchLogs(`/aws/http-api/${stackName}`, async () => { const response = await fetch(`${endpoint}/some-post`, { method: 'POST', }) await response.json() }).then((events) => { expect(events.length > 0).to.equal(true) })) }) describe('Catch-all endpoints', () => { before(async () => { const serviceData = await fixtures.setup('http-api-catch-all') ;({ servicePath: serviceDir } = serviceData) const serviceName = serviceData.serviceConfig.service stackName = `${serviceName}-${stage}` await deployService(serviceDir) return resolveEndpoint() }) after(async function () { if (this.test.parent.tests.some((test) => test.state === 'failed')) return log.notice('Removing service...') await removeService(serviceDir) }) it('should catch all root endpoint', async () => { const testEndpoint = `${endpoint}` const response = await fetch(testEndpoint, { method: 'GET' }) const json = await response.json() expect(json).to.deep.equal({ method: 'GET', path: '/' }) }) it('should catch all whatever endpoint', async () => { const testEndpoint = `${endpoint}/whatever` const response = await fetch(testEndpoint, { method: 'PATCH' }) const json = await response.json() expect(json).to.deep.equal({ method: 'PATCH', path: '/whatever' }) }) it('should catch all methods on method catch all endpoint', async () => { const testEndpoint = `${endpoint}/foo` const response = await fetch(testEndpoint, { method: 'PATCH' }) const json = await response.json() expect(json).to.deep.equal({ method: 'PATCH', path: '/foo' }) }) }) describe('Shared API', () => { let exportServicePath let serviceName before(async () => { const exportServiceData = await fixtures.setup('http-api-export') ;({ servicePath: exportServicePath } = exportServiceData) const exportServiceName = exportServiceData.serviceConfig.service await deployService(exportServicePath) const httpApiId = ( await awsRequest(CloudFormationService, 'describeStacks', { StackName: `${exportServiceName}-${stage}`, }) ).Stacks[0].Outputs[0].OutputValue endpoint = ( await awsRequest(ApiGatewayV2Service, 'getApi', { ApiId: httpApiId }) ).ApiEndpoint const serviceData = await fixtures.setup('http-api', { configExt: { provider: { httpApi: { id: httpApiId } }, }, }) ;({ servicePath: serviceDir } = serviceData) serviceName = serviceData.serviceConfig.service stackName = `${serviceName}-${stage}` await deployService(serviceDir) }) after(async () => { if (serviceName) { await removeService(serviceDir) } await removeService(exportServicePath) }) it('should expose an accessible POST HTTP endpoint', async () => { const testEndpoint = `${endpoint}/some-post` const response = await fetch(testEndpoint, { method: 'POST' }) const json = await response.json() expect(json).to.deep.equal({ method: 'POST', path: '/some-post' }) }) it('should expose an accessible paramed GET HTTP endpoint', async () => { const testEndpoint = `${endpoint}/bar/whatever` const response = await fetch(testEndpoint, { method: 'GET' }) const json = await response.json() expect(json).to.deep.equal({ method: 'GET', path: '/bar/whatever' }) }) it('should return 404 on not supported method', async () => { const testEndpoint = `${endpoint}/foo` const response = await fetch(testEndpoint, { method: 'POST' }) expect(response.status).to.equal(404) }) it('should return 404 on not configured path', async () => { const testEndpoint = `${endpoint}/not-configured` const response = await fetch(testEndpoint, { method: 'GET' }) expect(response.status).to.equal(404) }) }) })