Skip to content

Commit 775e53b

Browse files
authored
Merge pull request #2237 from fatso83/issue-2226
Fix 2226: restore props defined on prototype chain by deleting
2 parents 5436466 + 92dc087 commit 775e53b

8 files changed

+76
-14
lines changed

lib/sinon/default-behaviors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ var defaultBehaviors = {
258258
Object.defineProperty(rootStub.rootObj, rootStub.propName, {
259259
value: newVal,
260260
enumerable: true,
261-
configurable: isPropertyConfigurable(rootStub.rootObj, rootStub.propName)
261+
configurable: rootStub.shadowsPropOnPrototype || isPropertyConfigurable(rootStub.rootObj, rootStub.propName)
262262
});
263263

264264
return fake;

lib/sinon/sandbox.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ function Sandbox() {
191191
var descriptor = getPropertyDescriptor(object, property);
192192

193193
function restorer() {
194-
Object.defineProperty(object, property, descriptor);
194+
if (descriptor.isOwn) {
195+
Object.defineProperty(object, property, descriptor);
196+
} else {
197+
delete object[property];
198+
}
195199
}
196200
restorer.object = object;
197201
restorer.property = property;

lib/sinon/stub.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ var behaviors = require("./default-behaviors");
66
var createProxy = require("./proxy");
77
var functionName = require("@sinonjs/commons").functionName;
88
var hasOwnProperty = require("@sinonjs/commons").prototypes.object.hasOwnProperty;
9-
var isNonExistentOwnProperty = require("./util/core/is-non-existent-own-property");
9+
var isNonExistentProperty = require("./util/core/is-non-existent-property");
1010
var spy = require("./spy");
1111
var extend = require("./util/core/extend");
1212
var getPropertyDescriptor = require("./util/core/get-property-descriptor");
@@ -69,8 +69,8 @@ function stub(object, property) {
6969

7070
throwOnFalsyObject.apply(null, arguments);
7171

72-
if (isNonExistentOwnProperty(object, property)) {
73-
throw new TypeError("Cannot stub non-existent own property " + valueToString(property));
72+
if (isNonExistentProperty(object, property)) {
73+
throw new TypeError("Cannot stub non-existent property " + valueToString(property));
7474
}
7575

7676
var actualDescriptor = getPropertyDescriptor(object, property);
@@ -97,8 +97,9 @@ function stub(object, property) {
9797
extend.nonEnum(s, {
9898
rootObj: object,
9999
propName: property,
100+
shadowsPropOnPrototype: !actualDescriptor.isOwn,
100101
restore: function restore() {
101-
if (actualDescriptor !== undefined) {
102+
if (actualDescriptor !== undefined && actualDescriptor.isOwn) {
102103
Object.defineProperty(object, property, actualDescriptor);
103104
return;
104105
}

lib/sinon/util/core/get-property-descriptor.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@
33
module.exports = function getPropertyDescriptor(object, property) {
44
var proto = object;
55
var descriptor;
6+
var isOwn = Boolean(object && Object.getOwnPropertyDescriptor(object, property));
67

78
while (proto && !(descriptor = Object.getOwnPropertyDescriptor(proto, property))) {
89
proto = Object.getPrototypeOf(proto);
910
}
11+
12+
if (descriptor) {
13+
descriptor.isOwn = isOwn;
14+
}
15+
1016
return descriptor;
1117
};

lib/sinon/util/core/is-non-existent-own-property.js

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use strict";
2+
3+
/**
4+
* @param {*} object
5+
* @param {String} property
6+
* @returns whether a prop exists in the prototype chain
7+
*/
8+
function isNonExistentProperty(object, property) {
9+
return object && typeof property !== "undefined" && !(property in object);
10+
}
11+
12+
module.exports = isNonExistentProperty;

test/issues/issues-test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,9 +580,11 @@ describe("issues", function() {
580580
function ClassWithoutProps() {
581581
return;
582582
}
583+
583584
function AnotherClassWithoutProps() {
584585
return;
585586
}
587+
586588
ClassWithoutProps.prototype.constructor = ClassWithoutProps;
587589
AnotherClassWithoutProps.prototype.constructor = AnotherClassWithoutProps;
588590
var arg1 = new ClassWithoutProps(); //arg1.constructor.name === ClassWithoutProps
@@ -640,6 +642,7 @@ describe("issues", function() {
640642
function Foo() {
641643
return;
642644
}
645+
643646
// eslint-disable-next-line mocha/no-setup-in-describe
644647
Foo.prototype.testMethod = function() {
645648
return;
@@ -679,6 +682,7 @@ describe("issues", function() {
679682
function Foo() {
680683
return;
681684
}
685+
682686
// eslint-disable-next-line mocha/no-setup-in-describe
683687
Foo.prototype.testMethod = function() {
684688
return 1;
@@ -709,4 +713,46 @@ describe("issues", function() {
709713
refute.equals(fake.lastArg, 3);
710714
});
711715
});
716+
717+
describe("#2226 - props on prototype are not restored correctly", function() {
718+
function createObjectWithPropFromPrototype() {
719+
var proto = {};
720+
var obj = {};
721+
722+
Object.setPrototypeOf(obj, proto);
723+
Object.defineProperty(proto, "test", { writable: true, value: 1 });
724+
return obj;
725+
}
726+
727+
it("should restore fakes shadowing prototype props correctly", function() {
728+
var obj = createObjectWithPropFromPrototype();
729+
730+
var originalPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, "test");
731+
732+
sinon.replace(obj, "test", 2);
733+
var replacedPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, "test");
734+
735+
sinon.restore();
736+
var restoredPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, "test");
737+
738+
assert.isUndefined(originalPropertyDescriptor);
739+
refute.isUndefined(replacedPropertyDescriptor);
740+
assert.isUndefined(restoredPropertyDescriptor);
741+
});
742+
743+
it("should restore stubs shadowing prototype props correctly", function() {
744+
var obj = createObjectWithPropFromPrototype();
745+
var originalPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, "test");
746+
747+
sinon.stub(obj, "test").value(2);
748+
var replacedPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, "test");
749+
750+
sinon.restore();
751+
var restoredPropertyDescriptor = Object.getOwnPropertyDescriptor(obj, "test");
752+
753+
assert.isUndefined(originalPropertyDescriptor);
754+
refute.isUndefined(replacedPropertyDescriptor);
755+
assert.isUndefined(restoredPropertyDescriptor);
756+
});
757+
});
712758
});

test/sandbox-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ describe("Sandbox", function() {
562562
function() {
563563
sandbox.stub(object, Symbol());
564564
},
565-
{ message: "Cannot stub non-existent own property Symbol()" },
565+
{ message: "Cannot stub non-existent property Symbol()" },
566566
TypeError
567567
);
568568

0 commit comments

Comments
 (0)