aws-spa
Version:
A no-brainer script to deploy a single page app on AWS
377 lines (354 loc) • 17.4 kB
JavaScript
"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'
}
})])
})
}));
});
});
});