Skip to content

Commit 768255a

Browse files
Add integ tests for CFN stack standup/teardown
These tests stand the CFN stack up in a region, verify that the resulting CloudFront distribution is serving the static site's index.html, and then tears the stack down. It does this in both IAD and PDX, since we've had issues with S3 regional URLs before.
1 parent 0e415fd commit 768255a

File tree

5 files changed

+139
-4
lines changed

5 files changed

+139
-4
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ dev-portal/node_modules/swagger-ui/*
2828
# misc
2929
npm-debug.log
3030
.DS_Store
31-
packaged.yaml
31+
packaged*.yaml
3232
cognito.js
3333
.idea
3434
.vscode

.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ node_js:
66
cache: npm
77

88
before_install:
9+
# install clis in parallel
10+
# - pip install --user awscli
11+
# pip install --user aws-sam-cli
12+
# https://github.com/travis-ci/travis-ci/issues/3139
913
- cd dev-portal
1014
# install everything in dev portal
1115
- npm install

__tests__/cfn-integration-test.js

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const fs = require('fs')
2+
const rp = require('request-promise')
3+
4+
describe('template.yaml', () => {
5+
// NOTE: These tests all assume that the CFN template has already been packaged *PER REGION*!
6+
const cfnTimeout = 75,
7+
// run the test with a timeout of slightly longer than double the CFN stack timeout
8+
testTimeout = 1000*60*cfnTimeout*2 + 1
9+
const _console = console.log
10+
11+
async function commonTest(region, stackMiddlefix) {
12+
let unixTimestamp = Math.floor(new Date() / 1000),
13+
stackName = `cfn-integ-${ stackMiddlefix }-${ unixTimestamp }`,
14+
s3Params = {
15+
// CFN, when reading the template from S3, requires the S3 bucket to be in the same region as the CFN stack...
16+
Bucket: `dev-portal-integ-${ region }`,
17+
Body: fs.readFileSync(`./cloudformation/packaged-${region}.yaml`),
18+
Key: stackName
19+
},
20+
cfnParams = {
21+
StackName: stackName,
22+
Capabilities: ['CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'],
23+
Parameters: [
24+
{
25+
ParameterKey: 'DevPortalSiteS3BucketName',
26+
ParameterValue: `integ-${ stackMiddlefix }-${ unixTimestamp }-dev-portal-test`
27+
},
28+
{
29+
ParameterKey: 'ArtifactsS3BucketName',
30+
ParameterValue: `integ-${ stackMiddlefix }-${ unixTimestamp }-artifact-bucket`
31+
},
32+
{
33+
ParameterKey: 'DevPortalCustomersTableName',
34+
ParameterValue: `Customers${ unixTimestamp }`
35+
},
36+
{
37+
ParameterKey: 'CognitoDomainNameOrPrefix',
38+
ParameterValue: `integ-${ stackMiddlefix }-${ unixTimestamp }`
39+
}
40+
],
41+
// RoleARN: 'STRING_VALUE',
42+
TimeoutInMinutes: cfnTimeout
43+
}
44+
45+
console.log('commonTest', region)
46+
47+
const AWS = require('aws-sdk')
48+
AWS.config.update({ region: region })
49+
50+
// pin versions of SDKs
51+
const cfn = new AWS.CloudFormation({ region: region }),
52+
s3 = new AWS.S3({ region: region }),
53+
logger = function (input) {
54+
_console(`${region}:${stackName}:${input}`)
55+
}
56+
57+
// Upload the packaged template to S3, then use the resulting URL in the CFN createStack call
58+
// This is necessary because the file is too large to deliver in-line to CFN
59+
cfnParams.TemplateURL =(await s3.upload(s3Params).promise()).Location
60+
61+
logger('createStack call starting.')
62+
await cfn.createStack(cfnParams).promise()
63+
logger('createStack call succeeded.')
64+
65+
logger('stackExists waiter starting.')
66+
await cfn.waitFor('stackExists', { StackName: stackName }).promise()
67+
logger('stackExists waiter succeeded.')
68+
69+
logger('stackCreateComplete waiter starting.')
70+
let devPortalUrl =
71+
(await cfn.waitFor('stackCreateComplete', { StackName: stackName }).promise()).Stacks[0].Outputs
72+
.find((output) => output.OutputKey === 'WebsiteURL').OutputValue
73+
logger('stackCreateComplete waiter succeeded.')
74+
75+
logger(`verifying that stack is available at ${devPortalUrl} .`)
76+
let staticIndex = await rp(devPortalUrl)
77+
78+
expect(staticIndex.includes('<title>Developer Portal</title>')).toBeTruthy()
79+
logger(`verified that stack is available at ${devPortalUrl} .`)
80+
81+
// add RoleArn: ... later
82+
logger('deleteStack call starting.')
83+
await cfn.deleteStack({ StackName: stackName }).promise()
84+
logger('deleteStack call succeeded.')
85+
86+
logger('stackDeleteComplete waiter starting.')
87+
await cfn.waitFor('stackDeleteComplete', { StackName: stackName }).promise()
88+
logger('stackDeleteComplete waiter succeeded.')
89+
90+
// pass the test; we successfully stood the stack up and tore it down
91+
expect(true).toBe(true)
92+
93+
return true
94+
}
95+
96+
test.concurrent('should stand up and tear down the stack in IAD', async () => {
97+
return commonTest('us-east-1', 'us-east-1')
98+
}, testTimeout)
99+
100+
test.concurrent('should stand up and tear down the stack in a non-IAD region', async () => {
101+
return commonTest('us-west-2', 'us-west-2')
102+
}, testTimeout)
103+
104+
// Implement these! Hardest part will be having test ACM certs in the account
105+
// test('should stand up and tear down a custom domain stack in IAD', async () => {
106+
// return commonTest('us-east-1', 'domain-us-east-1')
107+
// }, testTimeout)
108+
//
109+
// test('should stand up and tear down a custom domain stack in a non-IAD region', async () => {
110+
// return commonTest('us-west-2', 'domain-us-west-2')
111+
// }, testTimeout)
112+
})

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99
"get-dev-portal-dependencies": "(cd dev-portal; npm run get-dependencies)",
1010
"test": "node scripts/test.js",
1111
"coverage": "node scripts/test.js --coverage=true",
12-
"cover": "npm run coverage"
12+
"integ": "node scripts/test.js --integ=true",
13+
"cover": "npm run coverage",
14+
"cfn-lint": "cfn-lint cloudformation/template.yaml -c I"
1315
},
1416
"license": "Apache-2.0",
1517
"devDependencies": {
18+
"aws-sdk": "^2.406.0",
1619
"fs-extra": "^7.0.1",
1720
"jest": "^23.6.0",
1821
"memorystream": "^0.3.1",
1922
"xml-js": "^1.6.8"
2023
},
21-
"dependencies": {}
24+
"dependencies": {
25+
"request-promise": "^4.2.4"
26+
}
2227
}

scripts/test.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { spawn } = require('child_process'),
1+
const { spawn, spawnSync } = require('child_process'),
22
path = require('path'),
33
convert = require('xml-js'),
44
fse = require('fs-extra')
@@ -149,12 +149,26 @@ async function runTests(withCoverage) {
149149
if(withCoverage) processCoverage(await synthesizeCoverage())
150150
}
151151

152+
function runIntegTests() {
153+
let params = { stdio: 'inherit', shell: true, cwd: process.cwd() }
154+
155+
// run the tests with 4 worker threads so the 40 minute-long test runs run in parallel
156+
// increase this as tests are added, if needed
157+
for(let region of ['us-east-1', 'us-west-2']) {
158+
spawnSync(`sam package --region ${region} --template-file ./cloudformation/template.yaml --output-template-file ./cloudformation/packaged-${region}.yaml --s3-bucket dev-portal-integ-${region}`, params)
159+
}
160+
161+
return spawnSync('jest -w 4 cfn-integration-test', params)
162+
}
163+
152164
// maybe bring in an args parsing library later
153165
let args = process.argv.slice(2)
154166
if(args[0] === '--coverage=true') {
155167
return runTests(true).catch((e) => console.error(e))
156168
} else if(args[0] === '--coverage=false') {
157169
return runTests(false).catch((e) => console.error(e))
170+
} else if(args[0] === '--integ=true'){
171+
return runIntegTests()
158172
} else {
159173
return runTests(false).catch((e) => console.error(e))
160174
}

0 commit comments

Comments
 (0)