UNPKG

aws-spa

Version:

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

377 lines (354 loc) 17.4 kB
"use strict"; var fs = _interopRequireWildcard(require("fs")); var _inquirer = _interopRequireDefault(require("inquirer")); var _awsServices = require("./aws-services"); var fsHelper = _interopRequireWildcard(require("./fs-helper")); var _logger = require("./logger"); var _s = require("./s3"); var _testHelper = require("./test-helper"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 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; } jest.mock('fs', () => _objectSpread(_objectSpread({}, jest.requireActual('fs')), {}, { createReadStream: jest.fn() })); jest.mock('inquirer', () => { return { __esModule: true, default: { prompt: jest.fn() } }; }); describe('s3', () => { const logSpy = jest.spyOn(_logger.logger, 'info'); afterEach(() => { logSpy.mockReset(); }); describe('createBucket', () => { const createBucketSpy = jest.spyOn(_awsServices.s3, 'createBucket'); afterEach(() => { createBucketSpy.mockReset(); }); it('should log a creation messages', async () => { createBucketSpy.mockReturnValue((0, _testHelper.awsResolve)()); await (0, _s.createBucket)('some-bucket'); expect(logSpy).toHaveBeenCalledTimes(1); expect(logSpy.mock.calls[0][0]).toContain('Creating "some-bucket"'); }); it('should call s3.createBucket with bucket name', async () => { createBucketSpy.mockReturnValue((0, _testHelper.awsResolve)()); await (0, _s.createBucket)('some-bucket'); expect(createBucketSpy).toHaveBeenCalledTimes(1); const createBucketParams = createBucketSpy.mock.calls[0][0]; expect(createBucketParams.Bucket).toEqual('some-bucket'); }); it('should throw if s3.createBucket throws', async () => { createBucketSpy.mockReturnValue((0, _testHelper.awsReject)(400, 'some error')); try { await (0, _s.createBucket)('some-bucket'); throw new Error('This test should have failed'); } catch (error) { expect(error.message).toEqual('some error'); } }); it('should handle bucket in other region', async () => { createBucketSpy.mockReturnValue((0, _testHelper.awsReject)(409)); try { await (0, _s.createBucket)('some-bucket'); throw new Error('This test should have failed'); } catch (error) { expect(error.message).toContain('bucket already exists'); } }); }); describe('setBucketWebsite', () => { const putBucketWebsiteSpy = jest.spyOn(_awsServices.s3, 'putBucketWebsite'); afterEach(() => { putBucketWebsiteSpy.mockReset(); }); it('should log a message', async () => { putBucketWebsiteSpy.mockReturnValue((0, _testHelper.awsResolve)()); await (0, _s.setBucketWebsite)('some-bucket'); expect(logSpy).toHaveBeenCalledTimes(1); expect(logSpy.mock.calls[0][0]).toContain('Set bucket website'); }); it('should set bucket website', async () => { putBucketWebsiteSpy.mockReturnValue((0, _testHelper.awsResolve)()); await (0, _s.setBucketWebsite)('some-bucket'); expect(putBucketWebsiteSpy).toHaveBeenCalledTimes(1); const websiteParams = putBucketWebsiteSpy.mock.calls[0][0]; expect(websiteParams.Bucket).toEqual('some-bucket'); }); it('should throw if s3.putBucketWebsite throws', async () => { putBucketWebsiteSpy.mockReturnValue((0, _testHelper.awsReject)(400, 'some error')); try { await (0, _s.setBucketWebsite)('some-bucket'); throw new Error('This test should have failed'); } catch (error) { expect(error.message).toEqual('some error'); } }); }); describe('setBucketPolicy', () => { const putBucketPolicySpy = jest.spyOn(_awsServices.s3, 'putBucketPolicy'); afterEach(() => { putBucketPolicySpy.mockReset(); }); it('should log a message', async () => { putBucketPolicySpy.mockReturnValue((0, _testHelper.awsResolve)()); await (0, _s.setBucketPolicy)('some-bucket'); expect(logSpy).toHaveBeenCalledTimes(1); expect(logSpy.mock.calls[0][0]).toContain('Allow public read'); }); it('should set bucket policy', async () => { putBucketPolicySpy.mockReturnValue((0, _testHelper.awsResolve)()); await (0, _s.setBucketPolicy)('some-bucket'); expect(putBucketPolicySpy).toHaveBeenCalledTimes(1); const policyParams = putBucketPolicySpy.mock.calls[0][0]; const statement = JSON.parse(policyParams.Policy).Statement[0]; expect(policyParams.Bucket).toEqual('some-bucket'); expect(statement.Sid).toEqual('AllowPublicRead'); expect(statement.Effect).toEqual('Allow'); expect(statement.Principal.AWS).toEqual('*'); expect(statement.Action).toEqual('s3:GetObject'); expect(statement.Resource).toEqual('arn:aws:s3:::some-bucket/*'); }); it('should throw if s3.putBucketWebsite throws', async () => { putBucketPolicySpy.mockReturnValue((0, _testHelper.awsReject)(400, 'some error')); try { await (0, _s.setBucketPolicy)('some-bucket'); throw new Error('This test should have failed'); } catch (error) { expect(error.message).toEqual('some error'); } }); }); describe('tagBucket', () => { const putBucketTaggingMock = jest.spyOn(_awsServices.s3, 'putBucketTagging'); afterEach(() => { putBucketTaggingMock.mockReset(); }); it('should log a message', async () => { putBucketTaggingMock.mockReturnValue((0, _testHelper.awsResolve)()); await (0, _s.tagBucket)('some-bucket'); expect(logSpy).toHaveBeenCalledTimes(1); expect(logSpy.mock.calls[0][0]).toContain('Tagging "some-bucket"'); }); it('should tag', async () => { putBucketTaggingMock.mockReturnValue((0, _testHelper.awsResolve)()); await (0, _s.tagBucket)('some-bucket'); expect(putBucketTaggingMock).toHaveBeenCalledTimes(1); const taggingParams = putBucketTaggingMock.mock.calls[0][0]; expect(taggingParams.Bucket).toEqual('some-bucket'); expect(taggingParams.Tagging.TagSet.length).toEqual(1); expect(taggingParams.Tagging.TagSet[0]).toEqual(_s.identifyingTag); }); it('should throw if s3.putBucketTagging throws', async () => { putBucketTaggingMock.mockReturnValue((0, _testHelper.awsReject)(400, 'some error')); try { await (0, _s.tagBucket)('some-bucket'); throw new Error('This test should have failed'); } catch (error) { expect(error.message).toEqual('some error'); } }); }); describe('doesS3BucketExists', () => { const headBucketSpy = jest.spyOn(_awsServices.s3, 'headBucket'); it('should handle success case', async () => { headBucketSpy.mockReturnValue((0, _testHelper.awsResolve)()); expect(await (0, _s.doesS3BucketExists)('some-bucket')).toBe(true); expect(logSpy).toBeCalledTimes(2); expect(logSpy.mock.calls[0][0]).toContain('Looking for bucket "some-bucket"'); }); it('should handle not found bucket', async () => { headBucketSpy.mockReturnValue((0, _testHelper.awsReject)(404)); expect(await (0, _s.doesS3BucketExists)('some-bucket')).toBe(false); expect(logSpy).toBeCalledTimes(2); expect(logSpy.mock.calls[1][0]).toContain('Bucket "some-bucket" not found'); }); it('should throw if headBucket throws non 404 error', async () => { headBucketSpy.mockReturnValue((0, _testHelper.awsReject)(400, 'some message')); try { await (0, _s.doesS3BucketExists)('some-bucket'); throw new Error('This test should have failed'); } catch (error) { expect(error.message).toEqual('some message'); } }); }); describe('confirmBucketManagement', () => { const getBucketTaggingMock = jest.spyOn(_awsServices.s3, 'getBucketTagging'); const promptMock = jest.spyOn(_inquirer.default, 'prompt'); afterEach(() => { getBucketTaggingMock.mockReset(); promptMock.mockReset(); }); it('should not prompt if bucket is tagged by aws-spa', async () => { getBucketTaggingMock.mockReturnValue((0, _testHelper.awsResolve)({ TagSet: [_s.identifyingTag] })); expect(await (0, _s.confirmBucketManagement)('some bucket')).toEqual(true); expect(getBucketTaggingMock.mock.calls.length).toEqual(1); const getTaggingParams = getBucketTaggingMock.mock.calls[0][0]; expect(getTaggingParams.Bucket).toEqual('some bucket'); expect(promptMock).not.toHaveBeenCalled(); }); it('should prompt if bucket has tag but no tag from aws-spa', async () => { getBucketTaggingMock.mockReturnValue((0, _testHelper.awsResolve)({ TagSet: [] })); promptMock.mockResolvedValue({ continueUpdate: true }); expect(await (0, _s.confirmBucketManagement)('some bucket')).toEqual(true); expect(promptMock).toHaveBeenCalled(); }); it('should prompt if bucket has no tag', async () => { getBucketTaggingMock.mockReturnValue((0, _testHelper.awsReject)(404)); promptMock.mockResolvedValue({ continueUpdate: true }); expect(await (0, _s.confirmBucketManagement)('some bucket')).toEqual(true); expect(promptMock).toHaveBeenCalled(); }); it('should throw if fetch tag throwed', async () => { getBucketTaggingMock.mockReturnValue((0, _testHelper.awsReject)(400, 'fetch tagging error')); try { await (0, _s.confirmBucketManagement)('some bucket'); throw new Error('this test should have failed'); } catch (error) { expect(error.message).toEqual('fetch tagging error'); } }); it('should throw if aws-spa management is refused', async () => { getBucketTaggingMock.mockReturnValue((0, _testHelper.awsResolve)({ TagSet: [] })); promptMock.mockResolvedValue({ continueUpdate: false }); try { await (0, _s.confirmBucketManagement)('some bucket'); throw new Error('this test should have failed'); } catch (error) { expect(error.message).not.toEqual('this test should have failed'); } }); }); describe('syncToS3', () => { const someFiles = ['images/icons/logo.png', 'index.html', 'static/1.bbbbbb.css', 'static/1.bbbbbb.js', 'static/main.aaaaaa.js'].map(path => `some-folder/${path}`); const readRecursivelyMock = jest.spyOn(fsHelper, 'readRecursively').mockReturnValue(someFiles); const putObjectSpy = jest.spyOn(_awsServices.s3, 'putObject').mockReturnValue((0, _testHelper.awsResolve)()); const createReadStreamSpy = jest.spyOn(fs, 'createReadStream').mockImplementation(() => 'file content'); afterEach(() => { readRecursivelyMock.mockClear(); putObjectSpy.mockClear(); createReadStreamSpy.mockClear(); }); it('should call s3.putObject for each file returned by readRecursively', async () => { await (0, _s.syncToS3)('some-folder', 'some-bucket', 'static/'); expect(readRecursivelyMock).toHaveBeenCalledWith('some-folder'); expect(putObjectSpy).toHaveBeenCalledTimes(someFiles.length); for (const call of putObjectSpy.mock.calls) { expect(call[0].Bucket).toEqual('some-bucket'); expect(call[0].Key).not.toContain('some-folder'); } }); it('should set the right cache-control', async () => { await (0, _s.syncToS3)('some-folder', 'some-bucket', 'static/'); expect(readRecursivelyMock).toHaveBeenCalledWith('some-folder'); expect(putObjectSpy).toHaveBeenCalledTimes(someFiles.length); const putObjectIndex = putObjectSpy.mock.calls.find(call => call[0].Key === 'index.html'); expect(putObjectIndex).toBeDefined(); expect(putObjectIndex[0].CacheControl).toEqual('public, must-revalidate, proxy-revalidate, max-age=0'); }); }); describe(_s.upsertLifeCycleConfiguration, () => { const getBucketLifecycleConfigurationSpy = jest.spyOn(_awsServices.s3, 'getBucketLifecycleConfiguration'); const putBucketLifecycleConfigurationSpy = jest.spyOn(_awsServices.s3, 'putBucketLifecycleConfiguration'); putBucketLifecycleConfigurationSpy.mockReturnValue((0, _testHelper.awsResolve)()); beforeEach(() => jest.resetAllMocks()); it('should add lifecycle rule if missing', async () => { getBucketLifecycleConfigurationSpy.mockReturnValue((0, _testHelper.awsResolve)({ Rules: [] })); await (0, _s.upsertLifeCycleConfiguration)('some-bucket', 60); expect(logSpy).toBeCalledTimes(1); expect(logSpy.mock.calls[0][0]).toContain('Lifecycle configuration "expire-old-branches" added'); expect(putBucketLifecycleConfigurationSpy).toBeCalledTimes(1); }); it('should not add lifecycle rule if present', async () => { getBucketLifecycleConfigurationSpy.mockReturnValue((0, _testHelper.awsResolve)({ Rules: [{ ID: _s.LIFE_CYCLE_OLD_BRANCH_ID, Expiration: { Days: 60 } }] })); await (0, _s.upsertLifeCycleConfiguration)('some-bucket', 60); expect(logSpy).toBeCalledTimes(1); expect(logSpy.mock.calls[0][0]).toContain('already exists, no update'); expect(putBucketLifecycleConfigurationSpy).toBeCalledTimes(0); }); it('should update lifecycle rule if duration is not the same', async () => { getBucketLifecycleConfigurationSpy.mockReturnValue((0, _testHelper.awsResolve)({ Rules: [{ ID: _s.LIFE_CYCLE_OLD_BRANCH_ID, Expiration: { Days: 45 } }] })); await (0, _s.upsertLifeCycleConfiguration)('some-bucket', 60); expect(putBucketLifecycleConfigurationSpy).toBeCalledTimes(1); const callParams = putBucketLifecycleConfigurationSpy.mock.calls[0][0]; expect(callParams.LifecycleConfiguration?.Rules).toHaveLength(1); expect(putBucketLifecycleConfigurationSpy).toHaveBeenCalledWith(expect.objectContaining({ LifecycleConfiguration: expect.objectContaining({ Rules: expect.arrayContaining([expect.objectContaining({ ID: _s.LIFE_CYCLE_OLD_BRANCH_ID, Expiration: { Days: 60 } })]) }) })); }); it('should not modify lifecycle rules already present except if prefix is not set', async () => { getBucketLifecycleConfigurationSpy.mockReturnValue((0, _testHelper.awsResolve)({ Rules: [{ ID: 'Test' }, { ID: 'Test2', Filter: { Prefix: 'Test2' } }] })); await (0, _s.upsertLifeCycleConfiguration)('some-bucket', 60); expect(putBucketLifecycleConfigurationSpy).toHaveBeenCalledWith(expect.objectContaining({ LifecycleConfiguration: expect.objectContaining({ Rules: expect.arrayContaining([expect.objectContaining({ ID: _s.LIFE_CYCLE_OLD_BRANCH_ID }), expect.objectContaining({ ID: 'Test', Filter: { Prefix: '' } }), expect.objectContaining({ ID: 'Test2', Filter: { Prefix: 'Test2' } })]) }) })); }); }); });