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
Browse files Browse the repository at this point in the history
  • Loading branch information
bishnubista committed Aug 21, 2023
1 parent b491dbf commit 7562b15
Show file tree
Hide file tree
Showing 5 changed files with 390 additions and 2 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
237 changes: 237 additions & 0 deletions __tests__/server/utils/cacheCDNModules.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import fs from 'fs';

import {
getUserHomeDirectory,
getCachedModules,
writeToCache,
removeDuplicatedModules,
showCacheInfo,
setupCacheFile,
cacheFileName,
oneAppDirectoryName,
oneAppDirectoryPath,
oneAppModuleCachePath,
} from '../../../src/server/utils/cacheCDNModules';

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

describe('Cache module utils', () => {
let logSpy;
let errorSpy;

beforeEach(() => {
logSpy = jest.spyOn(console, 'log');
errorSpy = jest.spyOn(console, 'error');
process.env.HOME = '';
});

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

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

describe('showCacheInfo', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should display the cache info when there is no error', () => {
const mockStats = {
size: 1048576 * 5, // 5 MB
};

fs.stat.mockImplementation((_path, callback) => {
callback(null, mockStats);
});

showCacheInfo();

expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('CACHE INFORMATION'));
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('File size of .one-app-module-cache: 5.00'));
expect(errorSpy).not.toHaveBeenCalled();
});

it('should display an error when there is an error checking file stats', () => {
const mockError = new Error('Test error');

fs.stat.mockImplementation((_path, callback) => {
callback(mockError, null);
});

showCacheInfo();

expect(errorSpy).toHaveBeenCalledWith('There was error checking file stat', mockError);
});
});

describe('setupCacheFile', () => {
beforeEach(() => {
jest.clearAllMocks();
fs.mkdir.mockRestore();
fs.writeFileSync.mockRestore();
});

it('should log success message when directory and file are created', () => {
fs.mkdir.mockImplementationOnce((_filePath, _options, cb) => cb(null));
fs.writeFileSync.mockImplementationOnce(() => ({}));

setupCacheFile();

expect(logSpy).toHaveBeenCalledWith(`Successfully created ${oneAppDirectoryPath}`);
expect(logSpy).toHaveBeenCalledWith(`Creating ${cacheFileName}`);
expect(logSpy).toHaveBeenCalledWith(`${cacheFileName} created successfully on ${oneAppModuleCachePath}`);
});

it('should log error when unable to create a directory', () => {
fs.mkdir.mockImplementationOnce((_filePath, _options, cb) => cb(new Error('Error creating directory')));
fs.writeFileSync.mockImplementationOnce(() => {});

setupCacheFile();
expect(errorSpy).toHaveBeenCalledWith(`There was error creating ${oneAppDirectoryName} directory`);
fs.mkdir.mockRestore();
});

it('should log error when unable to create a file', () => {
const error = new Error('Cannot create file');

fs.mkdir.mockImplementationOnce((_filePath, _options, cb) => cb(null));
fs.writeFileSync.mockImplementationOnce(() => { throw error; });
setupCacheFile();
expect(errorSpy).toHaveBeenCalledWith(`Error creating ${cacheFileName} on ${oneAppModuleCachePath}, \n${error}`);
});
});

describe('getCachedModules', () => {
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);
fs.mkdir.mockImplementationOnce((_filePath, _options, cb) => cb(null));
fs.writeFileSync.mockImplementationOnce(() => {});

const result = getCachedModules();

expect(logSpy).toHaveBeenCalledWith(`Successfully created ${oneAppDirectoryPath}`);
expect(logSpy).toHaveBeenCalledWith(`${cacheFileName} created successfully on ${oneAppModuleCachePath}`);
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 @@ -20,9 +20,18 @@ 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/cacheCDNModules';

jest.mock('node-fetch');

jest.mock('../../../src/server/utils/cacheCDNModules', () => ({
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 @@ -33,6 +42,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 @@ -140,6 +150,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 @@ -547,6 +558,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('.js');
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 7562b15

Please sign in to comment.