diff --git a/node/mock-run.ts b/node/mock-run.ts index 9989e40e5..d21878766 100644 --- a/node/mock-run.ts +++ b/node/mock-run.ts @@ -1,16 +1,19 @@ -import ma = require('./mock-answer'); -import mockery = require('mockery'); +import { TaskLibAnswers } from './mock-answer'; +import { SinonSandbox, createSandbox } from 'sinon'; import im = require('./internal'); +import * as taskLib from './task'; export class TaskMockRunner { constructor(taskPath: string) { this._taskPath = taskPath; + this._sandbox = createSandbox(); } _taskPath: string; - _answers: ma.TaskLibAnswers | undefined; + _answers: TaskLibAnswers | undefined; _exports: {[key: string]: any} = { }; _moduleCount: number = 0; + private _sandbox: SinonSandbox; public setInput(name: string, val: string) { let key: string = im._getVariableKey(name); @@ -32,21 +35,81 @@ export class TaskMockRunner { * * @param answers Answers to be returned when the task lib functions are called. */ - public setAnswers(answers: ma.TaskLibAnswers) { + public setAnswers(answers: TaskLibAnswers) { this._answers = answers; } + /** + * Checks if a module name is valid for import, avoiding local module references. + * + * @param {string} modName - The name of the module to be checked. + * @returns {boolean} Returns true if the module name is valid, otherwise false. + */ + checkModuleName(modName: string): boolean { + if (modName.includes('.')) { + console.error(`ERROR: ${modName} is a local module. Cannot import it from task-lib. Please pass full path.`); + return false; + } + return true; + } + + /** + * Checks if a method in a new module is mockable based on specified conditions. + * + * @param {object} newModule - The new module containing the method. + * @param {string} methodName - The name of the method to check. + * @param {object} oldModule - The original module from which the method might be inherited. + * @returns {boolean} Returns true if the method is mockable, otherwise false. + */ + checkIsMockable(newModule, methodName, oldModule) { + // Get the method from the newModule + const method = newModule[methodName]; + + // Check if the method exists and is not undefined + if (!newModule.hasOwnProperty(methodName) || typeof method === 'undefined') { + return false; + } + + // Check if the method is a function + if (typeof method !== 'function') { + console.log(`WARNING: ${methodName} of ${newModule} is not a function. There is no option to replace getter/setter in this implementation. You can consider changing it.`); + return false; + } + + // Check if the method is writable + const descriptor = Object.getOwnPropertyDescriptor(oldModule, methodName); + return descriptor && descriptor.writable !== false; + } /** - * Register a mock module. When require() is called for the module name, - * the mock implementation will be returned instead. + * Registers a mock module, allowing the replacement of methods with mock implementations. * - * @param modName Module name to override. - * @param val Mock implementation of the module. - * @returns void + * @param {string} modName - The name of the module to be overridden. + * @param {object} modMock - The mock implementation of the module. + * @returns {void} */ - public registerMock(modName: string, mod: any): void { + public registerMock(modName: string, modMock: object): void { this._moduleCount++; - mockery.registerMock(modName, mod); + let oldMod: object; + + // Check if the module name is valid and can be imported + if (this.checkModuleName(modName)) { + oldMod = require(modName); + } else { + console.error(`ERROR: Cannot import ${modName}.`); + return; + } + + // Iterate through methods in the old module and replace them with mock implementations + for (let method in oldMod) { + if (this.checkIsMockable(modMock, method, oldMod)) { + const replacement = modMock[method] || oldMod[method]; + try { + this._sandbox.replace(oldMod, method, replacement); + } catch (error) { + console.error('ERROR: Cannot replace ${method} in ${oldMod} by ${replacement}. ${error.message}', ); + } + } + } } /** @@ -69,11 +132,6 @@ export class TaskMockRunner { * @returns void */ public run(noMockTask?: boolean): void { - // determine whether to enable mockery - if (!noMockTask || this._moduleCount) { - mockery.enable({warnOnUnregistered: false}); - } - // answers and exports not compatible with "noMockTask" mode if (noMockTask) { if (this._answers || Object.keys(this._exports).length) { @@ -82,7 +140,7 @@ export class TaskMockRunner { } // register mock task lib else { - var tlm = require('azure-pipelines-task-lib/mock-task'); + let tlm = require('azure-pipelines-task-lib/mock-task'); if (this._answers) { tlm.setAnswers(this._answers); } @@ -92,10 +150,25 @@ export class TaskMockRunner { tlm[key] = this._exports[key]; }); - mockery.registerMock('azure-pipelines-task-lib/task', tlm); + // With sinon we have to iterate through methods in the old module and replace them with mock implementations + let tlt = require('azure-pipelines-task-lib/task'); + for (let method in tlt) { + if (tlm.hasOwnProperty(method)) { + this._sandbox.replace(tlt, method, tlm[method]); + } + } + } // run it require(this._taskPath); } + /** + * Restores the sandboxed environment to its original state. + * + * @returns {void} + */ + public restore() { + this._sandbox.restore(); + } } diff --git a/node/package-lock.json b/node/package-lock.json index dbe46d6c4..09b25aa48 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -4,6 +4,47 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" + }, "@types/concat-stream": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.0.tgz", @@ -449,8 +490,7 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "has-symbols": { "version": "1.0.3", @@ -584,6 +624,11 @@ "argparse": "^2.0.1" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==" + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -593,6 +638,11 @@ "p-locate": "^5.0.0" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -684,6 +734,28 @@ "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", "dev": true }, + "nise": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", + "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", + "requires": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "requires": { + "type-detect": "4.0.8" + } + } + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -742,6 +814,21 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + } + } + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -869,6 +956,34 @@ "object-inspect": "^1.9.0" } }, + "sinon": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", + "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -969,6 +1084,11 @@ "is-number": "^7.0.0" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", diff --git a/node/package.json b/node/package.json index 31484d447..f158d9d9e 100644 --- a/node/package.json +++ b/node/package.json @@ -32,6 +32,7 @@ "q": "^1.5.1", "semver": "^5.1.0", "shelljs": "^0.8.5", + "sinon": "^15.2.0", "sync-request": "6.1.0", "uuid": "^3.0.1" },