serverless/test/integration/aws/http-api.test.js
2024-05-29 11:51:04 -04:00

402 lines
13 KiB
JavaScript

'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)
})
})
})