• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import { mustNotMutateObjectDeep } from '../common/index.mjs';
2import assert from 'node:assert';
3import { promisify } from 'node:util';
4
5// Test common.mustNotMutateObjectDeep()
6
7const original = {
8  foo: { bar: 'baz' },
9  qux: null,
10  quux: [
11    'quuz',
12    { corge: 'grault' },
13  ],
14};
15
16// Make a copy to make sure original doesn't get altered by the function itself.
17const backup = structuredClone(original);
18
19// Wrapper for convenience:
20const obj = () => mustNotMutateObjectDeep(original);
21
22function testOriginal(root) {
23  assert.deepStrictEqual(root, backup);
24  return root.foo.bar === 'baz' && root.quux[1].corge.length === 6;
25}
26
27function definePropertyOnRoot(root) {
28  Object.defineProperty(root, 'xyzzy', {});
29}
30
31function definePropertyOnFoo(root) {
32  Object.defineProperty(root.foo, 'xyzzy', {});
33}
34
35function deletePropertyOnRoot(root) {
36  delete root.foo;
37}
38
39function deletePropertyOnFoo(root) {
40  delete root.foo.bar;
41}
42
43function preventExtensionsOnRoot(root) {
44  Object.preventExtensions(root);
45}
46
47function preventExtensionsOnFoo(root) {
48  Object.preventExtensions(root.foo);
49}
50
51function preventExtensionsOnRootViaSeal(root) {
52  Object.seal(root);
53}
54
55function preventExtensionsOnFooViaSeal(root) {
56  Object.seal(root.foo);
57}
58
59function preventExtensionsOnRootViaFreeze(root) {
60  Object.freeze(root);
61}
62
63function preventExtensionsOnFooViaFreeze(root) {
64  Object.freeze(root.foo);
65}
66
67function setOnRoot(root) {
68  root.xyzzy = 'gwak';
69}
70
71function setOnFoo(root) {
72  root.foo.xyzzy = 'gwak';
73}
74
75function setQux(root) {
76  root.qux = 'gwak';
77}
78
79function setQuux(root) {
80  root.quux.push('gwak');
81}
82
83function setQuuxItem(root) {
84  root.quux[0] = 'gwak';
85}
86
87function setQuuxProperty(root) {
88  root.quux[1].corge = 'gwak';
89}
90
91function setPrototypeOfRoot(root) {
92  Object.setPrototypeOf(root, Array);
93}
94
95function setPrototypeOfFoo(root) {
96  Object.setPrototypeOf(root.foo, Array);
97}
98
99function setPrototypeOfQuux(root) {
100  Object.setPrototypeOf(root.quux, Array);
101}
102
103
104{
105  assert.ok(testOriginal(obj()));
106
107  assert.throws(
108    () => definePropertyOnRoot(obj()),
109    { code: 'ERR_ASSERTION' }
110  );
111  assert.throws(
112    () => definePropertyOnFoo(obj()),
113    { code: 'ERR_ASSERTION' }
114  );
115  assert.throws(
116    () => deletePropertyOnRoot(obj()),
117    { code: 'ERR_ASSERTION' }
118  );
119  assert.throws(
120    () => deletePropertyOnFoo(obj()),
121    { code: 'ERR_ASSERTION' }
122  );
123  assert.throws(
124    () => preventExtensionsOnRoot(obj()),
125    { code: 'ERR_ASSERTION' }
126  );
127  assert.throws(
128    () => preventExtensionsOnFoo(obj()),
129    { code: 'ERR_ASSERTION' }
130  );
131  assert.throws(
132    () => preventExtensionsOnRootViaSeal(obj()),
133    { code: 'ERR_ASSERTION' }
134  );
135  assert.throws(
136    () => preventExtensionsOnFooViaSeal(obj()),
137    { code: 'ERR_ASSERTION' }
138  );
139  assert.throws(
140    () => preventExtensionsOnRootViaFreeze(obj()),
141    { code: 'ERR_ASSERTION' }
142  );
143  assert.throws(
144    () => preventExtensionsOnFooViaFreeze(obj()),
145    { code: 'ERR_ASSERTION' }
146  );
147  assert.throws(
148    () => setOnRoot(obj()),
149    { code: 'ERR_ASSERTION' }
150  );
151  assert.throws(
152    () => setOnFoo(obj()),
153    { code: 'ERR_ASSERTION' }
154  );
155  assert.throws(
156    () => setQux(obj()),
157    { code: 'ERR_ASSERTION' }
158  );
159  assert.throws(
160    () => setQuux(obj()),
161    { code: 'ERR_ASSERTION' }
162  );
163  assert.throws(
164    () => setQuux(obj()),
165    { code: 'ERR_ASSERTION' }
166  );
167  assert.throws(
168    () => setQuuxItem(obj()),
169    { code: 'ERR_ASSERTION' }
170  );
171  assert.throws(
172    () => setQuuxProperty(obj()),
173    { code: 'ERR_ASSERTION' }
174  );
175  assert.throws(
176    () => setPrototypeOfRoot(obj()),
177    { code: 'ERR_ASSERTION' }
178  );
179  assert.throws(
180    () => setPrototypeOfFoo(obj()),
181    { code: 'ERR_ASSERTION' }
182  );
183  assert.throws(
184    () => setPrototypeOfQuux(obj()),
185    { code: 'ERR_ASSERTION' }
186  );
187
188  // Test that no mutation happened:
189  assert.ok(testOriginal(obj()));
190}
191
192// Test various supported types, directly and nested:
193[
194  undefined, null, false, true, 42, 42n, Symbol('42'), NaN, Infinity, {}, [],
195  () => {}, async () => {}, Promise.resolve(), Math, Object.create(null),
196].forEach((target) => {
197  assert.deepStrictEqual(mustNotMutateObjectDeep(target), target);
198  assert.deepStrictEqual(mustNotMutateObjectDeep({ target }), { target });
199  assert.deepStrictEqual(mustNotMutateObjectDeep([ target ]), [ target ]);
200});
201
202// Test that passed functions keep working correctly:
203{
204  const fn = () => 'blep';
205  fn.foo = {};
206  const fnImmutableView = mustNotMutateObjectDeep(fn);
207  assert.deepStrictEqual(fnImmutableView, fn);
208
209  // Test that the function still works:
210  assert.strictEqual(fn(), 'blep');
211  assert.strictEqual(fnImmutableView(), 'blep');
212
213  // Test that the original function is not deeply frozen:
214  fn.foo.bar = 'baz';
215  assert.strictEqual(fn.foo.bar, 'baz');
216  assert.strictEqual(fnImmutableView.foo.bar, 'baz');
217
218  // Test the original function is not frozen:
219  fn.qux = 'quux';
220  assert.strictEqual(fn.qux, 'quux');
221  assert.strictEqual(fnImmutableView.qux, 'quux');
222
223  // Redefining util.promisify.custom also works:
224  promisify(mustNotMutateObjectDeep(promisify(fn)));
225}
226