UNPKG

aws-spa

Version:

A no-brainer script to deploy a single page app on AWS

503 lines (491 loc) 20 kB
"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(); }); }); });