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