aws-spa
Version:
A no-brainer script to deploy a single page app on AWS
503 lines (491 loc) • 20 kB
JavaScript
"use strict";
var _ = require(".");
var _awsServices = require("../aws-services");
var _testHelper = require("../test-helper");
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
describe('cloudfront', () => {
describe('findDeployedCloudfrontDistribution', () => {
const listDistributionMock = jest.spyOn(_awsServices.cloudfront, 'listDistributions');
const listTagsForResourceMock = jest.spyOn(_awsServices.cloudfront, 'listTagsForResource');
const waitForMock = jest.spyOn(_awsServices.waitUntil, 'distributionDeployed');
afterEach(() => {
listDistributionMock.mockReset();
listTagsForResourceMock.mockReset();
waitForMock.mockReset();
});
it('should return the distribution even if on page 2', async () => {
listDistributionMock.mockReturnValueOnce((0, _testHelper.awsResolve)({
DistributionList: {
NextMarker: 'xxx',
Items: [{
Id: 'GOODBYE',
Aliases: {
Items: ['goodbye.example.com']
}
}]
}
})).mockReturnValueOnce((0, _testHelper.awsResolve)({
DistributionList: {
Items: [{
Id: 'HELLO',
Status: 'Deployed',
Aliases: {
Items: ['hello.example.com']
}
}]
}
}));
listTagsForResourceMock.mockReturnValue((0, _testHelper.awsResolve)({
Tags: {
Items: [_.identifyingTag]
}
}));
const distribution = await (0, _.findDeployedCloudfrontDistribution)('hello.example.com');
expect(distribution).toBeDefined();
expect(distribution.Id).toEqual('HELLO');
});
it('should wait for distribution if distribution is not deployed', async () => {
listDistributionMock.mockReturnValue((0, _testHelper.awsResolve)({
DistributionList: {
Items: [{
Id: 'HELLO',
Status: 'In Progress',
Aliases: {
Items: ['hello.example.com']
}
}]
}
}));
listTagsForResourceMock.mockReturnValue((0, _testHelper.awsResolve)({
Tags: {
Items: [_.identifyingTag]
}
}));
waitForMock.mockReturnValue((0, _testHelper.awsResolve)());
await (0, _.findDeployedCloudfrontDistribution)('hello.example.com');
expect(waitForMock).toHaveBeenCalledTimes(1);
});
});
describe('invalidateCloudfrontCache', () => {
const createInvalidationMock = jest.spyOn(_awsServices.cloudfront, 'createInvalidation');
const waitForMock = jest.spyOn(_awsServices.waitUntil, 'invalidationCompleted');
afterEach(() => {
createInvalidationMock.mockReset();
waitForMock.mockReset();
});
it('should invalidate the specified path', async () => {
createInvalidationMock.mockReturnValue((0, _testHelper.awsResolve)({
Invalidation: {}
}));
await (0, _.invalidateCloudfrontCache)('some-distribution-id', 'index.html');
expect(createInvalidationMock).toHaveBeenCalledTimes(1);
const invalidationParams = createInvalidationMock.mock.calls[0][0];
expect(invalidationParams.DistributionId).toEqual('some-distribution-id');
expect(invalidationParams.InvalidationBatch.Paths.Items[0]).toEqual('index.html');
});
it('should invalidate the specified paths', async () => {
createInvalidationMock.mockReturnValue((0, _testHelper.awsResolve)({
Invalidation: {}
}));
await (0, _.invalidateCloudfrontCache)('some-distribution-id', 'index.html, static/*');
expect(createInvalidationMock).toHaveBeenCalledTimes(1);
const invalidationParams = createInvalidationMock.mock.calls[0][0];
expect(invalidationParams.DistributionId).toEqual('some-distribution-id');
expect(invalidationParams.InvalidationBatch.Paths.Items).toEqual(['index.html', 'static/*']);
});
it('should wait for invalidate if wait flag is true', async () => {
createInvalidationMock.mockReturnValue((0, _testHelper.awsResolve)({
Invalidation: {
Id: 'some-invalidation-id'
}
}));
waitForMock.mockReturnValue((0, _testHelper.awsResolve)());
await (0, _.invalidateCloudfrontCache)('some-distribution-id', 'index.html', true);
expect(waitForMock).toHaveBeenCalledTimes(1);
expect(waitForMock).toHaveBeenCalledWith(expect.anything(), {
DistributionId: 'some-distribution-id',
Id: 'some-invalidation-id'
});
});
});
describe('invalidateCloudfrontCacheWithRetry', () => {
const createInvalidationMock = jest.spyOn(_awsServices.cloudfront, 'createInvalidation');
const waitForMock = jest.spyOn(_awsServices.waitUntil, 'invalidationCompleted');
afterEach(() => {
createInvalidationMock.mockReset();
waitForMock.mockReset();
});
it('should retry once', async () => {
createInvalidationMock.mockReturnValueOnce((0, _testHelper.awsReject)(1)).mockReturnValueOnce((0, _testHelper.awsResolve)({
Invalidation: {}
}));
await (0, _.invalidateCloudfrontCacheWithRetry)('some-distribution-id', 'index.html, static/*');
expect(createInvalidationMock).toHaveBeenCalledTimes(2);
});
it('should retry 5 times at most', async () => {
createInvalidationMock.mockReturnValueOnce((0, _testHelper.awsReject)(1)).mockReturnValueOnce((0, _testHelper.awsReject)(1)).mockReturnValueOnce((0, _testHelper.awsReject)(1)).mockReturnValueOnce((0, _testHelper.awsReject)(1)).mockReturnValueOnce((0, _testHelper.awsReject)(1)).mockReturnValueOnce((0, _testHelper.awsResolve)({
Invalidation: {}
}));
try {
await (0, _.invalidateCloudfrontCacheWithRetry)('some-distribution-id', 'index.html, static/*');
} catch (error) {
expect(error).toBeDefined();
}
expect(createInvalidationMock).toHaveBeenCalledTimes(5);
});
});
describe('createCloudFrontDistribution', () => {
const createDistributionMock = jest.spyOn(_awsServices.cloudfront, 'createDistribution');
const waitForMock = jest.spyOn(_awsServices.waitUntil, 'distributionDeployed');
const tagResourceMock = jest.spyOn(_awsServices.cloudfront, 'tagResource');
afterEach(() => {
createDistributionMock.mockReset();
waitForMock.mockReset();
tagResourceMock.mockReset();
});
it('should create a distribution and wait for it to be available', async () => {
const distribution = {
Id: 'distribution-id'
};
createDistributionMock.mockReturnValue((0, _testHelper.awsResolve)({
Distribution: distribution
}));
tagResourceMock.mockReturnValue((0, _testHelper.awsResolve)());
waitForMock.mockReturnValue((0, _testHelper.awsResolve)());
const result = await (0, _.createCloudFrontDistribution)('hello.lalilo.com', 'arn:certificate');
expect(result).toEqual(distribution);
expect(tagResourceMock).toHaveBeenCalledTimes(1);
expect(createDistributionMock).toHaveBeenCalledTimes(1);
const distributionParam = createDistributionMock.mock.calls[0][0];
const distributionConfig = distributionParam.DistributionConfig;
expect(distributionConfig.Origins.Items[0].DomainName).toEqual('hello.lalilo.com.s3-website.eu-west-3.amazonaws.com');
expect(distributionConfig.DefaultCacheBehavior.ViewerProtocolPolicy).toEqual('redirect-to-https');
expect(distributionConfig.DefaultCacheBehavior.MinTTL).toEqual(0);
expect(distributionConfig.DefaultCacheBehavior.Compress).toEqual(true);
expect(distributionConfig.ViewerCertificate.ACMCertificateArn).toEqual('arn:certificate');
expect(waitForMock).toHaveBeenCalledTimes(1);
expect(waitForMock).toHaveBeenCalledWith(expect.anything(), {
Id: 'distribution-id'
});
});
});
describe('getCacheInvalidations', () => {
it.each([{
input: 'index.html',
expectedOutput: '/index.html'
}, {
input: '/index.html',
expectedOutput: '/index.html'
}, {
input: 'index.html, hello.html',
subFolder: undefined,
expectedOutput: '/index.html,/hello.html'
}, {
input: 'index.html',
subFolder: 'some-branch',
expectedOutput: '/some-branch/index.html'
}])('add missing slash', ({
input,
subFolder,
expectedOutput
}) => {
expect((0, _.getCacheInvalidations)(input, subFolder)).toEqual(expectedOutput);
});
});
describe('updateCloudFrontDistribution', () => {
const getDistributionConfigMock = jest.spyOn(_awsServices.cloudfront, 'getDistributionConfig');
const updateDistribution = jest.spyOn(_awsServices.cloudfront, 'updateDistribution');
const listFunctions = jest.spyOn(_awsServices.cloudfront, 'listFunctions');
const createFunction = jest.spyOn(_awsServices.cloudfront, 'createFunction');
const publishFunction = jest.spyOn(_awsServices.cloudfront, 'publishFunction');
beforeEach(() => {
getDistributionConfigMock.mockReset();
updateDistribution.mockReset();
listFunctions.mockReturnValueOnce((0, _testHelper.awsResolve)({
Functions: []
}));
publishFunction.mockReturnValue((0, _testHelper.awsResolve)({}));
createFunction.mockReturnValue((0, _testHelper.awsResolve)({
ETag: 'lol',
FunctionSummary: {
FunctionMetadata: {
Id: 'oac-id',
FunctionARN: 'plop'
}
}
}));
});
it.each([{
shouldBlockBucketPublicAccess: true,
noDefaultRootObject: false
}, {
shouldBlockBucketPublicAccess: false,
noDefaultRootObject: false
}, {
shouldBlockBucketPublicAccess: true,
noDefaultRootObject: true
}, {
shouldBlockBucketPublicAccess: false,
noDefaultRootObject: true
}])(`should not update the distribution if the configuration doesn't change %p`, async ({
shouldBlockBucketPublicAccess,
noDefaultRootObject
}) => {
if (noDefaultRootObject) {
listFunctions.mockReturnValueOnce((0, _testHelper.awsResolve)({
Functions: [{
FunctionConfig: {
FunctionMetadata: {
Id: 'oac-id',
FunctionARN: 'plop'
}
}
}]
}));
}
const domainName = 'hello.lalilo.com';
const originId = shouldBlockBucketPublicAccess ? (0, _awsServices.getS3DomainNameForBlockedBucket)(domainName) : (0, _awsServices.getOriginId)(domainName);
const originDomainName = shouldBlockBucketPublicAccess ? (0, _awsServices.getS3DomainNameForBlockedBucket)(domainName) : (0, _awsServices.getS3DomainName)(domainName);
const distribution = {
Id: 'distribution-id',
DefaultRootObject: noDefaultRootObject ? '' : 'index.html',
Origins: {
Quantity: 1,
Items: [{
Id: originId,
DomainName: originDomainName
}]
},
DefaultCacheBehavior: _objectSpread({
TargetOriginId: originId
}, noDefaultRootObject && {
FunctionAssociations: {
Quantity: 1,
Items: [{
FunctionARN: 'plop',
EventType: 'viewer-request'
}]
}
})
};
getDistributionConfigMock.mockReturnValue((0, _testHelper.awsResolve)({
DistributionConfig: distribution
}));
if (shouldBlockBucketPublicAccess) {
await (0, _.updateCloudFrontDistribution)(distribution.Id, domainName, {
shouldBlockBucketPublicAccess,
noDefaultRootObject,
oac: {
originAccessControl: {
Id: 'oac-id'
},
ETag: 'etag'
},
redirect403ToRoot: false
});
} else {
await (0, _.updateCloudFrontDistribution)(distribution.Id, domainName, {
shouldBlockBucketPublicAccess,
noDefaultRootObject,
oac: null,
redirect403ToRoot: false
});
}
expect(updateDistribution).not.toHaveBeenCalled();
});
it('should update the distribution with an OAC when shouldBlockBucketPublicAccess and oac is given', async () => {
const domainName = 'hello.lalilo.com';
const originIdForPrivateBucket = (0, _awsServices.getS3DomainNameForBlockedBucket)(domainName);
const oac = {
originAccessControl: {
Id: 'oac-id'
},
ETag: 'etag'
};
const distribution = {
Id: 'distribution-id',
Origins: {
Items: [{
DomainName: (0, _awsServices.getS3DomainName)(domainName)
}]
},
DefaultCacheBehavior: {
TargetOriginId: (0, _awsServices.getS3DomainName)(domainName)
}
};
getDistributionConfigMock.mockReturnValue((0, _testHelper.awsResolve)({
DistributionConfig: distribution
}));
updateDistribution.mockReturnValueOnce((0, _testHelper.awsResolve)());
await (0, _.updateCloudFrontDistribution)(distribution.Id, domainName, {
shouldBlockBucketPublicAccess: true,
noDefaultRootObject: false,
oac,
redirect403ToRoot: false
});
expect(updateDistribution).toHaveBeenCalled();
expect(updateDistribution).toHaveBeenCalledWith(expect.objectContaining({
DistributionConfig: expect.objectContaining({
DefaultRootObject: 'index.html',
Origins: expect.objectContaining({
Items: [expect.objectContaining({
Id: originIdForPrivateBucket,
DomainName: originIdForPrivateBucket,
OriginAccessControlId: oac.originAccessControl.Id,
S3OriginConfig: {
OriginAccessIdentity: ''
}
})]
}),
DefaultCacheBehavior: expect.objectContaining({
TargetOriginId: originIdForPrivateBucket
})
})
}));
});
it.each([{
noDefaultRootObject: false
}, {
noDefaultRootObject: true
}])(`should update the distribution if the defaultRootObject if different from the existing config (and not touch to other config) %p`, async ({
noDefaultRootObject
}) => {
const domainName = 'hello.lalilo.com';
const originIdForPrivateBucket = (0, _awsServices.getS3DomainNameForBlockedBucket)(domainName);
const oac = {
originAccessControl: {
Id: 'oac-id'
},
ETag: 'etag'
};
const distribution = {
Id: 'distribution-id',
Origins: {
Items: [{
DomainName: originIdForPrivateBucket
}]
},
DefaultCacheBehavior: {
TargetOriginId: originIdForPrivateBucket
},
DefaultRootObject: noDefaultRootObject ? 'index.html' : ''
};
const originalConfig = (0, _testHelper.awsResolve)({
DistributionConfig: distribution
});
getDistributionConfigMock.mockReturnValue(originalConfig);
updateDistribution.mockReturnValueOnce((0, _testHelper.awsResolve)());
await (0, _.updateCloudFrontDistribution)(distribution.Id, domainName, {
shouldBlockBucketPublicAccess: true,
noDefaultRootObject,
oac,
redirect403ToRoot: false
});
expect(updateDistribution).toHaveBeenCalled();
expect(updateDistribution).toHaveBeenCalledWith(expect.objectContaining({
DistributionConfig: expect.objectContaining({
DefaultRootObject: noDefaultRootObject ? '' : 'index.html',
Origins: expect.objectContaining({
Items: [expect.objectContaining({
DomainName: originIdForPrivateBucket
})]
}),
DefaultCacheBehavior: expect.objectContaining({
TargetOriginId: originIdForPrivateBucket
})
})
}));
});
it('should update the distribution if the 403 redirection option was not set in the existing config', async () => {
const domainName = 'hello.lalilo.com';
const originIdForPrivateBucket = (0, _awsServices.getS3DomainNameForBlockedBucket)(domainName);
const distribution = {
Id: 'distribution-id',
Origins: {
Items: [{
DomainName: originIdForPrivateBucket
}]
},
DefaultCacheBehavior: {
TargetOriginId: originIdForPrivateBucket
},
CustomErrorResponses: {
Quantity: 0
}
};
getDistributionConfigMock.mockReturnValue((0, _testHelper.awsResolve)({
DistributionConfig: distribution
}));
updateDistribution.mockReturnValueOnce((0, _testHelper.awsResolve)());
await (0, _.updateCloudFrontDistribution)(distribution.Id, domainName, {
shouldBlockBucketPublicAccess: false,
noDefaultRootObject: false,
oac: null,
redirect403ToRoot: true
});
expect(updateDistribution).toHaveBeenCalled();
expect(updateDistribution).toHaveBeenCalledWith(expect.objectContaining({
DistributionConfig: expect.objectContaining({
CustomErrorResponses: {
Quantity: 1,
Items: [{
ErrorCode: 403,
ResponsePagePath: '/index.html',
ResponseCode: '200',
ErrorCachingMinTTL: 10
}]
}
})
}));
});
it('should not update the distribution if the 403 redirection option is already set in the existing config', async () => {
const domainName = 'hello.lalilo.com';
const originIdForPrivateBucket = (0, _awsServices.getS3DomainNameForBlockedBucket)(domainName);
const distribution = {
Id: 'distribution-id',
Origins: {
Items: [{
DomainName: originIdForPrivateBucket
}]
},
DefaultRootObject: '',
DefaultCacheBehavior: {
TargetOriginId: originIdForPrivateBucket,
FunctionAssociations: {
Quantity: 1,
Items: [{
FunctionARN: 'plop',
EventType: 'viewer-request'
}]
}
},
CustomErrorResponses: {
Quantity: 1,
Items: [{
ErrorCode: 403,
ResponsePagePath: '/index.html',
ResponseCode: '200',
ErrorCachingMinTTL: 10
}]
}
};
getDistributionConfigMock.mockReturnValue((0, _testHelper.awsResolve)({
DistributionConfig: distribution
}));
await (0, _.updateCloudFrontDistribution)(distribution.Id, domainName, {
shouldBlockBucketPublicAccess: true,
noDefaultRootObject: true,
oac: {
originAccessControl: {
Id: 'oac-id'
},
ETag: 'etag'
},
redirect403ToRoot: true
});
expect(updateDistribution).not.toHaveBeenCalled();
});
});
});