import { EnvironmentStub } from './../stubs/EnvironmentStub'; import { OperatingSystem } from '@/domain/OperatingSystem'; import 'mocha'; import { expect } from 'chai'; import { runCodeAsync } from '@/infrastructure/CodeRunner'; describe('CodeRunner', () => { describe('runCodeAsync', () => { it('creates temporary directory recursively', async () => { // arrange const expectedDir = 'expected-dir'; const folderName = 'privacy.sexy'; const context = new TestContext(); context.mocks.os.setupTmpdir('tmp'); context.mocks.path.setupJoin(expectedDir, 'tmp', folderName); // act await context .withFolderName(folderName) .runCodeAsync(); // assert expect(context.mocks.fs.mkdirHistory.length).to.equal(1); expect(context.mocks.fs.mkdirHistory[0].isRecursive).to.equal(true); expect(context.mocks.fs.mkdirHistory[0].path).to.equal(expectedDir); }); it('creates a file with expected code and path', async () => { // arrange const expectedCode = 'expected-code'; const expectedFilePath = 'expected-file-path'; const extension = '.sh'; const expectedName = `run.${extension}`; const folderName = 'privacy.sexy'; const context = new TestContext(); context.mocks.os.setupTmpdir('tmp'); context.mocks.path.setupJoin('folder', 'tmp', folderName); context.mocks.path.setupJoin(expectedFilePath, 'folder', expectedName); // act await context .withCode(expectedCode) .withFolderName(folderName) .withExtension(extension) .runCodeAsync(); // assert expect(context.mocks.fs.writeFileHistory.length).to.equal(1); expect(context.mocks.fs.writeFileHistory[0].data).to.equal(expectedCode); expect(context.mocks.fs.writeFileHistory[0].path).to.equal(expectedFilePath); }); it('set file permissions as expected', async () => { // arrange const expectedMode = '755'; const expectedFilePath = 'expected-file-path'; const extension = '.sh'; const expectedName = `run.${extension}`; const folderName = 'privacy.sexy'; const context = new TestContext(); context.mocks.os.setupTmpdir('tmp'); context.mocks.path.setupJoin('folder', 'tmp', folderName); context.mocks.path.setupJoin(expectedFilePath, 'folder', expectedName); // act await context .withFolderName(folderName) .withExtension(extension) .runCodeAsync(); // assert expect(context.mocks.fs.chmodCallHistory.length).to.equal(1); expect(context.mocks.fs.chmodCallHistory[0].mode).to.equal(expectedMode); expect(context.mocks.fs.chmodCallHistory[0].path).to.equal(expectedFilePath); }); describe('executes as expected', () => { // arrange const filePath = 'expected-file-path'; const testData = [ { os: OperatingSystem.Windows, expected: filePath, }, { os: OperatingSystem.macOS, expected: `open -a Terminal.app ${filePath}`, }]; for (const data of testData) { it(`returns ${data.expected} on ${OperatingSystem[data.os]}`, async () => { const context = new TestContext(); context.mocks.os.setupTmpdir('non-important-temp-dir-name'); context.mocks.path.setupJoinSequence('non-important-folder-name', filePath); context.withOs(data.os); // act await context .withOs(data.os) .runCodeAsync(); // assert expect(context.mocks.child_process.executionHistory.length).to.equal(1); expect(context.mocks.child_process.executionHistory[0]).to.equal(data.expected); }); } }); it('runs in expected order', async () => { // arrange const expectedOrder = [ NodeJsCommand.mkdir, NodeJsCommand.writeFile, NodeJsCommand.chmod ]; const context = new TestContext(); context.mocks.os.setupTmpdir('non-important-temp-dir-name'); context.mocks.path.setupJoinSequence('non-important-folder-name1', 'non-important-folder-name2'); // act await context.runCodeAsync(); // assert const actualOrder = context.mocks.commandHistory.filter((command) => expectedOrder.includes(command)); expect(expectedOrder).to.deep.equal(actualOrder); }); }); }); class TestContext { public mocks = getNodeJsMocks(); private code: string = 'code'; private folderName: string = 'folderName'; private fileExtension: string = 'fileExtension'; private env = mockEnvironment(OperatingSystem.Windows); public async runCodeAsync(): Promise { await runCodeAsync(this.code, this.folderName, this.fileExtension, this.mocks, this.env); } public withOs(os: OperatingSystem) { this.env = mockEnvironment(os); return this; } public withFolderName(folderName: string) { this.folderName = folderName; return this; } public withCode(code: string) { this.code = code; return this; } public withExtension(fileExtension: string) { this.fileExtension = fileExtension; return this; } } function mockEnvironment(os: OperatingSystem) { return new EnvironmentStub().withOs(os); } const enum NodeJsCommand { tmpdir, join, exec, mkdir, writeFile, chmod } function getNodeJsMocks() { const commandHistory = new Array(); return { os: mockOs(commandHistory), path: mockPath(commandHistory), fs: mockNodeFs(commandHistory), child_process: mockChildProcess(commandHistory), commandHistory, }; } function mockOs(commandHistory: NodeJsCommand[]) { let tmpDir: string; return { setupTmpdir: (value: string): void => { tmpDir = value; }, tmpdir: (): string => { if (!tmpDir) { throw new Error('tmpdir not set up'); } commandHistory.push(NodeJsCommand.tmpdir); return tmpDir; }, }; } function mockPath(commandHistory: NodeJsCommand[]) { const sequence = new Array(); const scenarios = new Map(); const getScenarioKey = (paths: string[]) => paths.join('|'); return { setupJoin: (returnValue: string, ...paths: string[]): void => { scenarios.set(getScenarioKey(paths), returnValue); }, setupJoinSequence: (...valuesToReturn: string[]): void => { sequence.push(...valuesToReturn); sequence.reverse(); }, join: (...paths: string[]): string => { commandHistory.push(NodeJsCommand.join); if (sequence.length > 0) { return sequence.pop(); } const key = getScenarioKey(paths); if (!scenarios.has(key)) { return paths.join('/'); } return scenarios.get(key); }, }; } function mockChildProcess(commandHistory: NodeJsCommand[]) { const executionHistory = new Array(); return { exec: (command: string): void => { commandHistory.push(NodeJsCommand.exec); executionHistory.push(command); }, executionHistory, }; } function mockNodeFs(commandHistory: NodeJsCommand[]) { interface IMkdirCall { path: string; isRecursive: boolean; } interface IWriteFileCall { path: string; data: string; } interface IChmodCall { path: string; mode: string | number; } const mkdirHistory = new Array(); const writeFileHistory = new Array(); const chmodCallHistory = new Array(); return { promises: { mkdir: (path, options) => { commandHistory.push(NodeJsCommand.mkdir); mkdirHistory.push({ path, isRecursive: options && options.recursive }); return Promise.resolve(path); }, writeFile: (path, data) => { commandHistory.push(NodeJsCommand.writeFile); writeFileHistory.push({ path, data }); return Promise.resolve(); }, chmod: (path, mode) => { commandHistory.push(NodeJsCommand.chmod); chmodCallHistory.push({ path, mode }); return Promise.resolve(); }, }, mkdirHistory, writeFileHistory, chmodCallHistory, }; }