Prevent monkey patching
This doesn't affect globals (describe, it, expect, etc). Those belong to the user and Jasmine doesn't depend on them.
This commit is contained in:
@@ -50,10 +50,16 @@ const getJasmineHtmlRequireObj = (function() {
|
||||
private$.FailuresView = htmlRequire.FailuresView(j$, private$);
|
||||
private$.PerformanceView = htmlRequire.PerformanceView(j$, private$);
|
||||
private$.TabBar = htmlRequire.TabBar(j$, private$);
|
||||
j$.HtmlReporterV2Urls = htmlRequire.HtmlReporterV2Urls(j$, private$);
|
||||
j$.HtmlReporterV2 = htmlRequire.HtmlReporterV2(j$, private$);
|
||||
j$.QueryString = htmlRequire.QueryString();
|
||||
private$.HtmlSpecFilterV2 = htmlRequire.HtmlSpecFilterV2();
|
||||
|
||||
for (const k of ['HtmlReporterV2Urls', 'HtmlReporterV2', 'QueryString']) {
|
||||
Object.defineProperty(j$, k, {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: htmlRequire[k](j$, private$)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return getJasmineHtmlRequire;
|
||||
@@ -101,6 +107,7 @@ getJasmineHtmlRequireObj().QueryString = function() {
|
||||
*/
|
||||
constructor(options) {
|
||||
this.#getWindowLocation = options.getWindowLocation;
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,6 +175,7 @@ getJasmineHtmlRequireObj().QueryString = function() {
|
||||
return '?' + qStrPairs.join('&');
|
||||
}
|
||||
|
||||
Object.freeze(QueryString.prototype);
|
||||
return QueryString;
|
||||
};
|
||||
|
||||
@@ -772,6 +780,8 @@ getJasmineHtmlRequireObj().HtmlReporterV2 = function(j$, private$) {
|
||||
);
|
||||
this.#container.appendChild(this.#htmlReporterMain);
|
||||
this.#failures.show();
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
jasmineStarted(options) {
|
||||
@@ -949,6 +959,7 @@ getJasmineHtmlRequireObj().HtmlReporterV2 = function(j$, private$) {
|
||||
}
|
||||
}
|
||||
|
||||
Object.freeze(HtmlReporterV2.prototype);
|
||||
return HtmlReporterV2;
|
||||
};
|
||||
|
||||
@@ -972,6 +983,8 @@ getJasmineHtmlRequireObj().HtmlReporterV2Urls = function(j$, private$) {
|
||||
return window.location;
|
||||
}
|
||||
});
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1022,6 +1035,7 @@ getJasmineHtmlRequireObj().HtmlReporterV2Urls = function(j$, private$) {
|
||||
}
|
||||
}
|
||||
|
||||
Object.freeze(HtmlReporterV2Urls.prototype);
|
||||
return HtmlReporterV2Urls;
|
||||
};
|
||||
|
||||
|
||||
@@ -141,6 +141,28 @@ const getJasmineRequireObj = (function() {
|
||||
|
||||
private$.loadedAsBrowserEsm = loadedAsBrowserEsm;
|
||||
|
||||
// Prevent monkey patching of existing properties but allow adding new ones.
|
||||
// jasmine-html.js needs to be able to add to the jasmine namespace.
|
||||
// jasmine-ajax also installs itself this way.
|
||||
const writeable = [
|
||||
'DEFAULT_TIMEOUT_INTERVAL',
|
||||
'MAX_PRETTY_PRINT_ARRAY_LENGTH',
|
||||
'MAX_PRETTY_PRINT_CHARS',
|
||||
'MAX_PRETTY_PRINT_DEPTH'
|
||||
];
|
||||
const descriptors = Object.getOwnPropertyDescriptors(j$);
|
||||
|
||||
for (const [k, d] of Object.entries(descriptors)) {
|
||||
if (!writeable.includes(k)) {
|
||||
Object.defineProperty(j$, k, {
|
||||
value: d.value,
|
||||
enumerable: d.enumerable,
|
||||
configurable: false,
|
||||
writable: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { jasmine: j$, private: private$ };
|
||||
};
|
||||
|
||||
@@ -241,6 +263,7 @@ getJasmineRequireObj().base = function(j$, private$, jasmineGlobal) {
|
||||
*/
|
||||
let DEFAULT_TIMEOUT_INTERVAL = 5000;
|
||||
Object.defineProperty(j$, 'DEFAULT_TIMEOUT_INTERVAL', {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return DEFAULT_TIMEOUT_INTERVAL;
|
||||
},
|
||||
@@ -2077,6 +2100,8 @@ getJasmineRequireObj().Env = function(j$, private$) {
|
||||
this.cleanup_ = function() {
|
||||
uninstallGlobalErrors();
|
||||
};
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
function indirectCallerFilename(depth) {
|
||||
@@ -2087,6 +2112,8 @@ getJasmineRequireObj().Env = function(j$, private$) {
|
||||
return frames[depth] && frames[depth].file;
|
||||
}
|
||||
|
||||
Object.freeze(Env);
|
||||
Object.freeze(Env.prototype);
|
||||
return Env;
|
||||
};
|
||||
|
||||
@@ -3009,6 +3036,8 @@ callbacks to execute _before_ running the next one.
|
||||
setInterval[IsMockClockTimingFn] = true;
|
||||
clearInterval[IsMockClockTimingFn] = true;
|
||||
|
||||
Object.freeze(this);
|
||||
|
||||
return this;
|
||||
|
||||
// Advances the Clock's time until the mode changes.
|
||||
@@ -3162,6 +3191,8 @@ callbacks to execute _before_ running the next one.
|
||||
};
|
||||
|
||||
Clock.IsMockClockTimingFn = IsMockClockTimingFn;
|
||||
Object.freeze(Clock);
|
||||
Object.freeze(Clock.prototype);
|
||||
return Clock;
|
||||
};
|
||||
|
||||
@@ -8038,9 +8069,12 @@ getJasmineRequireObj().ParallelReportDispatcher = function(j$, private$) {
|
||||
for (const eventName of private$.reporterEvents) {
|
||||
this[eventName] = dispatcher[eventName].bind(dispatcher);
|
||||
}
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
Object.freeze(ParallelReportDispatcher.prototype);
|
||||
return ParallelReportDispatcher;
|
||||
};
|
||||
|
||||
@@ -11698,8 +11732,12 @@ getJasmineRequireObj().Timer = function() {
|
||||
this.elapsed = function() {
|
||||
return now() - startTime;
|
||||
};
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
Object.freeze(Timer);
|
||||
Object.freeze(Timer.prototype);
|
||||
return Timer;
|
||||
};
|
||||
|
||||
|
||||
@@ -1247,4 +1247,8 @@ describe('Clock (acceptance)', function() {
|
||||
|
||||
clock.tick(400);
|
||||
});
|
||||
|
||||
isNonMonkeyPatchableClass(privateUnderTest.Clock, function() {
|
||||
return new privateUnderTest.Clock({}, function() {}, {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -874,4 +874,8 @@ describe('Env', function() {
|
||||
}).toThrowError('Jasmine cannot be configured via Env in parallel mode');
|
||||
});
|
||||
});
|
||||
|
||||
isNonMonkeyPatchableClass(privateUnderTest.Env, function() {
|
||||
return new privateUnderTest.Env();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,6 +160,13 @@ describe('ParallelReportDispatcher', function() {
|
||||
);
|
||||
});
|
||||
|
||||
isNonMonkeyPatchableClass(
|
||||
jasmineUnderTest.ParallelReportDispatcher,
|
||||
function() {
|
||||
return new jasmineUnderTest.ParallelReportDispatcher();
|
||||
}
|
||||
);
|
||||
|
||||
function mockGlobalErrors() {
|
||||
const globalErrors = jasmine.createSpyObj('globalErrors', [
|
||||
'install',
|
||||
|
||||
@@ -30,4 +30,8 @@ describe('Timer', function() {
|
||||
expect(timer.elapsed()).toEqual(jasmine.any(Number));
|
||||
});
|
||||
});
|
||||
|
||||
isNonMonkeyPatchableClass(jasmineUnderTest.Timer, function() {
|
||||
return new jasmineUnderTest.Timer();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,13 +13,61 @@ describe('The jasmine namespace', function() {
|
||||
expect(setDifference(actualKeys, expectedKeys())).toEqual(new Set());
|
||||
});
|
||||
|
||||
describe('Preventing monkey patching', function() {
|
||||
const mutable = mutableKeys();
|
||||
|
||||
for (const key of expectedKeys()) {
|
||||
if (mutable.includes(key)) {
|
||||
it(`allows overwriting of jasmine.${key}`, function() {
|
||||
const existingVal = jasmineUnderTest[key];
|
||||
|
||||
try {
|
||||
jasmineUnderTest[key] = 'new value';
|
||||
expect(jasmineUnderTest[key]).toEqual('new value');
|
||||
} finally {
|
||||
jasmineUnderTest[key] = existingVal;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
it(`prevents overwriting of jasmine.${key}`, function() {
|
||||
const existingVal = jasmineUnderTest[key];
|
||||
|
||||
try {
|
||||
jasmineUnderTest[key] = 'monkey patch';
|
||||
expect(jasmineUnderTest[key]).toBe(existingVal);
|
||||
} finally {
|
||||
// This will be a no-op if the test passed, but will prevent state
|
||||
// leakage if it failed.
|
||||
jasmineUnderTest[key] = existingVal;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
it('allows additions', function() {
|
||||
try {
|
||||
jasmineUnderTest.Ajax = 'it worked';
|
||||
expect(jasmineUnderTest.Ajax).toEqual('it worked');
|
||||
} finally {
|
||||
delete jasmineUnderTest.Ajax;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function mutableKeys() {
|
||||
return [
|
||||
'MAX_PRETTY_PRINT_ARRAY_LENGTH',
|
||||
'MAX_PRETTY_PRINT_CHARS',
|
||||
'MAX_PRETTY_PRINT_DEPTH',
|
||||
'DEFAULT_TIMEOUT_INTERVAL'
|
||||
];
|
||||
}
|
||||
|
||||
function expectedKeys() {
|
||||
// Does not include properties added by requireInterface(), since that isn't
|
||||
// called by defineJasmineUnderTest.js/nodeDefineJasmineUnderTest.js.
|
||||
const result = new Set([
|
||||
'MAX_PRETTY_PRINT_ARRAY_LENGTH',
|
||||
'MAX_PRETTY_PRINT_CHARS',
|
||||
'MAX_PRETTY_PRINT_DEPTH',
|
||||
...mutableKeys(),
|
||||
'debugLog',
|
||||
'getEnv',
|
||||
'isSpy',
|
||||
|
||||
67
spec/helpers/monkeyPatchingSpecs.js
Normal file
67
spec/helpers/monkeyPatchingSpecs.js
Normal file
@@ -0,0 +1,67 @@
|
||||
globalThis.isNonMonkeyPatchableClass = function(ctor, makeInstance) {
|
||||
describe('Monkey patching prevention', function() {
|
||||
it(`prevents overwriting ${ctor.name}.prototype`, function() {
|
||||
const existing = ctor.prototype;
|
||||
|
||||
try {
|
||||
ctor.prototype = {};
|
||||
expect(ctor.prototype).toBe(existing);
|
||||
} finally {
|
||||
// This will be a no-op if the test passed, but will prevent state
|
||||
// leakage if it failed.
|
||||
ctor.prototype = existing;
|
||||
}
|
||||
});
|
||||
|
||||
it("prevents overwriting an instance's prototype", function() {
|
||||
const instance = makeInstance();
|
||||
let thrown;
|
||||
|
||||
// The message varies from browser to browser, so we can't rely on it
|
||||
try {
|
||||
instance.__proto__ = {};
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
|
||||
expect(thrown).toBeInstanceOf(TypeError);
|
||||
});
|
||||
|
||||
it('prevents overwriting prototype properties', function() {
|
||||
let any = false;
|
||||
|
||||
for (const k of Object.getOwnPropertyNames(ctor.prototype)) {
|
||||
any = true;
|
||||
const existingValue = ctor.prototype[k];
|
||||
|
||||
try {
|
||||
ctor.prototype[k] = {};
|
||||
expect(ctor.prototype[k])
|
||||
.withContext(k)
|
||||
.toBe(existingValue);
|
||||
} finally {
|
||||
// This will be a no-op if the test passed, but will prevent state
|
||||
// leakage if it failed.
|
||||
ctor.prototype[k] = existingValue;
|
||||
}
|
||||
}
|
||||
|
||||
expect(any).toBe(true);
|
||||
});
|
||||
|
||||
it('prevents overriding prototype properties', function() {
|
||||
const instance = makeInstance();
|
||||
let any = false;
|
||||
|
||||
for (const k of Object.getOwnPropertyNames(ctor.prototype)) {
|
||||
any = true;
|
||||
instance[k] = {};
|
||||
expect(instance[k])
|
||||
.withContext(k)
|
||||
.toBe(ctor.prototype[k]);
|
||||
}
|
||||
|
||||
expect(any).toBe(true);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1396,4 +1396,6 @@ describe('HtmlReporterV2', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
isNonMonkeyPatchableClass(jasmineUnderTest.HtmlReporterV2, setup);
|
||||
});
|
||||
|
||||
@@ -63,4 +63,8 @@ describe('HtmlReporterV2Urls', function() {
|
||||
return qs;
|
||||
}
|
||||
});
|
||||
|
||||
isNonMonkeyPatchableClass(jasmineUnderTest.HtmlReporterV2Urls, function() {
|
||||
return new jasmineUnderTest.HtmlReporterV2Urls({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,4 +77,12 @@ describe('QueryString', function() {
|
||||
expect(queryString.getParam('baz')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
isNonMonkeyPatchableClass(jasmineUnderTest.QueryString, function() {
|
||||
return new jasmineUnderTest.QueryString({
|
||||
getWindowLocation: function() {
|
||||
return { search: '' };
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ module.exports = {
|
||||
'helpers/domHelpers.js',
|
||||
'helpers/integrationMatchers.js',
|
||||
'helpers/callerFilenameShim.js',
|
||||
'helpers/monkeyPatchingSpecs.js',
|
||||
'helpers/defineJasmineUnderTest.js',
|
||||
'helpers/resetEnv.js'
|
||||
],
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"helpers/integrationMatchers.js",
|
||||
"helpers/callerFilenameShim.js",
|
||||
"helpers/overrideConsoleLogForCircleCi.js",
|
||||
"helpers/monkeyPatchingSpecs.js",
|
||||
"helpers/nodeDefineJasmineUnderTest.js",
|
||||
"helpers/resetEnv.js"
|
||||
],
|
||||
|
||||
@@ -192,6 +192,8 @@ callbacks to execute _before_ running the next one.
|
||||
setInterval[IsMockClockTimingFn] = true;
|
||||
clearInterval[IsMockClockTimingFn] = true;
|
||||
|
||||
Object.freeze(this);
|
||||
|
||||
return this;
|
||||
|
||||
// Advances the Clock's time until the mode changes.
|
||||
@@ -345,5 +347,7 @@ callbacks to execute _before_ running the next one.
|
||||
};
|
||||
|
||||
Clock.IsMockClockTimingFn = IsMockClockTimingFn;
|
||||
Object.freeze(Clock);
|
||||
Object.freeze(Clock.prototype);
|
||||
return Clock;
|
||||
};
|
||||
|
||||
@@ -835,6 +835,8 @@ getJasmineRequireObj().Env = function(j$, private$) {
|
||||
this.cleanup_ = function() {
|
||||
uninstallGlobalErrors();
|
||||
};
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
function indirectCallerFilename(depth) {
|
||||
@@ -845,5 +847,7 @@ getJasmineRequireObj().Env = function(j$, private$) {
|
||||
return frames[depth] && frames[depth].file;
|
||||
}
|
||||
|
||||
Object.freeze(Env);
|
||||
Object.freeze(Env.prototype);
|
||||
return Env;
|
||||
};
|
||||
|
||||
@@ -87,8 +87,11 @@ getJasmineRequireObj().ParallelReportDispatcher = function(j$, private$) {
|
||||
for (const eventName of private$.reporterEvents) {
|
||||
this[eventName] = dispatcher[eventName].bind(dispatcher);
|
||||
}
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
|
||||
Object.freeze(ParallelReportDispatcher.prototype);
|
||||
return ParallelReportDispatcher;
|
||||
};
|
||||
|
||||
@@ -39,7 +39,11 @@ getJasmineRequireObj().Timer = function() {
|
||||
this.elapsed = function() {
|
||||
return now() - startTime;
|
||||
};
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
Object.freeze(Timer);
|
||||
Object.freeze(Timer.prototype);
|
||||
return Timer;
|
||||
};
|
||||
|
||||
@@ -43,6 +43,7 @@ getJasmineRequireObj().base = function(j$, private$, jasmineGlobal) {
|
||||
*/
|
||||
let DEFAULT_TIMEOUT_INTERVAL = 5000;
|
||||
Object.defineProperty(j$, 'DEFAULT_TIMEOUT_INTERVAL', {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return DEFAULT_TIMEOUT_INTERVAL;
|
||||
},
|
||||
|
||||
@@ -116,6 +116,28 @@ const getJasmineRequireObj = (function() {
|
||||
|
||||
private$.loadedAsBrowserEsm = loadedAsBrowserEsm;
|
||||
|
||||
// Prevent monkey patching of existing properties but allow adding new ones.
|
||||
// jasmine-html.js needs to be able to add to the jasmine namespace.
|
||||
// jasmine-ajax also installs itself this way.
|
||||
const writeable = [
|
||||
'DEFAULT_TIMEOUT_INTERVAL',
|
||||
'MAX_PRETTY_PRINT_ARRAY_LENGTH',
|
||||
'MAX_PRETTY_PRINT_CHARS',
|
||||
'MAX_PRETTY_PRINT_DEPTH'
|
||||
];
|
||||
const descriptors = Object.getOwnPropertyDescriptors(j$);
|
||||
|
||||
for (const [k, d] of Object.entries(descriptors)) {
|
||||
if (!writeable.includes(k)) {
|
||||
Object.defineProperty(j$, k, {
|
||||
value: d.value,
|
||||
enumerable: d.enumerable,
|
||||
configurable: false,
|
||||
writable: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { jasmine: j$, private: private$ };
|
||||
};
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@ getJasmineHtmlRequireObj().HtmlReporterV2 = function(j$, private$) {
|
||||
);
|
||||
this.#container.appendChild(this.#htmlReporterMain);
|
||||
this.#failures.show();
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
jasmineStarted(options) {
|
||||
@@ -275,5 +277,6 @@ getJasmineHtmlRequireObj().HtmlReporterV2 = function(j$, private$) {
|
||||
}
|
||||
}
|
||||
|
||||
Object.freeze(HtmlReporterV2.prototype);
|
||||
return HtmlReporterV2;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ getJasmineHtmlRequireObj().HtmlReporterV2Urls = function(j$, private$) {
|
||||
return window.location;
|
||||
}
|
||||
});
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,5 +70,6 @@ getJasmineHtmlRequireObj().HtmlReporterV2Urls = function(j$, private$) {
|
||||
}
|
||||
}
|
||||
|
||||
Object.freeze(HtmlReporterV2Urls.prototype);
|
||||
return HtmlReporterV2Urls;
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ getJasmineHtmlRequireObj().QueryString = function() {
|
||||
*/
|
||||
constructor(options) {
|
||||
this.#getWindowLocation = options.getWindowLocation;
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,5 +82,6 @@ getJasmineHtmlRequireObj().QueryString = function() {
|
||||
return '?' + qStrPairs.join('&');
|
||||
}
|
||||
|
||||
Object.freeze(QueryString.prototype);
|
||||
return QueryString;
|
||||
};
|
||||
|
||||
@@ -25,10 +25,16 @@ const getJasmineHtmlRequireObj = (function() {
|
||||
private$.FailuresView = htmlRequire.FailuresView(j$, private$);
|
||||
private$.PerformanceView = htmlRequire.PerformanceView(j$, private$);
|
||||
private$.TabBar = htmlRequire.TabBar(j$, private$);
|
||||
j$.HtmlReporterV2Urls = htmlRequire.HtmlReporterV2Urls(j$, private$);
|
||||
j$.HtmlReporterV2 = htmlRequire.HtmlReporterV2(j$, private$);
|
||||
j$.QueryString = htmlRequire.QueryString();
|
||||
private$.HtmlSpecFilterV2 = htmlRequire.HtmlSpecFilterV2();
|
||||
|
||||
for (const k of ['HtmlReporterV2Urls', 'HtmlReporterV2', 'QueryString']) {
|
||||
Object.defineProperty(j$, k, {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: htmlRequire[k](j$, private$)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return getJasmineHtmlRequire;
|
||||
|
||||
Reference in New Issue
Block a user