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

@@ -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"
],