diff --git a/package-lock.json b/package-lock.json index 069bf50912..7c8bf0e6a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "mongodb": "6.17.0", "mustache": "4.2.0", "otpauth": "9.4.0", - "parse": "6.1.1", + "parse": "7.0.1", "path-to-regexp": "6.3.0", "pg-monitor": "3.0.0", "pg-promise": "12.2.0", @@ -2502,13 +2502,12 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz", - "integrity": "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", "license": "MIT", "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" + "core-js-pure": "^3.43.0" }, "engines": { "node": ">=6.9.0" @@ -9036,10 +9035,11 @@ } }, "node_modules/core-js-pure": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.41.0.tgz", - "integrity": "sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q==", + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz", + "integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -18589,17 +18589,16 @@ } }, "node_modules/parse": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/parse/-/parse-6.1.1.tgz", - "integrity": "sha512-zf70XcHKesDcqpO2RVKyIc1l7pngxBsYQVl0Yl/A38pftOSP8BQeampqqLEqMknzUetNZy8B+wrR3k5uTQDXOw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/parse/-/parse-7.0.1.tgz", + "integrity": "sha512-6hCnE8EWky/MqDtlpMnztzL0BEEsU3jVI7iKl2+AlJeSAeWkCgkPcb30eBNq57FcCnqWWC6uVJAaUMmX3+zrvg==", "license": "Apache-2.0", "dependencies": { - "@babel/runtime-corejs3": "7.27.0", - "idb-keyval": "6.2.1", + "@babel/runtime-corejs3": "7.28.4", + "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", "uuid": "10.0.0", - "ws": "8.18.1", - "xmlhttprequest": "1.8.0" + "ws": "8.18.3" }, "engines": { "node": "18 || 19 || 20 || 22" @@ -18635,6 +18634,12 @@ "node": ">=6" } }, + "node_modules/parse/node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, "node_modules/parse/node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", @@ -18643,14 +18648,15 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/parse/node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -24615,12 +24621,11 @@ } }, "@babel/runtime-corejs3": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz", - "integrity": "sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", + "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", "requires": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" + "core-js-pure": "^3.43.0" } }, "@babel/template": { @@ -29217,9 +29222,9 @@ } }, "core-js-pure": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.41.0.tgz", - "integrity": "sha512-71Gzp96T9YPk63aUvE5Q5qP+DryB4ZloUZPSOebGM88VNw8VNfvdA7z6kGA8iGOTEzAomsRidp4jXSmUIJsL+Q==" + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.46.0.tgz", + "integrity": "sha512-NMCW30bHNofuhwLhYPt66OLOKTMbOhgTTatKVbaQC3KRHpTCiRIBYvtshr+NBYSnBxwAFhjW/RfJ0XbIjS16rw==" }, "core-util-is": { "version": "1.0.3", @@ -35848,28 +35853,32 @@ } }, "parse": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/parse/-/parse-6.1.1.tgz", - "integrity": "sha512-zf70XcHKesDcqpO2RVKyIc1l7pngxBsYQVl0Yl/A38pftOSP8BQeampqqLEqMknzUetNZy8B+wrR3k5uTQDXOw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/parse/-/parse-7.0.1.tgz", + "integrity": "sha512-6hCnE8EWky/MqDtlpMnztzL0BEEsU3jVI7iKl2+AlJeSAeWkCgkPcb30eBNq57FcCnqWWC6uVJAaUMmX3+zrvg==", "requires": { - "@babel/runtime-corejs3": "7.27.0", + "@babel/runtime-corejs3": "7.28.4", "crypto-js": "4.2.0", - "idb-keyval": "6.2.1", + "idb-keyval": "6.2.2", "react-native-crypto-js": "1.0.0", "uuid": "10.0.0", - "ws": "8.18.1", - "xmlhttprequest": "1.8.0" + "ws": "8.18.3" }, "dependencies": { + "idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==" + }, "uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" }, "ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "requires": {} } } diff --git a/package.json b/package.json index c4b6f6b0b3..76fc250904 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "mongodb": "6.17.0", "mustache": "4.2.0", "otpauth": "9.4.0", - "parse": "6.1.1", + "parse": "7.0.1", "path-to-regexp": "6.3.0", "pg-monitor": "3.0.0", "pg-promise": "12.2.0", diff --git a/spec/Adapters/Auth/linkedIn.spec.js b/spec/Adapters/Auth/linkedIn.spec.js index f6c84a79af..9f5a4b37ae 100644 --- a/spec/Adapters/Auth/linkedIn.spec.js +++ b/spec/Adapters/Auth/linkedIn.spec.js @@ -89,12 +89,16 @@ describe('LinkedInAdapter', function () { describe('Test getUserFromAccessToken', function () { it('should fetch user successfully', async function () { - global.fetch = jasmine.createSpy().and.returnValue( - Promise.resolve({ - ok: true, - json: () => Promise.resolve({ id: 'validUserId' }), - }) - ); + mockFetch([ + { + url: 'https://api.linkedin.com/v2/me', + method: 'GET', + response: { + ok: true, + json: () => Promise.resolve({ id: 'validUserId' }), + }, + }, + ]); const user = await adapter.getUserFromAccessToken('validToken', false); @@ -104,14 +108,21 @@ describe('LinkedInAdapter', function () { 'x-li-format': 'json', 'x-li-src': undefined, }, + method: 'GET', }); expect(user).toEqual({ id: 'validUserId' }); }); it('should throw error for invalid response', async function () { - global.fetch = jasmine.createSpy().and.returnValue( - Promise.resolve({ ok: false }) - ); + mockFetch([ + { + url: 'https://api.linkedin.com/v2/me', + method: 'GET', + response: { + ok: false, + }, + }, + ]); await expectAsync(adapter.getUserFromAccessToken('invalidToken', false)).toBeRejectedWith( new Error('LinkedIn API request failed.') @@ -121,12 +132,16 @@ describe('LinkedInAdapter', function () { describe('Test getAccessTokenFromCode', function () { it('should fetch token successfully', async function () { - global.fetch = jasmine.createSpy().and.returnValue( - Promise.resolve({ - ok: true, - json: () => Promise.resolve({ access_token: 'validToken' }), - }) - ); + mockFetch([ + { + url: 'https://www.linkedin.com/oauth/v2/accessToken', + method: 'POST', + response: { + ok: true, + json: () => Promise.resolve({ access_token: 'validToken' }), + }, + }, + ]); const tokenResponse = await adapter.getAccessTokenFromCode('validCode', 'http://example.com'); @@ -139,9 +154,15 @@ describe('LinkedInAdapter', function () { }); it('should throw error for invalid response', async function () { - global.fetch = jasmine.createSpy().and.returnValue( - Promise.resolve({ ok: false }) - ); + mockFetch([ + { + url: 'https://www.linkedin.com/oauth/v2/accessToken', + method: 'POST', + response: { + ok: false, + }, + }, + ]); await expectAsync( adapter.getAccessTokenFromCode('invalidCode', 'http://example.com') diff --git a/spec/Adapters/Auth/wechat.spec.js b/spec/Adapters/Auth/wechat.spec.js index b82e3e877a..43518ec0df 100644 --- a/spec/Adapters/Auth/wechat.spec.js +++ b/spec/Adapters/Auth/wechat.spec.js @@ -23,7 +23,8 @@ describe('WeChatAdapter', function () { const user = await adapter.getUserFromAccessToken('validToken', { id: 'validOpenId' }); expect(global.fetch).toHaveBeenCalledWith( - 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId' + 'https://api.weixin.qq.com/sns/auth?access_token=validToken&openid=validOpenId', + jasmine.any(Object) ); expect(user).toEqual({ errcode: 0, id: 'validUserId' }); }); @@ -64,7 +65,8 @@ describe('WeChatAdapter', function () { const token = await adapter.getAccessTokenFromCode(authData); expect(global.fetch).toHaveBeenCalledWith( - 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code' + 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=validAppId&secret=validAppSecret&code=validCode&grant_type=authorization_code', + jasmine.any(Object) ); expect(token).toEqual('validToken'); }); diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index a16b52365a..a405b6fc48 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -189,7 +189,7 @@ describe('Cloud Code Logger', () => { }); }); - it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)('should log cloud function execution using the custom log level', async done => { + it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)('should log cloud function execution using the custom log level', async () => { Parse.Cloud.define('aFunction', () => { return 'it worked!'; }); @@ -203,6 +203,7 @@ describe('Cloud Code Logger', () => { expect(log).toEqual('info'); }); + Parse.Cloud._removeAllHooks(); await reconfigureServer({ silent: true, logLevels: { @@ -211,6 +212,10 @@ describe('Cloud Code Logger', () => { }, }); + Parse.Cloud.define('bFunction', () => { + throw new Error('Failed'); + }); + spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); try { @@ -221,15 +226,12 @@ describe('Cloud Code Logger', () => { .allArgs() .find(log => log[1].startsWith('Failed running cloud function bFunction for '))?.[0]; expect(log).toEqual('info'); - done(); } }); it('should log cloud function triggers using the custom log level', async () => { - Parse.Cloud.beforeSave('TestClass', () => {}); - Parse.Cloud.afterSave('TestClass', () => {}); - const execTest = async (logLevel, triggerBeforeSuccess, triggerAfter) => { + Parse.Cloud._removeAllHooks(); await reconfigureServer({ silent: true, logLevel, @@ -239,6 +241,9 @@ describe('Cloud Code Logger', () => { }, }); + Parse.Cloud.beforeSave('TestClass', () => { }); + Parse.Cloud.afterSave('TestClass', () => { }); + spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); const obj = new Parse.Object('TestClass'); await obj.save(); @@ -344,6 +349,7 @@ describe('Cloud Code Logger', () => { }); it('should log cloud function execution using the silent log level', async () => { + Parse.Cloud._removeAllHooks(); await reconfigureServer({ logLevels: { cloudFunctionSuccess: 'silent', @@ -367,6 +373,7 @@ describe('Cloud Code Logger', () => { }); it('should log cloud function triggers using the silent log level', async () => { + Parse.Cloud._removeAllHooks(); await reconfigureServer({ logLevels: { triggerAfter: 'silent', diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index 10558b209d..cf65e2df47 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -1395,10 +1395,10 @@ describe('Parse.Object testing', () => { .save() .then(function () { const query = new Parse.Query(TestObject); - return query.find(object.id); + return query.get(object.id); }) - .then(function (results) { - updatedObject = results[0]; + .then(function (result) { + updatedObject = result; updatedObject.set('x', 11); return updatedObject.save(); }) @@ -1409,7 +1409,8 @@ describe('Parse.Object testing', () => { equal(object.createdAt.getTime(), updatedObject.createdAt.getTime()); equal(object.updatedAt.getTime(), updatedObject.updatedAt.getTime()); done(); - }); + }) + .catch(done.fail); }); xit('fetchAll backbone-style callbacks', function (done) { diff --git a/spec/ParseQuery.Comment.spec.js b/spec/ParseQuery.Comment.spec.js index 7b37f2a2c2..df5b4aeac6 100644 --- a/spec/ParseQuery.Comment.spec.js +++ b/spec/ParseQuery.Comment.spec.js @@ -46,7 +46,7 @@ describe_only_db('mongo')('Parse.Query with comment testing', () => { }); it('send comment with query through REST', async () => { - const comment = 'Hello Parse'; + const comment = `Hello Parse ${Date.now()}`; const object = new TestObject(); object.set('name', 'object'); await object.save(); @@ -58,23 +58,55 @@ describe_only_db('mongo')('Parse.Query with comment testing', () => { }, }); await request(options); - const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } }); + + // Wait for profile entry to appear with retry logic + let result; + const maxRetries = 10; + const retryDelay = 100; + for (let i = 0; i < maxRetries; i++) { + result = await database.collection('system.profile').findOne( + { 'command.explain.comment': comment }, + { sort: { ts: -1 } } + ); + if (result) { + break; + } + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + + expect(result).toBeDefined(); expect(result.command.explain.comment).toBe(comment); }); it('send comment with query', async () => { - const comment = 'Hello Parse'; + const comment = `Hello Parse ${Date.now()}`; const object = new TestObject(); object.set('name', 'object'); await object.save(); const collection = await config.database.adapter._adaptiveCollection('TestObject'); await collection._rawFind({ name: 'object' }, { comment: comment }); - const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } }); + + // Wait for profile entry to appear with retry logic + let result; + const maxRetries = 10; + const retryDelay = 100; + for (let i = 0; i < maxRetries; i++) { + result = await database.collection('system.profile').findOne( + { 'command.comment': comment }, + { sort: { ts: -1 } } + ); + if (result) { + break; + } + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + + expect(result).toBeDefined(); expect(result.command.comment).toBe(comment); }); it('send a comment with a count query', async () => { - const comment = 'Hello Parse'; + const comment = `Hello Parse ${Date.now()}`; const object = new TestObject(); object.set('name', 'object'); await object.save(); @@ -86,12 +118,28 @@ describe_only_db('mongo')('Parse.Query with comment testing', () => { const collection = await config.database.adapter._adaptiveCollection('TestObject'); const countResult = await collection.count({ name: 'object' }, { comment: comment }); expect(countResult).toEqual(2); - const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } }); + + // Wait for profile entry to appear with retry logic + let result; + const maxRetries = 10; + const retryDelay = 100; + for (let i = 0; i < maxRetries; i++) { + result = await database.collection('system.profile').findOne( + { 'command.comment': comment }, + { sort: { ts: -1 } } + ); + if (result) { + break; + } + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + + expect(result).toBeDefined(); expect(result.command.comment).toBe(comment); }); it('attach a comment to an aggregation', async () => { - const comment = 'Hello Parse'; + const comment = `Hello Parse ${Date.now()}`; const object = new TestObject(); object.set('name', 'object'); await object.save(); @@ -100,7 +148,23 @@ describe_only_db('mongo')('Parse.Query with comment testing', () => { explain: true, comment: comment, }); - const result = await database.collection('system.profile').findOne({}, { sort: { ts: -1 } }); + + // Wait for profile entry to appear with retry logic + let result; + const maxRetries = 10; + const retryDelay = 100; + for (let i = 0; i < maxRetries; i++) { + result = await database.collection('system.profile').findOne( + { 'command.explain.comment': comment }, + { sort: { ts: -1 } } + ); + if (result) { + break; + } + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + + expect(result).toBeDefined(); expect(result.command.explain.comment).toBe(comment); }); }); diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index f0c746065d..98b4938433 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -517,7 +517,7 @@ describe('Parse.Relation testing', () => { // Parent object is un-fetched, so this will call /1/classes/Car instead // of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }. - return query.find(origWheel.id); + return query.find(); }) .then(function (results) { // Make sure this is Wheel and not Car. diff --git a/spec/helper.js b/spec/helper.js index 9c31053421..d2f26e584c 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -7,6 +7,15 @@ const { SpecReporter } = require('jasmine-spec-reporter'); const SchemaCache = require('../lib/Adapters/Cache/SchemaCache').default; const { sleep, Connections } = require('../lib/TestUtils'); +const originalFetch = global.fetch; +let fetchWasMocked = false; + +global.restoreFetch = () => { + global.fetch = originalFetch; + fetchWasMocked = false; +} + + // Ensure localhost resolves to ipv4 address first on node v17+ if (dns.setDefaultResultOrder) { dns.setDefaultResultOrder('ipv4first'); @@ -205,6 +214,7 @@ const reconfigureServer = async (changedConfiguration = {}) => { }; beforeAll(async () => { + global.restoreFetch(); await reconfigureServer(); Parse.initialize('test', 'test', 'test'); Parse.serverURL = serverURL; @@ -212,7 +222,18 @@ beforeAll(async () => { Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1); }); +beforeEach(async () => { + if(fetchWasMocked) { + global.restoreFetch(); + } +}); + global.afterEachFn = async () => { + // Restore fetch to prevent mock pollution between tests (only if it was mocked) + if (fetchWasMocked) { + global.restoreFetch(); + } + Parse.Cloud._removeAllHooks(); Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(); defaults.protectedFields = { _User: { '*': ['email'] } }; @@ -251,6 +272,7 @@ global.afterEachFn = async () => { afterEach(global.afterEachFn); afterAll(() => { + global.restoreFetch(); global.displayTestStats(); }); @@ -388,9 +410,22 @@ function mockShortLivedAuth() { } function mockFetch(mockResponses) { - global.fetch = jasmine.createSpy('fetch').and.callFake((url, options = { }) => { + const spy = jasmine.createSpy('fetch'); + fetchWasMocked = true; // Track that fetch was mocked for cleanup + + global.fetch = (url, options = {}) => { + // Allow requests to the Parse Server to pass through WITHOUT recording in spy + // This prevents tests from failing when they check that fetch wasn't called + // but the Parse SDK makes internal requests to the Parse Server + if (typeof url === 'string' && url.includes(serverURL)) { + return originalFetch(url, options); + } + + // Record non-Parse-Server calls in the spy + spy(url, options); + options.method ||= 'GET'; - const mockResponse = mockResponses.find( + const mockResponse = mockResponses?.find( (mock) => mock.url === url && mock.method === options.method ); @@ -402,7 +437,11 @@ function mockFetch(mockResponses) { ok: false, statusText: 'Unknown URL or method', }); - }); + }; + + // Expose spy methods for test assertions + global.fetch.calls = spy.calls; + global.fetch.and = spy.and; } diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index f7a94cd221..0d66c0a135 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -175,12 +175,10 @@ describe('Vulnerabilities', () => { }, }); }); - await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith( - new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - 'Prohibited keyword in request data: {"key":"constructor"}.' - ) - ); + // The new Parse SDK handles prototype pollution prevention in .set() + // so no error is thrown, but the object prototype should not be polluted + await new Parse.Object('TestObject').save(); + expect(Object.prototype.dummy).toBeUndefined(); }); it('denies creating global config with polluted data', async () => { @@ -270,12 +268,10 @@ describe('Vulnerabilities', () => { res.json({ success: object }); }); await Parse.Hooks.createTrigger('TestObject', 'beforeSave', hookServerURL + '/BeforeSave'); - await expectAsync(new Parse.Object('TestObject').save()).toBeRejectedWith( - new Parse.Error( - Parse.Error.INVALID_KEY_NAME, - 'Prohibited keyword in request data: {"key":"constructor"}.' - ) - ); + // The new Parse SDK handles prototype pollution prevention in .set() + // so no error is thrown, but the object prototype should not be polluted + await new Parse.Object('TestObject').save(); + expect(Object.prototype.dummy).toBeUndefined(); await new Promise(resolve => server.close(resolve)); });