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:
Steve Gravrock
2026-02-16 11:06:27 -08:00
parent 281c0d1ee8
commit 63ac7da082
23 changed files with 263 additions and 9 deletions

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -1247,4 +1247,8 @@ describe('Clock (acceptance)', function() {
clock.tick(400);
});
isNonMonkeyPatchableClass(privateUnderTest.Clock, function() {
return new privateUnderTest.Clock({}, function() {}, {});
});
});

View File

@@ -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();
});
});

View File

@@ -160,6 +160,13 @@ describe('ParallelReportDispatcher', function() {
);
});
isNonMonkeyPatchableClass(
jasmineUnderTest.ParallelReportDispatcher,
function() {
return new jasmineUnderTest.ParallelReportDispatcher();
}
);
function mockGlobalErrors() {
const globalErrors = jasmine.createSpyObj('globalErrors', [
'install',

View File

@@ -30,4 +30,8 @@ describe('Timer', function() {
expect(timer.elapsed()).toEqual(jasmine.any(Number));
});
});
isNonMonkeyPatchableClass(jasmineUnderTest.Timer, function() {
return new jasmineUnderTest.Timer();
});
});

View File

@@ -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',

View 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);
});
});
};

View File

@@ -1396,4 +1396,6 @@ describe('HtmlReporterV2', function() {
});
});
});
isNonMonkeyPatchableClass(jasmineUnderTest.HtmlReporterV2, setup);
});

View File

@@ -63,4 +63,8 @@ describe('HtmlReporterV2Urls', function() {
return qs;
}
});
isNonMonkeyPatchableClass(jasmineUnderTest.HtmlReporterV2Urls, function() {
return new jasmineUnderTest.HtmlReporterV2Urls({});
});
});

View File

@@ -77,4 +77,12 @@ describe('QueryString', function() {
expect(queryString.getParam('baz')).toBeFalsy();
});
});
isNonMonkeyPatchableClass(jasmineUnderTest.QueryString, function() {
return new jasmineUnderTest.QueryString({
getWindowLocation: function() {
return { search: '' };
}
});
});
});

View File

@@ -25,6 +25,7 @@ module.exports = {
'helpers/domHelpers.js',
'helpers/integrationMatchers.js',
'helpers/callerFilenameShim.js',
'helpers/monkeyPatchingSpecs.js',
'helpers/defineJasmineUnderTest.js',
'helpers/resetEnv.js'
],

View File

@@ -10,6 +10,7 @@
"helpers/integrationMatchers.js",
"helpers/callerFilenameShim.js",
"helpers/overrideConsoleLogForCircleCi.js",
"helpers/monkeyPatchingSpecs.js",
"helpers/nodeDefineJasmineUnderTest.js",
"helpers/resetEnv.js"
],

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
},

View File

@@ -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$ };
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;