Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
feat(cache-module): implemented cached modules (#1094)
Browse files Browse the repository at this point in the history
Co-authored-by: Matthew Mallimo <matthew.c.mallimo@aexp.com>
  • Loading branch information
bishnubista and Matthew-Mallimo authored Sep 11, 2023
1 parent a1d73b0 commit 84838a8
Show file tree
Hide file tree
Showing 5 changed files with 414 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ exports[`one-app-dev-cdn module-map.json uses the local map overriding the cdn u
}
`;

exports[`one-app-dev-cdn modules gets remote modules from cached data if incoming url is matching: module map response 1`] = `"{"key":"not-used-in-development","modules":{"module-b":{"node":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.node.js","integrity":"123"},"browser":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.browser.js","integrity":"234"},"legacyBrowser":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.legacy.browser.js","integrity":"345"}}}}"`;

exports[`one-app-dev-cdn modules gets remote modules: module map response 1`] = `"{"key":"not-used-in-development","modules":{"module-b":{"node":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.node.js","integrity":"123"},"browser":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.browser.js","integrity":"234"},"legacyBrowser":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.legacy.browser.js","integrity":"345"}}}}"`;

exports[`one-app-dev-cdn modules returns a 404 if a request for something not known as a module from the module map comes in: module map response 1`] = `"{"key":"not-used-in-development","modules":{"module-b":{"node":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.node.js","integrity":"123"},"browser":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.browser.js","integrity":"234"},"legacyBrowser":{"url":"http://localhost:3001/static/cdn/module-b/1.0.0/module-b.legacy.browser.js","integrity":"345"}}}}"`;
Expand Down
251 changes: 251 additions & 0 deletions __tests__/server/utils/cdnCache.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/*
* Copyright 2023 American Express Travel Related Services Company, Inc.
*
* Licensed 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 CONDITIONS OF ANY KIND,either express
* or implied. See the License for the specific language governing permissions and limitations
* under the License.
*/

import fs, { promises as fsPromises } from 'fs';
import chalk from 'chalk';
import {
getUserHomeDirectory,
showCacheInfo,
setupCacheFile,
getCachedModules,
writeToCache,
removeDuplicatedModules,
cacheFileName,
oneAppDirectoryName,
oneAppDirectoryPath,
oneAppModuleCachePath,
} from '../../../src/server/utils/cdnCache';

jest.mock('fs', () => {
const actualFsModule = jest.requireActual('fs');
const actualFsPromisesModule = jest.requireActual('fs/promises');
return {
...actualFsModule,
existsSync: jest.spyOn(actualFsModule, 'existsSync'),
readFileSync: jest.spyOn(actualFsModule, 'readFileSync'),
...actualFsPromisesModule,
stat: jest.spyOn(actualFsPromisesModule, 'stat'),
mkdir: jest.spyOn(actualFsPromisesModule, 'mkdir'),
writeFile: jest.spyOn(actualFsPromisesModule, 'writeFile'),
};
});

jest.mock('chalk', () => ({
bold: {
cyanBright: jest.fn((text) => text),
greenBright: jest.fn((text) => text),
redBright: jest.fn((text) => text),
},
}));

describe('cacheUtils', () => {
let logSpy;
let errorSpy;
beforeEach(() => {
logSpy = jest.spyOn(console, 'log');
errorSpy = jest.spyOn(console, 'error');
});

afterEach(() => {
logSpy.mockRestore();
errorSpy.mockRestore();
jest.clearAllMocks();
});

it('should get USERPROFILE for windows user', () => {
delete process.env.HOME;
process.env.USERPROFILE = 'Users/windows';
expect(getUserHomeDirectory()).toBe('Users/windows');
});

describe('showCacheInfo', () => {
it('showCacheInfo should log cache information', async () => {
const mockStats = {
size: 1024 * 1024 * 5, // 5 MB
};
fsPromises.stat.mockResolvedValue(mockStats);

await showCacheInfo();

expect(fsPromises.stat).toHaveBeenCalledWith(oneAppModuleCachePath);
expect(chalk.bold.cyanBright).toHaveBeenCalledTimes(2);
expect(chalk.bold.redBright).toHaveBeenCalledWith('CACHE INFORMATION');
expect(chalk.bold.greenBright).toHaveBeenCalledWith('5.00', 'MB');
});

it('showCacheInfo should handle error', async () => {
const expectedError = new Error('File not found');
fsPromises.stat.mockRejectedValue(expectedError);

await showCacheInfo();

expect(fsPromises.stat).toHaveBeenCalledWith(oneAppModuleCachePath);
expect(logSpy).not.toHaveBeenCalled();
expect(errorSpy).toHaveBeenCalledWith('There was error checking file stat', expectedError);
});
});

describe('setupCacheFile', () => {
it('setupCacheFile should create cache directory and file', async () => {
fsPromises.mkdir.mockResolvedValueOnce();
fsPromises.writeFile.mockResolvedValueOnce();
await setupCacheFile();

expect(fsPromises.mkdir).toHaveBeenCalledWith(oneAppDirectoryPath, { recursive: true });
expect(logSpy).toHaveBeenCalledWith(`Successfully created ${oneAppDirectoryPath}`);
expect(logSpy).toHaveBeenCalledWith(`Creating ${cacheFileName}`);
expect(fsPromises.writeFile).toHaveBeenCalledWith(
oneAppModuleCachePath,
JSON.stringify('{}')
);
expect(logSpy).toHaveBeenCalledWith(`${cacheFileName} created successfully on ${oneAppModuleCachePath}`);
expect(errorSpy).not.toHaveBeenCalled();
});

it('setupCacheFile should handle error when creating cache file', async () => {
const expectedError = new Error('Failed to write file');
fsPromises.mkdir.mockResolvedValueOnce();
fsPromises.writeFile.mockRejectedValueOnce(expectedError);
await setupCacheFile();
expect(fsPromises.mkdir).toHaveBeenCalledWith(oneAppDirectoryPath, { recursive: true });
expect(errorSpy).toHaveBeenCalledWith(`Error creating ${cacheFileName} on ${oneAppModuleCachePath}, \n${expectedError}`);
});

it('setupCacheFile should handle error when creating cache directory', async () => {
const expectedError = new Error('Failed to create directory');
fsPromises.mkdir.mockRejectedValue(expectedError);

await setupCacheFile();
expect(fsPromises.mkdir).toHaveBeenCalledWith(oneAppDirectoryPath, { recursive: true });
expect(errorSpy).toHaveBeenCalledWith(`There was error creating ${oneAppDirectoryName} directory`);
});
});

describe('getCachedModules', () => {
beforeAll(() => {
fsPromises.stat.mockResolvedValue('');
fsPromises.mkdir.mockResolvedValue();
fsPromises.writeFile.mockResolvedValue();
});

it('should return an empty object if the cache file does not exist', () => {
fs.existsSync.mockImplementationOnce(() => false);
const result = getCachedModules();
expect(result).toEqual({});
});

it('should create a new cache file and return an empty object if the cache file does not exist', () => {
fs.existsSync.mockImplementationOnce(() => false);
const result = getCachedModules();
expect(result).toEqual({});
});

it('should return an empty object if the cache file contains invalid JSON', () => {
const invalidJSON = 'invalid JSON';
fs.existsSync.mockImplementationOnce(() => true);
fs.readFileSync.mockImplementationOnce(() => invalidJSON);

const result = getCachedModules();
let error;
try {
JSON.parse(invalidJSON);
} catch (err) {
error = err;
}
expect(errorSpy).toHaveBeenCalledWith('Could not parse JSON content', error);
expect(result).toEqual({});
});

it('should return the content of the cache file as a JSON object if the cache file exists and contains valid JSON', () => {
const validJSON = '{"module":"test"}';
fs.existsSync.mockImplementationOnce(() => true);
fs.readFileSync.mockImplementationOnce(() => validJSON);
const result = getCachedModules();
expect(result).toEqual(JSON.parse(validJSON));
});
});

describe('writeToCache', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('should set content on cache after a delay', () => {
fs.writeFile.mockImplementation((_filePath, _content, callback) => callback(null));

const content = { module: 'test' };
writeToCache(content);

expect(fs.writeFile).not.toHaveBeenCalled();

jest.runAllTimers();

expect(fs.writeFile).toHaveBeenCalled();
expect(fs.writeFile.mock.calls[0][1]).toBe(JSON.stringify(content, null, 2));
});

it('should handle error when writing to file fails', () => {
const error = new Error('write error');
fs.writeFile.mockImplementation((_filePath, _content, callback) => callback(error));

const content = { module: 'test' };
writeToCache(content);

jest.runAllTimers();

expect(fs.writeFile).toHaveBeenCalled();
expect(logSpy).toHaveBeenCalledWith(`There was an error updating content \n ${error}`);
});
});

describe('removeDuplicatedModules', () => {
it('removes the matching modules from cachedModules', () => {
const url = '/somepath/moduleA/someotherpath';
const cachedModules = {
'/path/to/moduleA/1': 'data',
'/path/to/moduleA/2': 'data',
'/path/to/moduleB/1': 'data',
};
const moduleNames = ['moduleA', 'moduleB', 'moduleC'];

const result = removeDuplicatedModules(url, cachedModules, moduleNames);

expect(result).toEqual({
'/path/to/moduleB/1': 'data',
});

expect(logSpy).toHaveBeenCalledWith('Deleted /path/to/moduleA/1 from cache');
expect(logSpy).toHaveBeenCalledWith('Deleted /path/to/moduleA/2 from cache');
});

it('returns cachedModules unchanged if no module matches', () => {
const url = '/somepath/moduleX/someotherpath';
const cachedModules = {
'/path/to/moduleA/1': 'data',
'/path/to/moduleA/2': 'data',
'/path/to/moduleB/1': 'data',
};
const moduleNames = ['moduleA', 'moduleB', 'moduleC'];

const result = removeDuplicatedModules(url, cachedModules, moduleNames);

expect(result).toEqual(cachedModules);
expect(logSpy).not.toHaveBeenCalled();
});
});
});
37 changes: 37 additions & 0 deletions __tests__/server/utils/devCdnFactory.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ import path from 'path';
import mkdirp from 'mkdirp';
import ProxyAgent from 'proxy-agent';
import oneAppDevCdn from '../../../src/server/utils/devCdnFactory';
import { removeDuplicatedModules } from '../../../src/server/utils/cdnCache';

jest.mock('node-fetch');
jest.mock('pino');

jest.mock('../../../src/server/utils/cdnCache', () => ({
getCachedModules: jest.fn(() => ({
'/cdn/module-b/1.0.0/module-c.node.js': 'console.log("c");',
})),
writeToCache: jest.fn(() => ({})),
removeDuplicatedModules: jest.fn(() => ({})),
}));

const pathToStubs = path.join(__dirname, 'stubs');
const pathToCache = path.join(__dirname, '..', '.cache');
const mockLocalDevPublicPath = path.join(pathToStubs, 'public');
Expand All @@ -35,6 +44,7 @@ describe('one-app-dev-cdn', () => {
jest.spyOn(console, 'warn');
jest.spyOn(console, 'log');
jest.spyOn(console, 'error');

const defaultLocalMap = {
key: 'not-used-in-development',
modules: {
Expand Down Expand Up @@ -142,6 +152,7 @@ describe('one-app-dev-cdn', () => {
},
},
};
removeDuplicatedModules.mockImplementation(() => ({}));
fetch.mockImplementation((url) => Promise.reject(new Error(`no mock for ${url} set up`)));
});

Expand Down Expand Up @@ -549,6 +560,32 @@ describe('one-app-dev-cdn', () => {
]);
});

it('gets remote modules from cached data if incoming url is matching', async () => {
expect.assertions(6);
const fcdn = setupTest({
useLocalModules: false,
appPort: 3000,
remoteModuleMapUrl: 'https://example.com/module-map.json',
});
fetch.mockReturnJsonOnce(defaultRemoteMap);
fetch.mockReturnFileOnce('console.log("a");');

const moduleMapResponse = await fcdn.inject()
.get('/module-map.json');

expect(moduleMapResponse.statusCode).toBe(200);
expect(moduleMapResponse.headers['content-type']).toMatch(/^application\/json/);
expect(
sanitizeModuleMapForSnapshot(moduleMapResponse.body)
).toMatchSnapshot('module map response');

const moduleResponse = await fcdn.inject()
.get('/cdn/module-b/1.0.0/module-c.node.js');
expect(moduleResponse.statusCode).toBe(200);
expect(moduleResponse.headers['content-type']).toMatch('application/json');
expect(moduleResponse.body).toBe('console.log("c");');
});

it('returns a 404 if a request for something not known as a module from the module map comes in', async () => {
expect.assertions(5);

Expand Down
Loading

0 comments on commit 84838a8

Please sign in to comment.