'use strict'; const { expect } = require('chai'); const log = require('log').get('serverless:test'); const awsRequest = require('@serverless/test/aws-request'); const fixtures = require('../../fixtures'); const { confirmCloudWatchLogs } = require('../../utils/misc'); const { getTmpDirPath } = require('../../utils/fs'); const { createTestService, deployService, removeService, fetch, } = require('../../utils/integration'); describe('HTTP API Integration Test', function() { this.timeout(1000 * 60 * 20); // Involves time-taking deploys let serviceName; let endpoint; let stackName; let tmpDirPath; const stage = 'dev'; const resolveEndpoint = async () => { const result = await awsRequest('CloudFormation', '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 () => { tmpDirPath = getTmpDirPath(); log.debug('temporary path %s', tmpDirPath); poolId = ( await awsRequest('CognitoIdentityServiceProvider', 'createUserPool', { PoolName: `test-http-api-${process.hrtime()[1]}`, }) ).UserPool.Id; [clientId] = await Promise.all([ awsRequest('CognitoIdentityServiceProvider', 'createUserPoolClient', { ClientName: 'test-http-api', UserPoolId: poolId, ExplicitAuthFlows: ['ALLOW_USER_PASSWORD_AUTH', 'ALLOW_REFRESH_TOKEN_AUTH'], PreventUserExistenceErrors: 'ENABLED', }).then(result => result.UserPoolClient.ClientId), awsRequest('CognitoIdentityServiceProvider', 'adminCreateUser', { UserPoolId: poolId, Username: userName, }).then(() => awsRequest('CognitoIdentityServiceProvider', 'adminSetUserPassword', { UserPoolId: poolId, Username: userName, Password: userPassword, Permanent: true, }) ), ]); const serverlessConfig = await createTestService(tmpDirPath, { templateDir: await fixtures.extend('httpApi', { provider: { httpApi: { cors: { exposedResponseHeaders: 'X-foo' }, authorizers: { someAuthorizer: { identitySource: '$request.header.Authorization', issuerUrl: `https://cognito-idp.us-east-1.amazonaws.com/${poolId}`, audience: clientId, }, }, }, logs: { httpApi: true }, }, functions: { foo: { events: [ { httpApi: { authorizer: 'someAuthorizer', }, }, ], }, other: { events: [ { httpApi: { timeout: 1, }, }, ], }, }, }), }); serviceName = serverlessConfig.service; stackName = `${serviceName}-${stage}`; log.notice('deploying %s service', serviceName); await deployService(tmpDirPath); return resolveEndpoint(); }); after(async () => { await awsRequest('CognitoIdentityServiceProvider', 'deleteUserPool', { UserPoolId: poolId }); if (!serviceName) return; log.notice('Removing service...'); await removeService(tmpDirPath); }); 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(503); }); 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('CognitoIdentityServiceProvider', '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 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 () => { tmpDirPath = getTmpDirPath(); log.debug('temporary path %s', tmpDirPath); const serverlessConfig = await createTestService(tmpDirPath, { templateDir: fixtures.map.httpApiCatchAll, }); serviceName = serverlessConfig.service; stackName = `${serviceName}-${stage}`; log.notice('deploying %s service', serviceName); await deployService(tmpDirPath); return resolveEndpoint(); }); after(async function() { // Added temporarily to inspect random fails // TODO: Remove once properly diagnosed if (this.test.parent.tests.some(test => test.state === 'failed')) return; log.notice('Removing service...'); await removeService(tmpDirPath); }); 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; before(async () => { exportServicePath = getTmpDirPath(); log.debug('service #1 path %s', exportServicePath); const exportServiceConfig = await createTestService(exportServicePath, { templateDir: fixtures.map.httpApiExport, }); log.notice('deploying %s service', exportServiceConfig.service); await deployService(exportServicePath); const httpApiId = ( await awsRequest('CloudFormation', 'describeStacks', { StackName: `${exportServiceConfig.service}-${stage}`, }) ).Stacks[0].Outputs[0].OutputValue; tmpDirPath = getTmpDirPath(); log.debug('sevice #2 path %s', tmpDirPath); const serverlessConfig = await createTestService(tmpDirPath, { templateDir: await fixtures.extend('httpApi', { provider: { httpApi: { id: httpApiId } }, }), }); serviceName = serverlessConfig.service; stackName = `${serviceName}-${stage}`; log.notice('deploying %s service', serviceName); await deployService(tmpDirPath); endpoint = (await awsRequest('ApiGatewayV2', 'getApi', { ApiId: httpApiId })).ApiEndpoint; }); after(async () => { if (serviceName) { log.notice('removing service #2'); await removeService(tmpDirPath); } log.notice('removing service #1'); 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); }); }); });