UNPKG

@adobe/aio-lib-web

Version:

Utility tooling library to build and deploy Adobe I/O Project Firefly app static sites to CDN

577 lines (506 loc) 23.8 kB
/* Copyright 2019 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const mockS3 = { listObjectsV2: jest.fn(), deleteObjects: jest.fn(), putObject: jest.fn() } jest.mock('@aws-sdk/client-s3', () => Object({ S3: jest.fn(() => { return mockS3 }) })) const { S3 } = require('@aws-sdk/client-s3') const { vol } = global.mockFs() const path = require('path') const RemoteStorage = require('../../lib/remote-storage') describe('RemoteStorage', () => { beforeEach(() => { // resets all mock s3 functions, do not use jest.resetAllMocks() as it also resets the s3 client constructor mock mockS3.listObjectsV2.mockReset() mockS3.deleteObjects.mockReset() mockS3.putObject.mockReset() S3.mockClear() // resets the mock fs global.cleanFs(vol) }) test('Constructor should throw when missing credentials', async () => { const instantiate = () => new RemoteStorage({}) expect(instantiate.bind(this)).toThrowWithMessageContaining(['required']) }) test('Constructor initializes the S3 constructor properly using tvm credentials', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) expect(S3).toHaveBeenCalledWith({ credentials: { accessKeyId: global.fakeTVMResponse.accessKeyId, secretAccessKey: global.fakeTVMResponse.secretAccessKey, sessionToken: global.fakeTVMResponse.sessionToken, expiration: new Date(global.fakeTVMResponse.expiration) }, region: 'us-east-1', requestHandler: expect.any(Object) }) rs.bucket = global.fakeTVMResponse.Bucket }) test('Constructor initializes the S3 constructor properly using byo credentials', async () => { const rs = new RemoteStorage(global.fakeBYOCredentials) expect(S3).toHaveBeenCalledWith({ credentials: { accessKeyId: global.fakeTVMResponse.accessKeyId, secretAccessKey: global.fakeTVMResponse.secretAccessKey, sessionToken: undefined, expiration: undefined }, region: 'us-east-1', requestHandler: expect.any(Object) }) rs.bucket = global.fakeTVMResponse.Bucket }) describe('Proxy configuration', () => { const originalEnv = process.env beforeEach(() => { // Clear environment variables before each test delete process.env.https_proxy delete process.env.HTTPS_PROXY delete process.env.http_proxy delete process.env.HTTP_PROXY }) afterAll(() => { // Restore original environment process.env = originalEnv }) test('Constructor uses HTTPS_PROXY when set (uppercase)', async () => { process.env.HTTPS_PROXY = 'http://proxy.example.com:8080' // eslint-disable-next-line no-new new RemoteStorage(global.fakeTVMResponse) expect(S3).toHaveBeenCalledWith(expect.objectContaining({ requestHandler: expect.any(Object), credentials: expect.any(Object), region: 'us-east-1' })) }) test('Constructor uses https_proxy when set (lowercase)', async () => { process.env.https_proxy = 'http://proxy.example.com:3128' // eslint-disable-next-line no-new new RemoteStorage(global.fakeTVMResponse) expect(S3).toHaveBeenCalledWith(expect.objectContaining({ requestHandler: expect.any(Object), credentials: expect.any(Object), region: 'us-east-1' })) }) test('Constructor uses HTTP_PROXY when HTTPS_PROXY not set', async () => { process.env.HTTP_PROXY = 'http://proxy.example.com:8080' // eslint-disable-next-line no-new new RemoteStorage(global.fakeTVMResponse) expect(S3).toHaveBeenCalledWith(expect.objectContaining({ requestHandler: expect.any(Object), credentials: expect.any(Object), region: 'us-east-1' })) }) test('Constructor uses http_proxy when other proxy vars not set', async () => { process.env.http_proxy = 'http://proxy.example.com:3128' // eslint-disable-next-line no-new new RemoteStorage(global.fakeTVMResponse) expect(S3).toHaveBeenCalledWith(expect.objectContaining({ requestHandler: expect.any(Object), credentials: expect.any(Object), region: 'us-east-1' })) }) test('Constructor prioritizes HTTPS_PROXY over HTTP_PROXY', async () => { process.env.HTTPS_PROXY = 'http://https-proxy.example.com:8080' process.env.HTTP_PROXY = 'http://http-proxy.example.com:8080' // eslint-disable-next-line no-new new RemoteStorage(global.fakeTVMResponse) expect(S3).toHaveBeenCalledWith(expect.objectContaining({ requestHandler: expect.any(Object), credentials: expect.any(Object), region: 'us-east-1' })) }) test('Constructor prioritizes https_proxy over HTTP_PROXY', async () => { process.env.https_proxy = 'http://https-proxy.example.com:3128' process.env.HTTP_PROXY = 'http://http-proxy.example.com:8080' // eslint-disable-next-line no-new new RemoteStorage(global.fakeTVMResponse) expect(S3).toHaveBeenCalledWith(expect.objectContaining({ requestHandler: expect.any(Object), credentials: expect.any(Object), region: 'us-east-1' })) }) test('Constructor always includes requestHandler with ProxyAgent', async () => { // eslint-disable-next-line no-new new RemoteStorage(global.fakeTVMResponse) expect(S3).toHaveBeenCalledWith({ credentials: expect.any(Object), region: 'us-east-1', requestHandler: expect.any(Object) }) // ProxyAgent handles proxy detection automatically via proxy-from-env const s3CallArgs = S3.mock.calls[S3.mock.calls.length - 1][0] expect(s3CallArgs).toHaveProperty('requestHandler') }) }) test('folderExists missing prefix', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) await expect(rs.folderExists()).rejects.toEqual(expect.objectContaining({ message: 'prefix must be a valid string' })) }) test('emptyFolder missing prefix', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) await expect(rs.emptyFolder()).rejects.toEqual(expect.objectContaining({ message: 'prefix must be a valid string' })) }) test('uploadFile missing prefix', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) await expect(rs.uploadFile()).rejects.toEqual(expect.objectContaining({ message: 'prefix must be a valid string' })) }) test('uploadDir missing prefix', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) await expect(rs.uploadDir()).rejects.toEqual(expect.objectContaining({ message: 'prefix must be a valid string' })) }) test('folderExists should return false if there are no files', async () => { mockS3.listObjectsV2.mockResolvedValue({ KeyCount: 0 }) const rs = new RemoteStorage(global.fakeTVMResponse) expect((await rs.folderExists('fakeprefix'))).toBe(false) expect(mockS3.listObjectsV2).toHaveBeenCalledWith({ Bucket: 'fake-bucket', Prefix: 'fakeprefix' }) }) test('folderExists should return true if there are files', async () => { mockS3.listObjectsV2.mockResolvedValue({ KeyCount: 1 }) const rs = new RemoteStorage(global.fakeTVMResponse) expect((await rs.folderExists('fakeprefix'))).toBe(true) }) test('emptyFolder should not throw if there are no files', async () => { mockS3.listObjectsV2.mockResolvedValue({ KeyCount: 0 }) const rs = new RemoteStorage(global.fakeTVMResponse) expect(rs.emptyFolder.bind(rs, 'fakeprefix')).not.toThrow() }) test('emptyFolder should not call S3#deleteObjects if already empty', async () => { mockS3.listObjectsV2.mockResolvedValue({ KeyCount: 0 }) const rs = new RemoteStorage(global.fakeTVMResponse) await rs.emptyFolder('fakeprefix') expect(mockS3.deleteObjects).toHaveBeenCalledTimes(0) }) test('emptyFolder should call S3#deleteObjects with correct parameters with one file', async () => { const content = [{ Key: 'fakeprefix/index.html' }] mockS3.listObjectsV2.mockResolvedValue({ KeyCount: 1, Contents: content }) const rs = new RemoteStorage(global.fakeTVMResponse) await rs.emptyFolder('fakeprefix') expect(mockS3.deleteObjects).toHaveBeenCalledWith({ Bucket: 'fake-bucket', Delete: { Objects: content } }) }) test('emptyFolder should call S3#deleteObjects with correct parameters with multiple files', async () => { const content = [{ Key: 'fakeprefix/index.html' }, { Key: 'fakeprefix/index.css' }, { Key: 'fakeprefix/index.css' }] mockS3.listObjectsV2.mockResolvedValue({ KeyCount: 3, Contents: content }) const rs = new RemoteStorage(global.fakeTVMResponse) await rs.emptyFolder('fakeprefix') expect(mockS3.deleteObjects).toHaveBeenCalledWith({ Bucket: 'fake-bucket', Delete: { Objects: content } }) }) test('emptyFolder should call S3#deleteObjects multiple time if listObjects is truncated', async () => { const content = [{ Key: 'fakeprefix/index.html' }, { Key: 'fakeprefix/index.css' }, { Key: 'fakeprefix/index.js' }] let iterations = 2 mockS3.listObjectsV2.mockImplementation(() => { const res = { Contents: [content[iterations]], IsTruncated: iterations > 0 } iterations-- return Promise.resolve(res) }) const rs = new RemoteStorage(global.fakeTVMResponse) await rs.emptyFolder('fakeprefix') expect(mockS3.deleteObjects).toHaveBeenCalledWith({ Bucket: 'fake-bucket', Delete: { Objects: [content[0]] } }) expect(mockS3.deleteObjects).toHaveBeenCalledWith({ Bucket: 'fake-bucket', Delete: { Objects: [content[1]] } }) expect(mockS3.deleteObjects).toHaveBeenCalledWith({ Bucket: 'fake-bucket', Delete: { Objects: [content[2]] } }) }) test('uploadFile should call S3#upload with the correct parameters', async () => { global.addFakeFiles(vol, 'fakeDir', { 'index.js': 'fake content' }) const rs = new RemoteStorage(global.fakeTVMResponse) const fakeConfig = global.fakeConfig await rs.uploadFile('fakeDir/index.js', 'fakeprefix', fakeConfig, 'fakeDir') const body = Buffer.from('fake content', 'utf8') expect(mockS3.putObject).toHaveBeenCalledWith(expect.objectContaining({ Bucket: 'fake-bucket', Key: 'fakeprefix/index.js', Body: body, ContentType: 'application/javascript' })) }) test('uploadFile should call S3#upload with the correct parameters and slash-prefix', async () => { global.addFakeFiles(vol, 'fakeDir', { 'index.js': 'fake content' }) const rs = new RemoteStorage(global.fakeTVMResponse) const fakeConfig = global.fakeConfig await rs.uploadFile('fakeDir/index.js', '/slash-prefix', fakeConfig, 'fakeDir') const body = Buffer.from('fake content', 'utf8') expect(mockS3.putObject).toHaveBeenCalledWith(expect.objectContaining({ Bucket: 'fake-bucket', Key: '/slash-prefix/index.js', Body: body, ContentType: 'application/javascript' })) }) test('uploadFile S3#upload with an unknown Content-Type', async () => { global.addFakeFiles(vol, 'fakeDir', { 'index.mst': 'fake content' }) const rs = new RemoteStorage(global.fakeTVMResponse) const fakeConfig = {} await rs.uploadFile('fakeDir/index.mst', 'fakeprefix', fakeConfig, 'fakeDir') const body = Buffer.from('fake content', 'utf8') expect(mockS3.putObject).toHaveBeenCalledWith(expect.objectContaining({ Bucket: 'fake-bucket', Key: 'fakeprefix/index.mst', Body: body })) expect(mockS3.putObject.mock.calls[0][0]).not.toHaveProperty('ContentType') }) test('uploadDir should call S3#upload one time per file', async () => { await global.addFakeFiles(vol, 'fakeDir', ['index.js', 'index.css', 'index.html']) const rs = new RemoteStorage(global.fakeTVMResponse) await rs.uploadDir('fakeDir', 'fakeprefix', global.fakeConfig) expect(mockS3.putObject).toHaveBeenCalledTimes(3) }) test('uploadDir should call a callback once per uploaded file', async () => { await global.addFakeFiles(vol, 'fakeDir', ['index.js', 'index.css', 'index.html', 'test/i.js']) const cbMock = jest.fn() const rs = new RemoteStorage(global.fakeTVMResponse) await rs.uploadDir('fakeDir', 'fakeprefix', global.fakeConfig, cbMock) expect(cbMock).toHaveBeenCalledTimes(4) }) test('cachecontrol string for html', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) const response = rs._getCacheControlConfig('text/html', global.fakeConfig.app) expect(response).toBe('s-maxage=0, max-age=60') }) test('cachecontrol string for JS', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) const response = rs._getCacheControlConfig('application/javascript', global.fakeConfig.app) expect(response).toBe('s-maxage=0, max-age=604800') }) test('cachecontrol string for CSS', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) const response = rs._getCacheControlConfig('text/css', global.fakeConfig.app) expect(response).toBe('s-maxage=0, max-age=604800') }) test('cachecontrol string for Image', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) const response = rs._getCacheControlConfig('image/jpeg', global.fakeConfig.app) expect(response).toBe('s-maxage=0, max-age=604800') }) test('cachecontrol string for default', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) const response = rs._getCacheControlConfig('application/pdf', global.fakeConfig.app) expect(response).toBe('s-maxage=0') }) // response header tests test('get response header from config with multiple rules', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) const newConfig = global.configWithModifiedWeb(global.fakeConfig, { 'response-headers': { '/*': { testHeader: 'generic-header' }, '/testFolder/*': { testHeader: 'folder-header' }, '/testFolder/*.js': { testHeader: 'all-js-file-in-folder-header' }, '/test.js': { testHeader: 'specific-file-header' } } }) const folderPath1 = 'testFolder' + path.sep + 'index.html' const folderPath2 = 'testFolder' + path.sep + 'test.js' await global.addFakeFiles(vol, 'fakeDir', ['index.html', 'test.js', folderPath1, folderPath2]) const files = await rs.walkDir('fakeDir') const fakeDistRoot = path.parse(files[0]).dir const expectedValMap = { 'index.html': { 'adp-testHeader': 'generic-header' }, 'test.js': { 'adp-testHeader': 'specific-file-header' } } expectedValMap[folderPath1] = { 'adp-testHeader': 'folder-header' } expectedValMap[folderPath2] = { 'adp-testHeader': 'all-js-file-in-folder-header' } files.forEach(f => { const fileName = f.replace(path.join(fakeDistRoot, path.sep), '') const response = rs.getResponseHeadersForFile(f, fakeDistRoot, newConfig) const expected = expectedValMap[fileName] expect(response).toStrictEqual(expected) }) }) test('get response header for folder based path rules', async () => { // setup files and paths const rs = new RemoteStorage(global.fakeTVMResponse) const folderPath1 = 'css' + path.sep + 'ui.css' const folderPath2 = 'scripts' + path.sep + 'test.js' const folderPath3 = 'images' + path.sep + 'image.png' const folderPath4 = 'images' + path.sep + 'thumbnails' + path.sep + 'test.jpeg' await global.addFakeFiles(vol, 'fakeDir', ['index.html', 'test.js', folderPath1, folderPath2, folderPath3, folderPath4]) const files = await rs.walkDir('fakeDir') const fakeDistRoot = path.parse(files[0]).dir // create a config of rules for files in specific folder const newConfig = global.configWithModifiedWeb(global.fakeConfig, { 'response-headers': { '/*': { testHeader: 'generic-header' }, '/css/*': { testHeader: 'all-files-in-css-folder-header' }, '/scripts/*': { testHeader: 'all-files-in-js-folder-header' }, '/images/*': { testHeader: 'all-files-in-images-folder-header' } } }) // set the expectation const expectedValMap = { 'index.html': { 'adp-testHeader': 'generic-header' }, 'test.js': { 'adp-testHeader': 'generic-header' } } expectedValMap[folderPath1] = { 'adp-testHeader': 'all-files-in-css-folder-header' } expectedValMap[folderPath2] = { 'adp-testHeader': 'all-files-in-js-folder-header' } expectedValMap[folderPath3] = { 'adp-testHeader': 'all-files-in-images-folder-header' } expectedValMap[folderPath4] = { 'adp-testHeader': 'all-files-in-images-folder-header' } // check header application per file files.forEach(f => { const fileName = f.replace(path.join(fakeDistRoot, path.sep), '') const response = rs.getResponseHeadersForFile(f, fakeDistRoot, newConfig) const expected = expectedValMap[fileName] expect(response).toStrictEqual(expected) }) }) test('get response header for specific file based path rules', async () => { // setup files and paths const rs = new RemoteStorage(global.fakeTVMResponse) const folderPath1 = 'css' + path.sep + 'ui.css' const folderPath2 = 'scripts' + path.sep + 'test.js' const folderPath3 = 'images' + path.sep + 'image.png' await global.addFakeFiles(vol, 'fakeDir', ['index.html', 'test.js', folderPath1, folderPath2, folderPath3]) const files = await rs.walkDir('fakeDir') const fakeDistRoot = path.parse(files[0]).dir // create a config of rules for spcefic files which overrider folder rules const newConfig = global.configWithModifiedWeb(global.fakeConfig, { 'response-headers': { '/*': { testHeader: 'generic-header' }, '/css/*': { testHeader: 'all-files-in-css-folder-header' }, '/css/ui.css': { testHeader: 'specific-css-file-header' // overrides previous css folder rule }, '/scripts/*': { testHeader: 'all-files-in-js-folder-header' }, '/scripts/test.js': { testHeader: 'specific-js-file-header' // overrides previous js folder rule }, '/images/*': { testHeader: 'all-files-in-images-folder-header' }, '/images/image.png': { testHeader: 'specific-image-file-header' // overrides previous image folder rule } } }) // set the expectation const expectedValMap = { 'index.html': { 'adp-testHeader': 'generic-header' }, 'test.js': { 'adp-testHeader': 'generic-header' } } expectedValMap[folderPath1] = { 'adp-testHeader': 'specific-css-file-header' } expectedValMap[folderPath2] = { 'adp-testHeader': 'specific-js-file-header' } expectedValMap[folderPath3] = { 'adp-testHeader': 'specific-image-file-header' } // check header application per file files.forEach(f => { const fileName = f.replace(path.join(fakeDistRoot, path.sep), '') const response = rs.getResponseHeadersForFile(f, fakeDistRoot, newConfig) const expected = expectedValMap[fileName] expect(response).toStrictEqual(expected) }) }) test('get response header for file extension based path rules', async () => { // setup files and paths const rs = new RemoteStorage(global.fakeTVMResponse) const folderPath1 = 'css' + path.sep + 'ui.css' const folderPath2 = 'scripts' + path.sep + 'test.js' const folderPath3 = 'images' + path.sep + 'image.png' await global.addFakeFiles(vol, 'fakeDir', ['index.html', 'test.js', folderPath1, folderPath2, folderPath3]) const files = await rs.walkDir('fakeDir') const fakeDistRoot = path.parse(files[0]).dir // create a config of rules for spcefic files which overrider folder rules const newConfig = global.configWithModifiedWeb(global.fakeConfig, { 'response-headers': { '/*': { testHeader: 'generic-header' }, '/*.css': { testHeader: 'all-css-files-header' }, '/*.js': { testHeader: 'all-js-files-header' }, '/*.png': { testHeader: 'all-png-files-header' } } }) // set the expectation const expectedValMap = { 'index.html': { 'adp-testHeader': 'generic-header' }, 'test.js': { 'adp-testHeader': 'all-js-files-header' } } expectedValMap[folderPath1] = { 'adp-testHeader': 'all-css-files-header' } expectedValMap[folderPath2] = { 'adp-testHeader': 'all-js-files-header' } expectedValMap[folderPath3] = { 'adp-testHeader': 'all-png-files-header' } // check header application per file files.forEach(f => { const fileName = f.replace(path.join(fakeDistRoot, path.sep), '') const response = rs.getResponseHeadersForFile(f, fakeDistRoot, newConfig) const expected = expectedValMap[fileName] expect(response).toStrictEqual(expected) }) }) test('get response header with invalid header name', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) const newConfig = global.configWithModifiedWeb(global.fakeConfig, { 'response-headers': { '/*': { 無効な名前: 'generic-header' } } }) const fakeDistRoot = '/fake/web-prod/' expect(() => rs.getResponseHeadersForFile(fakeDistRoot + 'index.html', fakeDistRoot, newConfig)).toThrowWithMessageContaining( '[WebLib:ERROR_INVALID_HEADER_NAME] `無効な名前` is not a valid response header name') }) test('get response header with invalid header value', async () => { const rs = new RemoteStorage(global.fakeTVMResponse) const newConfig = global.configWithModifiedWeb(global.fakeConfig, { 'response-headers': { '/*': { testHeader: '無効な値' } } }) const fakeDistRoot = '/fake/web-prod/' expect(() => rs.getResponseHeadersForFile(fakeDistRoot + 'index.html', fakeDistRoot, newConfig)).toThrowWithMessageContaining( '[WebLib:ERROR_INVALID_HEADER_VALUE] `無効な値` is not a valid response header value for `testHeader`') }) test('Metadata check for response headers', async () => { global.addFakeFiles(vol, 'fakeDir', { 'index.js': 'fake content' }) const rs = new RemoteStorage(global.fakeTVMResponse) const files = await rs.walkDir('fakeDir') const fakeDistRoot = files[0].substring(0, files[0].indexOf('index.js')) const newConfig = global.configWithModifiedWeb(global.fakeConfig, { 'response-headers': { '/*': { testHeader: 'generic-header' } } }) // const fakeConfig = {} await rs.uploadFile('fakeDir/index.js', 'fakeprefix', newConfig, fakeDistRoot) const body = Buffer.from('fake content', 'utf8') const expected = { Bucket: 'fake-bucket', Key: 'fakeprefix/index.js', Body: body, ContentType: 'application/javascript', Metadata: { 'adp-testHeader': 'generic-header' } } expect(mockS3.putObject).toHaveBeenCalledWith(expect.objectContaining(expected)) }) })