apisurf
Version:
Analyze API surface changes between npm package versions to catch breaking changes
181 lines (180 loc) • 8.5 kB
JavaScript
import { describe, expect, it, jest, beforeEach } from '@jest/globals';
// Create mocks
const mockFs = {
readFileSync: jest.fn()
};
const mockVm = {
createContext: jest.fn(),
runInContext: jest.fn()
};
// Mock dependencies
jest.unstable_mockModule('fs', () => mockFs);
jest.unstable_mockModule('vm', () => mockVm);
// Import after mocking
const { loadModuleWithVM } = await import('./loadModuleWithVM.js');
describe('loadModuleWithVM', () => {
beforeEach(() => {
jest.clearAllMocks();
// Mock console.warn to suppress warning messages during tests
jest.spyOn(console, 'warn').mockImplementation(() => { });
});
it('should successfully load a simple CommonJS module', () => {
const modulePath = '/test/module.js';
const moduleSource = `
exports.simpleFunction = function() { return 'hello'; };
exports.simpleValue = 42;
`;
mockFs.readFileSync.mockReturnValue(moduleSource);
const mockContext = {
module: { exports: {} },
exports: {},
require: jest.fn()
};
mockVm.createContext.mockReturnValue(mockContext);
// Simulate the module execution
mockVm.runInContext.mockImplementation((_code, context) => {
// Simulate the effect of running the CommonJS module
context.exports.simpleFunction = function () { return 'hello'; };
context.exports.simpleValue = 42;
context.module.exports = context.exports;
return context.module.exports;
});
const result = loadModuleWithVM(modulePath);
expect(mockFs.readFileSync).toHaveBeenCalledWith(modulePath, 'utf8');
expect(mockVm.createContext).toHaveBeenCalled();
expect(mockVm.runInContext).toHaveBeenCalledWith(moduleSource, mockContext, {
filename: modulePath,
timeout: 10000
});
expect(result).toBeDefined();
});
it('should handle module.exports assignment', () => {
const modulePath = '/test/default-export.js';
const moduleSource = 'function MyClass() {} module.exports = MyClass;';
mockFs.readFileSync.mockReturnValue(moduleSource);
mockVm.createContext.mockReturnValue({});
mockVm.runInContext.mockImplementation(() => {
// Simulate successful execution
});
const result = loadModuleWithVM(modulePath);
// Just verify it returns something (not null) and basic calls were made
expect(result).toBeDefined();
expect(mockFs.readFileSync).toHaveBeenCalledWith(modulePath, 'utf8');
expect(mockVm.createContext).toHaveBeenCalled();
expect(mockVm.runInContext).toHaveBeenCalled();
});
it('should return null when VM execution fails', () => {
const modulePath = '/test/failing-module.js';
const moduleSource = 'throw new Error("Module failed to load");';
mockFs.readFileSync.mockReturnValue(moduleSource);
mockVm.createContext.mockReturnValue({});
mockVm.runInContext.mockImplementation(() => {
throw new Error('Execution failed');
});
const result = loadModuleWithVM(modulePath);
expect(result).toBeNull();
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('VM execution failed while parsing unknown package'), expect.any(Error));
});
it('should return null when file reading fails', () => {
const modulePath = '/test/non-existent.js';
mockFs.readFileSync.mockImplementation(() => {
throw new Error('File not found');
});
const result = loadModuleWithVM(modulePath);
expect(result).toBeNull();
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('VM execution failed while parsing unknown package'), expect.any(Error));
});
it('should include package info in error messages when provided', () => {
const modulePath = '/test/module.js';
mockFs.readFileSync.mockImplementation(() => {
throw new Error('Read failed');
});
const result = loadModuleWithVM(modulePath, 'test-package', '1.0.0');
expect(result).toBeNull();
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('VM execution failed while parsing test-package@1.0.0'), expect.any(Error));
});
it('should set up proper sandbox environment', () => {
const modulePath = '/test/module.js';
const moduleSource = 'exports.test = true;';
mockFs.readFileSync.mockReturnValue(moduleSource);
let capturedSandbox;
mockVm.createContext.mockImplementation((sandbox) => {
capturedSandbox = sandbox;
return sandbox;
});
mockVm.runInContext.mockImplementation(() => { });
loadModuleWithVM(modulePath);
// Verify sandbox has all required properties
expect(capturedSandbox).toBeDefined();
expect(capturedSandbox.require).toBeInstanceOf(Function);
expect(capturedSandbox.module).toHaveProperty('exports');
expect(capturedSandbox.exports).toBeDefined();
expect(capturedSandbox.__filename).toBe(modulePath);
expect(capturedSandbox.__dirname).toBe('/test');
expect(capturedSandbox.process).toHaveProperty('env');
expect(capturedSandbox.console).toHaveProperty('log');
expect(capturedSandbox.Buffer).toBe(Buffer);
expect(capturedSandbox.Object).toBe(Object);
expect(capturedSandbox.global).toBe(capturedSandbox);
expect(capturedSandbox.globalThis).toBe(capturedSandbox);
});
it('should handle module requires correctly', () => {
const modulePath = '/test/module.js';
const moduleSource = 'const dep = require("dependency");';
mockFs.readFileSync.mockReturnValue(moduleSource);
let capturedRequire;
mockVm.createContext.mockImplementation((sandbox) => {
capturedRequire = sandbox.require;
return sandbox;
});
mockVm.runInContext.mockImplementation(() => { });
loadModuleWithVM(modulePath);
// Verify the require function was set up in the sandbox
expect(capturedRequire).toBeInstanceOf(Function);
// Test that require is called for relative paths
// Since we can't mock the global require, we'll just test the function exists
expect(() => capturedRequire('./relative')).not.toThrow();
});
it('should return Object.assign for object-assign package', () => {
const modulePath = '/test/react-module.js';
const moduleSource = 'const _assign = require("object-assign");';
mockFs.readFileSync.mockReturnValue(moduleSource);
let capturedRequire;
mockVm.createContext.mockImplementation((sandbox) => {
capturedRequire = sandbox.require;
return sandbox;
});
mockVm.runInContext.mockImplementation(() => { });
loadModuleWithVM(modulePath);
// Test that requiring 'object-assign' returns Object.assign
const result = capturedRequire('object-assign');
expect(result).toBe(Object.assign);
});
it('should return empty object when require fails', () => {
const modulePath = '/test/module.js';
const moduleSource = 'const dep = require("missing");';
mockFs.readFileSync.mockReturnValue(moduleSource);
let capturedRequire;
mockVm.createContext.mockImplementation((sandbox) => {
capturedRequire = sandbox.require;
return sandbox;
});
mockVm.runInContext.mockImplementation(() => { });
loadModuleWithVM(modulePath);
// The require function in sandbox should return empty object for missing modules
// This is handled by the catch block in the sandbox's require implementation
const result = capturedRequire('missing-module-that-does-not-exist');
expect(result).toEqual({});
});
it('should handle timeout correctly', () => {
const modulePath = '/test/infinite-loop.js';
const moduleSource = 'while(true) {}';
mockFs.readFileSync.mockReturnValue(moduleSource);
mockVm.createContext.mockReturnValue({});
// Check that timeout is set to 10000ms
mockVm.runInContext.mockImplementation((_code, _context, options) => {
expect(options?.timeout).toBe(10000);
});
loadModuleWithVM(modulePath);
});
});