diff --git a/package-lock.json b/package-lock.json index 7823a641de..f85043fad5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -99,7 +99,7 @@ "node-abort-controller": "3.1.1", "node-fetch": "3.2.10", "nyc": "17.1.0", - "prettier": "2.0.5", + "prettier": "3.5.3", "semantic-release": "24.2.1", "typescript": "5.8.2", "yaml": "2.7.0" @@ -18401,15 +18401,19 @@ } }, "node_modules/prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, + "license": "MIT", "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/pretty-ms": { @@ -35040,9 +35044,9 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true }, "pretty-ms": { diff --git a/package.json b/package.json index 217cedd0ad..9f0638a495 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "node-abort-controller": "3.1.1", "node-fetch": "3.2.10", "nyc": "17.1.0", - "prettier": "2.0.5", + "prettier": "3.5.3", "semantic-release": "24.2.1", "typescript": "5.8.2", "yaml": "2.7.0" diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 1525147a40..da0ebdc96c 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -317,59 +317,62 @@ describe('AudiencesRouter', () => { ); }); - it_id('af1111b5-3251-4b40-8f06-fb0fc624fa91')(it_exclude_dbs(['postgres']))('should support legacy parse.com audience fields', done => { - const database = Config.get(Parse.applicationId).database.adapter.database; - const now = new Date(); - Parse._request( - 'POST', - 'push_audiences', - { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) }, - { useMasterKey: true } - ).then(audience => { - database - .collection('test__Audience') - .updateOne( - { _id: audience.objectId }, - { - $set: { - times_used: 1, - _last_used: now, - }, - } - ) - .then(result => { - expect(result).toBeTruthy(); + it_id('af1111b5-3251-4b40-8f06-fb0fc624fa91')(it_exclude_dbs(['postgres']))( + 'should support legacy parse.com audience fields', + done => { + const database = Config.get(Parse.applicationId).database.adapter.database; + const now = new Date(); + Parse._request( + 'POST', + 'push_audiences', + { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) }, + { useMasterKey: true } + ).then(audience => { + database + .collection('test__Audience') + .updateOne( + { _id: audience.objectId }, + { + $set: { + times_used: 1, + _last_used: now, + }, + } + ) + .then(result => { + expect(result).toBeTruthy(); - database - .collection('test__Audience') - .find({ _id: audience.objectId }) - .toArray() - .then(rows => { - expect(rows[0]['times_used']).toEqual(1); - expect(rows[0]['_last_used']).toEqual(now); - Parse._request( - 'GET', - 'push_audiences/' + audience.objectId, - {}, - { useMasterKey: true } - ) - .then(audience => { - expect(audience.name).toEqual('My Audience'); - expect(audience.query.deviceType).toEqual('ios'); - expect(audience.timesUsed).toEqual(1); - expect(audience.lastUsed).toEqual(now.toISOString()); - done(); - }) - .catch(error => { - done.fail(error); - }); - }) - .catch(error => { - done.fail(error); - }); - }); - }); - }); + database + .collection('test__Audience') + .find({ _id: audience.objectId }) + .toArray() + .then(rows => { + expect(rows[0]['times_used']).toEqual(1); + expect(rows[0]['_last_used']).toEqual(now); + Parse._request( + 'GET', + 'push_audiences/' + audience.objectId, + {}, + { useMasterKey: true } + ) + .then(audience => { + expect(audience.name).toEqual('My Audience'); + expect(audience.query.deviceType).toEqual('ios'); + expect(audience.timesUsed).toEqual(1); + expect(audience.lastUsed).toEqual(now.toISOString()); + done(); + }) + .catch(error => { + done.fail(error); + }); + }) + .catch(error => { + done.fail(error); + }); + }); + }); + } + ); it('should be able to search on audiences', done => { Parse._request( diff --git a/spec/Auth.spec.js b/spec/Auth.spec.js index a055cda5bc..a718a1c2d7 100644 --- a/spec/Auth.spec.js +++ b/spec/Auth.spec.js @@ -239,16 +239,10 @@ describe('extendSessionOnUse', () => { const { shouldUpdateSessionExpiry } = require('../lib/Auth'); let update = new Date(Date.now() - 86410 * 1000); - const res = shouldUpdateSessionExpiry( - { sessionLength: 86460 }, - { updatedAt: update } - ); + const res = shouldUpdateSessionExpiry({ sessionLength: 86460 }, { updatedAt: update }); update = new Date(Date.now() - 43210 * 1000); - const res2 = shouldUpdateSessionExpiry( - { sessionLength: 86460 }, - { updatedAt: update } - ); + const res2 = shouldUpdateSessionExpiry({ sessionLength: 86460 }, { updatedAt: update }); expect(res).toBe(true); expect(res2).toBe(false); diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index a2defde3e5..d8340c60f6 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -1143,9 +1143,9 @@ describe('phant auth adapter', () => { auth: { phantauth: { enableInsecureAuth: true, - } - } - }) + }, + }, + }); const authData = { id: 'fakeid', access_token: 'sometoken', @@ -1218,26 +1218,29 @@ describe('facebook limited auth adapter', () => { } }); - it_id('7bfa55ab-8fd7-4526-992e-6de3df16bf9c')(it)('should use algorithm from key header to verify id_token (facebook.com)', async () => { - const fakeClaim = { - iss: 'https://www.facebook.com', - aud: 'secret', - exp: Date.now(), - sub: 'the_user_id', - }; - const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; - const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; - spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header); - spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); - spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + it_id('7bfa55ab-8fd7-4526-992e-6de3df16bf9c')(it)( + 'should use algorithm from key header to verify id_token (facebook.com)', + async () => { + const fakeClaim = { + iss: 'https://www.facebook.com', + aud: 'secret', + exp: Date.now(), + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; + spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken.header); + spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); - const result = await facebook.validateAuthData( - { id: 'the_user_id', token: 'the_token' }, - { clientId: 'secret' } - ); - expect(result).toEqual(fakeClaim); - expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg); - }); + const result = await facebook.validateAuthData( + { id: 'the_user_id', token: 'the_token' }, + { clientId: 'secret' } + ); + expect(result).toEqual(fakeClaim); + expect(jwt.verify.calls.first().args[2].algorithms).toEqual(fakeDecodedToken.header.alg); + } + ); it('should not verify invalid id_token', async () => { const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; @@ -1268,89 +1271,101 @@ describe('facebook limited auth adapter', () => { } }); - it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)('using client id as string) should verify id_token (facebook.com)', async () => { - const fakeClaim = { - iss: 'https://www.facebook.com', - aud: 'secret', - exp: Date.now(), - sub: 'the_user_id', - }; - const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; - const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; - spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); - spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); - spyOn(jwt, 'verify').and.callFake(() => fakeClaim); - - const result = await facebook.validateAuthData( - { id: 'the_user_id', token: 'the_token' }, - { clientId: 'secret' } - ); - expect(result).toEqual(fakeClaim); - }); - - it_id('c521a272-2ac2-4d8b-b5ed-ea250336d8b1')(it)('(using client id as array) should verify id_token (facebook.com)', async () => { - const fakeClaim = { - iss: 'https://www.facebook.com', - aud: 'secret', - exp: Date.now(), - sub: 'the_user_id', - }; - const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; - const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; - spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); - spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); - spyOn(jwt, 'verify').and.callFake(() => fakeClaim); - - const result = await facebook.validateAuthData( - { id: 'the_user_id', token: 'the_token' }, - { clientId: ['secret'] } - ); - expect(result).toEqual(fakeClaim); - }); - - it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)('(using client id as array with multiple items) should verify id_token (facebook.com)', async () => { - const fakeClaim = { - iss: 'https://www.facebook.com', - aud: 'secret', - exp: Date.now(), - sub: 'the_user_id', - }; - const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; - const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; - spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); - spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); - spyOn(jwt, 'verify').and.callFake(() => fakeClaim); - - const result = await facebook.validateAuthData( - { id: 'the_user_id', token: 'the_token' }, - { clientId: ['secret', 'secret 123'] } - ); - expect(result).toEqual(fakeClaim); - }); - - it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)('(using client id as string) should throw error with with invalid jwt issuer (facebook.com)', async () => { - const fakeClaim = { - iss: 'https://not.facebook.com', - sub: 'the_user_id', - }; - const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; - const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; - spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); - spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); - spyOn(jwt, 'verify').and.callFake(() => fakeClaim); - - try { - await facebook.validateAuthData( + it_id('4bcb1a1a-11f8-4e12-a3f6-73f7e25e355a')(it)( + 'using client id as string) should verify id_token (facebook.com)', + async () => { + const fakeClaim = { + iss: 'https://www.facebook.com', + aud: 'secret', + exp: Date.now(), + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; + spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); + spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + + const result = await facebook.validateAuthData( { id: 'the_user_id', token: 'the_token' }, { clientId: 'secret' } ); - fail(); - } catch (e) { - expect(e.message).toBe( - 'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com' + expect(result).toEqual(fakeClaim); + } + ); + + it_id('c521a272-2ac2-4d8b-b5ed-ea250336d8b1')(it)( + '(using client id as array) should verify id_token (facebook.com)', + async () => { + const fakeClaim = { + iss: 'https://www.facebook.com', + aud: 'secret', + exp: Date.now(), + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; + spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); + spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + + const result = await facebook.validateAuthData( + { id: 'the_user_id', token: 'the_token' }, + { clientId: ['secret'] } ); + expect(result).toEqual(fakeClaim); } - }); + ); + + it_id('e3f16404-18e9-4a87-a555-4710cfbdac67')(it)( + '(using client id as array with multiple items) should verify id_token (facebook.com)', + async () => { + const fakeClaim = { + iss: 'https://www.facebook.com', + aud: 'secret', + exp: Date.now(), + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; + spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); + spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + + const result = await facebook.validateAuthData( + { id: 'the_user_id', token: 'the_token' }, + { clientId: ['secret', 'secret 123'] } + ); + expect(result).toEqual(fakeClaim); + } + ); + + it_id('549c33a1-3a6b-4732-8cf6-8f010ad4569c')(it)( + '(using client id as string) should throw error with with invalid jwt issuer (facebook.com)', + async () => { + const fakeClaim = { + iss: 'https://not.facebook.com', + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; + spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); + spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + + try { + await facebook.validateAuthData( + { id: 'the_user_id', token: 'the_token' }, + { clientId: 'secret' } + ); + fail(); + } catch (e) { + expect(e.message).toBe( + 'id token not issued by correct OpenID provider - expected: https://www.facebook.com | from: https://not.facebook.com' + ); + } + } + ); // TODO: figure out a way to generate our own facebook signed tokens, perhaps with a parse facebook account // and a private key @@ -1459,28 +1474,31 @@ describe('facebook limited auth adapter', () => { } }); - it_id('c194d902-e697-46c9-a303-82c2d914473c')(it)('should throw error with with invalid user id (facebook.com)', async () => { - const fakeClaim = { - iss: 'https://www.facebook.com', - aud: 'invalid_client_id', - sub: 'a_different_user_id', - }; - const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; - const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; - spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); - spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); - spyOn(jwt, 'verify').and.callFake(() => fakeClaim); - - try { - await facebook.validateAuthData( - { id: 'the_user_id', token: 'the_token' }, - { clientId: 'secret' } - ); - fail(); - } catch (e) { - expect(e.message).toBe('auth data is invalid for this user.'); + it_id('c194d902-e697-46c9-a303-82c2d914473c')(it)( + 'should throw error with with invalid user id (facebook.com)', + async () => { + const fakeClaim = { + iss: 'https://www.facebook.com', + aud: 'invalid_client_id', + sub: 'a_different_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + const fakeSigningKey = { kid: '123', rsaPublicKey: 'the_rsa_public_key' }; + spyOn(authUtils, 'getHeaderFromToken').and.callFake(() => fakeDecodedToken); + spyOn(authUtils, 'getSigningKey').and.resolveTo(fakeSigningKey); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + + try { + await facebook.validateAuthData( + { id: 'the_user_id', token: 'the_token' }, + { clientId: 'secret' } + ); + fail(); + } catch (e) { + expect(e.message).toBe('auth data is invalid for this user.'); + } } - }); + ); }); describe('OTP TOTP auth adatper', () => { diff --git a/spec/AuthenticationAdaptersV2.spec.js b/spec/AuthenticationAdaptersV2.spec.js index 7301ab54c1..5ec30a65f6 100644 --- a/spec/AuthenticationAdaptersV2.spec.js +++ b/spec/AuthenticationAdaptersV2.spec.js @@ -361,11 +361,11 @@ describe('Auth Adapter features', () => { break; } } - expect(afterSpy).toHaveBeenCalledWith( - { id: 'modernAdapter3Data' }, - undefined, - { ip: '127.0.0.1', user, master: false }, - ); + expect(afterSpy).toHaveBeenCalledWith({ id: 'modernAdapter3Data' }, undefined, { + ip: '127.0.0.1', + user, + master: false, + }); expect(spy).toHaveBeenCalled(); }); @@ -488,7 +488,7 @@ describe('Auth Adapter features', () => { await user.save({ authData: { - baseAdapter: { id: 'baseAdapter', token: "sometoken1" }, + baseAdapter: { id: 'baseAdapter', token: 'sometoken1' }, }, }); @@ -497,7 +497,7 @@ describe('Auth Adapter features', () => { const user2 = new Parse.User(); await user2.save({ authData: { - baseAdapter: { id: 'baseAdapter', token: "sometoken2" }, + baseAdapter: { id: 'baseAdapter', token: 'sometoken2' }, }, }); diff --git a/spec/CLI.spec.js b/spec/CLI.spec.js index e131a6def5..76f8e3952d 100644 --- a/spec/CLI.spec.js +++ b/spec/CLI.spec.js @@ -219,7 +219,11 @@ describe('execution', () => { childProcess.stdout.on('data', data => { data = data.toString(); aggregatedData.push(data); - if (requiredData.every(required => aggregatedData.some(aggregated => aggregated.includes(required)))) { + if ( + requiredData.every(required => + aggregatedData.some(aggregated => aggregated.includes(required)) + ) + ) { done(); } }); @@ -267,70 +271,79 @@ describe('execution', () => { handleError(childProcess, done); }); - it_id('d7165081-b133-4cba-901b-19128ce41301')(it)('should start Parse Server with GraphQL', async done => { - const env = { ...process.env }; - env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation'; - childProcess = spawn( - binPath, - [ - '--appId', - 'test', - '--masterKey', - 'test', - '--databaseURI', - databaseURI, - '--port', - '1340', - '--mountGraphQL', - ], - { env } - ); - handleStdout(childProcess, done, aggregatedData, [ - 'parse-server running on', - 'GraphQL running on', - ]); - handleStderr(childProcess, done); - handleError(childProcess, done); - }); + it_id('d7165081-b133-4cba-901b-19128ce41301')(it)( + 'should start Parse Server with GraphQL', + async done => { + const env = { ...process.env }; + env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation'; + childProcess = spawn( + binPath, + [ + '--appId', + 'test', + '--masterKey', + 'test', + '--databaseURI', + databaseURI, + '--port', + '1340', + '--mountGraphQL', + ], + { env } + ); + handleStdout(childProcess, done, aggregatedData, [ + 'parse-server running on', + 'GraphQL running on', + ]); + handleStderr(childProcess, done); + handleError(childProcess, done); + } + ); - it_id('2769cdb4-ce8a-484d-8a91-635b5894ba7e')(it)('should start Parse Server with GraphQL and Playground', async done => { - const env = { ...process.env }; - env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation'; - childProcess = spawn( - binPath, - [ - '--appId', - 'test', - '--masterKey', - 'test', - '--databaseURI', - databaseURI, - '--port', - '1341', - '--mountGraphQL', - '--mountPlayground', - ], - { env } - ); - handleStdout(childProcess, done, aggregatedData, [ - 'parse-server running on', - 'Playground running on', - 'GraphQL running on', - ]); - handleStderr(childProcess, done); - handleError(childProcess, done); - }); + it_id('2769cdb4-ce8a-484d-8a91-635b5894ba7e')(it)( + 'should start Parse Server with GraphQL and Playground', + async done => { + const env = { ...process.env }; + env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation'; + childProcess = spawn( + binPath, + [ + '--appId', + 'test', + '--masterKey', + 'test', + '--databaseURI', + databaseURI, + '--port', + '1341', + '--mountGraphQL', + '--mountPlayground', + ], + { env } + ); + handleStdout(childProcess, done, aggregatedData, [ + 'parse-server running on', + 'Playground running on', + 'GraphQL running on', + ]); + handleStderr(childProcess, done); + handleError(childProcess, done); + } + ); - it_id('23caddd7-bfea-4869-8bd4-0f2cd283c8bd')(it)('can start Parse Server with auth via CLI', done => { - const env = { ...process.env }; - env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation'; - childProcess = spawn( - binPath, - ['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'], - { env } - ); - handleStdout(childProcess, done, aggregatedData, ['parse-server running on']); - handleStderr(childProcess, done); - handleError(childProcess, done); - }); + it_id('23caddd7-bfea-4869-8bd4-0f2cd283c8bd')(it)( + 'can start Parse Server with auth via CLI', + done => { + const env = { ...process.env }; + env.NODE_OPTIONS = '--dns-result-order=ipv4first --trace-deprecation'; + childProcess = spawn( + binPath, + ['--databaseURI', databaseURI, './spec/configs/CLIConfigAuth.json'], + { env } + ); + handleStdout(childProcess, done, aggregatedData, ['parse-server running on']); + handleStderr(childProcess, done); + handleError(childProcess, done); + } + ); }); diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 11ccc82766..9b33ddc213 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -734,37 +734,43 @@ describe('cloud validator', () => { done(); }); - it_id('893eec0c-41bd-4adf-8f0a-306087ad8d61')(it)('basic beforeSave Parse.Config skipWithMasterKey', async () => { - Parse.Cloud.beforeSave( - Parse.Config, - () => { - throw 'beforeSaveFile should have resolved using master key.'; - }, - { - skipWithMasterKey: true, - } - ); - const config = await testConfig(); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - }); - - it_id('91e739a4-6a38-405c-8f83-f36d48220734')(it)('basic afterSave Parse.Config skipWithMasterKey', async () => { - Parse.Cloud.afterSave( - Parse.Config, - () => { - throw 'beforeSaveFile should have resolved using master key.'; - }, - { - skipWithMasterKey: true, - } - ); - const config = await testConfig(); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - }); + it_id('893eec0c-41bd-4adf-8f0a-306087ad8d61')(it)( + 'basic beforeSave Parse.Config skipWithMasterKey', + async () => { + Parse.Cloud.beforeSave( + Parse.Config, + () => { + throw 'beforeSaveFile should have resolved using master key.'; + }, + { + skipWithMasterKey: true, + } + ); + const config = await testConfig(); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + } + ); + + it_id('91e739a4-6a38-405c-8f83-f36d48220734')(it)( + 'basic afterSave Parse.Config skipWithMasterKey', + async () => { + Parse.Cloud.afterSave( + Parse.Config, + () => { + throw 'beforeSaveFile should have resolved using master key.'; + }, + { + skipWithMasterKey: true, + } + ); + const config = await testConfig(); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + } + ); it('beforeSave validateMasterKey and skipWithMasterKey fail', async function (done) { Parse.Cloud.beforeSave( @@ -1531,23 +1537,29 @@ describe('cloud validator', () => { } }); - it_id('32ca1a99-7f2b-429d-a7cf-62b6661d0af6')(it)('validate beforeSave Parse.Config', async () => { - Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorSuccess); - const config = await testConfig(); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - }); + it_id('32ca1a99-7f2b-429d-a7cf-62b6661d0af6')(it)( + 'validate beforeSave Parse.Config', + async () => { + Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorSuccess); + const config = await testConfig(); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + } + ); - it_id('c84d11e7-d09c-4843-ad98-f671511bf612')(it)('validate beforeSave Parse.Config fail', async () => { - Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorFail); - try { - await testConfig(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + it_id('c84d11e7-d09c-4843-ad98-f671511bf612')(it)( + 'validate beforeSave Parse.Config fail', + async () => { + Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorFail); + try { + await testConfig(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + } } - }); + ); it_id('b18b9a6a-0e35-4b60-9771-30f53501df3c')(it)('validate afterSave Parse.Config', async () => { Parse.Cloud.afterSave(Parse.Config, () => {}, validatorSuccess); @@ -1557,15 +1569,18 @@ describe('cloud validator', () => { expect(config.get('number')).toBe(12); }); - it_id('ef761222-1758-4614-b984-da84d73fc10c')(it)('validate afterSave Parse.Config fail', async () => { - Parse.Cloud.afterSave(Parse.Config, () => {}, validatorFail); - try { - await testConfig(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + it_id('ef761222-1758-4614-b984-da84d73fc10c')(it)( + 'validate afterSave Parse.Config fail', + async () => { + Parse.Cloud.afterSave(Parse.Config, () => {}, validatorFail); + try { + await testConfig(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + } } - }); + ); it('Should have validator', async done => { Parse.Cloud.define( diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index e26a310655..59dea5b3f3 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3,8 +3,8 @@ const Config = require('../lib/Config'); const Parse = require('parse/node'); const ParseServer = require('../lib/index').ParseServer; const request = require('../lib/request'); -const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter') - .InMemoryCacheAdapter; +const InMemoryCacheAdapter = + require('../lib/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter; const mockAdapter = { createFile: async filename => ({ @@ -1588,13 +1588,13 @@ describe('Cloud Code', () => { obj.set('points', 10); obj.set('num', 10); obj.save(null, { useMasterKey: true }).then(function () { - Parse.Cloud.run('cloudIncrementClassFunction', { objectId: obj.id }).then(function ( - savedObj - ) { - expect(savedObj.get('num')).toEqual(1); - expect(savedObj.get('points')).toEqual(0); - done(); - }); + Parse.Cloud.run('cloudIncrementClassFunction', { objectId: obj.id }).then( + function (savedObj) { + expect(savedObj.get('num')).toEqual(1); + expect(savedObj.get('points')).toEqual(0); + done(); + } + ); }); }); @@ -1768,12 +1768,8 @@ describe('Cloud Code', () => { obj.increment('objectField.number', 10); await obj.save(); - const [ - , - , - , - /* className */ /* schema */ /* query */ update, - ] = adapter.findOneAndUpdate.calls.first().args; + const [, , , /* className */ /* schema */ /* query */ update] = + adapter.findOneAndUpdate.calls.first().args; expect(update).toEqual({ 'objectField.number': { __op: 'Increment', amount: 10 }, foo: 'baz', @@ -2919,55 +2915,61 @@ describe('afterFind hooks', () => { }).toThrow('Only the _Session class is allowed for the afterLogout trigger.'); }); - it_id('c16159b5-e8ee-42d5-8fe3-e2f7c006881d')(it)('should skip afterFind hooks for aggregate', done => { - const hook = { - method: function () { - return Promise.reject(); - }, - }; - spyOn(hook, 'method').and.callThrough(); - Parse.Cloud.afterFind('MyObject', hook.method); - const obj = new Parse.Object('MyObject'); - const pipeline = [ - { - $group: { _id: {} }, - }, - ]; - obj - .save() - .then(() => { - const query = new Parse.Query('MyObject'); - return query.aggregate(pipeline); - }) - .then(results => { - expect(results[0].objectId).toEqual(null); - expect(hook.method).not.toHaveBeenCalled(); - done(); - }); - }); + it_id('c16159b5-e8ee-42d5-8fe3-e2f7c006881d')(it)( + 'should skip afterFind hooks for aggregate', + done => { + const hook = { + method: function () { + return Promise.reject(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.afterFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + const pipeline = [ + { + $group: { _id: {} }, + }, + ]; + obj + .save() + .then(() => { + const query = new Parse.Query('MyObject'); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results[0].objectId).toEqual(null); + expect(hook.method).not.toHaveBeenCalled(); + done(); + }); + } + ); - it_id('ca55c90d-36db-422c-9060-a30583ce5224')(it)('should skip afterFind hooks for distinct', done => { - const hook = { - method: function () { - return Promise.reject(); - }, - }; - spyOn(hook, 'method').and.callThrough(); - Parse.Cloud.afterFind('MyObject', hook.method); - const obj = new Parse.Object('MyObject'); - obj.set('score', 10); - obj - .save() - .then(() => { - const query = new Parse.Query('MyObject'); - return query.distinct('score'); - }) - .then(results => { - expect(results[0]).toEqual(10); - expect(hook.method).not.toHaveBeenCalled(); - done(); - }); - }); + it_id('ca55c90d-36db-422c-9060-a30583ce5224')(it)( + 'should skip afterFind hooks for distinct', + done => { + const hook = { + method: function () { + return Promise.reject(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.afterFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + obj.set('score', 10); + obj + .save() + .then(() => { + const query = new Parse.Query('MyObject'); + return query.distinct('score'); + }) + .then(results => { + expect(results[0]).toEqual(10); + expect(hook.method).not.toHaveBeenCalled(); + done(); + }); + } + ); it('should throw error if context header is malformed', async () => { let calledBefore = false; @@ -3033,37 +3035,40 @@ describe('afterFind hooks', () => { expect(calledAfter).toBe(false); }); - it_id('55ef1741-cf72-4a7c-a029-00cb75f53233')(it)('should expose context in beforeSave/afterSave via header', async () => { - let calledBefore = false; - let calledAfter = false; - Parse.Cloud.beforeSave('TestObject', req => { - expect(req.object.get('foo')).toEqual('bar'); - expect(req.context.otherKey).toBe(1); - expect(req.context.key).toBe('value'); - calledBefore = true; - }); - Parse.Cloud.afterSave('TestObject', req => { - expect(req.object.get('foo')).toEqual('bar'); - expect(req.context.otherKey).toBe(1); - expect(req.context.key).toBe('value'); - calledAfter = true; - }); - const req = request({ - method: 'POST', - url: 'http://localhost:8378/1/classes/TestObject', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', - }, - body: { - foo: 'bar', - }, - }); - await req; - expect(calledBefore).toBe(true); - expect(calledAfter).toBe(true); - }); + it_id('55ef1741-cf72-4a7c-a029-00cb75f53233')(it)( + 'should expose context in beforeSave/afterSave via header', + async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', + }, + body: { + foo: 'bar', + }, + }); + await req; + expect(calledBefore).toBe(true); + expect(calledAfter).toBe(true); + } + ); it('should override header context with body context in beforeSave/afterSave', async () => { let calledBefore = false; @@ -3347,20 +3352,23 @@ describe('beforeLogin hook', () => { expect(response).toEqual(error); }); - it_id('5656d6d7-65ef-43d1-8ca6-6942ae3614d5')(it)('should have expected data in request in beforeLogin', async done => { - Parse.Cloud.beforeLogin(req => { - expect(req.object).toBeDefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - }); + it_id('5656d6d7-65ef-43d1-8ca6-6942ae3614d5')(it)( + 'should have expected data in request in beforeLogin', + async done => { + Parse.Cloud.beforeLogin(req => { + expect(req.object).toBeDefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + }); - await Parse.User.signUp('tupac', 'shakur'); - await Parse.User.logIn('tupac', 'shakur'); - done(); - }); + await Parse.User.signUp('tupac', 'shakur'); + await Parse.User.logIn('tupac', 'shakur'); + done(); + } + ); it('afterFind should not be triggered when saving an object', async () => { let beforeSaves = 0; @@ -3464,20 +3472,23 @@ describe('afterLogin hook', () => { done(); }); - it_id('e86155c4-62e1-4c6e-ab4a-9ac6c87c60f2')(it)('should have expected data in request in afterLogin', async done => { - Parse.Cloud.afterLogin(req => { - expect(req.object).toBeDefined(); - expect(req.user).toBeDefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - }); + it_id('e86155c4-62e1-4c6e-ab4a-9ac6c87c60f2')(it)( + 'should have expected data in request in afterLogin', + async done => { + Parse.Cloud.afterLogin(req => { + expect(req.object).toBeDefined(); + expect(req.user).toBeDefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + }); - await Parse.User.signUp('testuser', 'p@ssword'); - await Parse.User.logIn('testuser', 'p@ssword'); - done(); - }); + await Parse.User.signUp('testuser', 'p@ssword'); + await Parse.User.logIn('testuser', 'p@ssword'); + done(); + } + ); it('context options should override _context object property when saving a new object', async () => { Parse.Cloud.beforeSave('TestObject', req => { @@ -3502,9 +3513,8 @@ describe('afterLogin hook', () => { 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Cloud-Context': '{"a":"a"}', }, - body: JSON.stringify({_context: { hello: 'world' }}), + body: JSON.stringify({ _context: { hello: 'world' } }), }); - }); it('should have access to context when saving a new object', async () => { @@ -3934,61 +3944,70 @@ describe('Cloud Config hooks', () => { return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true }); } - it_id('997fe20a-96f7-454a-a5b0-c155b8d02f05')(it)('beforeSave(Parse.Config) can run hook with new config', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, (req) => { - expect(req.object).toBeDefined(); - expect(req.original).toBeUndefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - const config = req.object; + it_id('997fe20a-96f7-454a-a5b0-c155b8d02f05')(it)( + 'beforeSave(Parse.Config) can run hook with new config', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, req => { + expect(req.object).toBeDefined(); + expect(req.original).toBeUndefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + const config = req.object; + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + count += 1; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); expect(config.get('internal')).toBe('i'); expect(config.get('string')).toBe('s'); expect(config.get('number')).toBe(12); - count += 1; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); + expect(count).toBe(1); + } + ); - it_id('06a9b66c-ffb4-43d1-a025-f7d2192500e7')(it)('beforeSave(Parse.Config) can run hook with existing config', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, (req) => { - if (count === 0) { - expect(req.object.get('number')).toBe(12); - expect(req.original).toBeUndefined(); - } - if (count === 1) { - expect(req.object.get('number')).toBe(13); - expect(req.original.get('number')).toBe(12); - } - count += 1; - }); - await testConfig(); - await Parse.Config.save({ number: 13 }); - expect(count).toBe(2); - }); + it_id('06a9b66c-ffb4-43d1-a025-f7d2192500e7')(it)( + 'beforeSave(Parse.Config) can run hook with existing config', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, req => { + if (count === 0) { + expect(req.object.get('number')).toBe(12); + expect(req.original).toBeUndefined(); + } + if (count === 1) { + expect(req.object.get('number')).toBe(13); + expect(req.original.get('number')).toBe(12); + } + count += 1; + }); + await testConfig(); + await Parse.Config.save({ number: 13 }); + expect(count).toBe(2); + } + ); - it_id('ca76de8e-671b-4c2d-9535-bd28a855fa1a')(it)('beforeSave(Parse.Config) should not change config if nothing is returned', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, () => { - count += 1; - return; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); + it_id('ca76de8e-671b-4c2d-9535-bd28a855fa1a')(it)( + 'beforeSave(Parse.Config) should not change config if nothing is returned', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, () => { + count += 1; + return; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + expect(count).toBe(1); + } + ); it('beforeSave(Parse.Config) throw custom error', async () => { Parse.Cloud.beforeSave(Parse.Config, () => { @@ -4029,60 +4048,69 @@ describe('Cloud Config hooks', () => { } }); - it_id('3e7a75c0-6c2e-4c7e-b042-6eb5f23acf94')(it)('afterSave(Parse.Config) can run hook with new config', async () => { - let count = 0; - Parse.Cloud.afterSave(Parse.Config, (req) => { - expect(req.object).toBeDefined(); - expect(req.original).toBeUndefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - const config = req.object; + it_id('3e7a75c0-6c2e-4c7e-b042-6eb5f23acf94')(it)( + 'afterSave(Parse.Config) can run hook with new config', + async () => { + let count = 0; + Parse.Cloud.afterSave(Parse.Config, req => { + expect(req.object).toBeDefined(); + expect(req.original).toBeUndefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + const config = req.object; + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + count += 1; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); expect(config.get('internal')).toBe('i'); expect(config.get('string')).toBe('s'); expect(config.get('number')).toBe(12); - count += 1; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); - - it_id('5cffb28a-2924-4857-84bb-f5778d80372a')(it)('afterSave(Parse.Config) can run hook with existing config', async () => { - let count = 0; - Parse.Cloud.afterSave(Parse.Config, (req) => { - if (count === 0) { - expect(req.object.get('number')).toBe(12); - expect(req.original).toBeUndefined(); - } - if (count === 1) { - expect(req.object.get('number')).toBe(13); - expect(req.original.get('number')).toBe(12); - } - count += 1; - }); - await testConfig(); - await Parse.Config.save({ number: 13 }); - expect(count).toBe(2); - }); + expect(count).toBe(1); + } + ); - it_id('49883992-ce91-4797-85f9-7cce1f819407')(it)('afterSave(Parse.Config) should throw error', async () => { - Parse.Cloud.afterSave(Parse.Config, () => { - throw new Parse.Error(400, 'It should fail'); - }); - try { + it_id('5cffb28a-2924-4857-84bb-f5778d80372a')(it)( + 'afterSave(Parse.Config) can run hook with existing config', + async () => { + let count = 0; + Parse.Cloud.afterSave(Parse.Config, req => { + if (count === 0) { + expect(req.object.get('number')).toBe(12); + expect(req.original).toBeUndefined(); + } + if (count === 1) { + expect(req.object.get('number')).toBe(13); + expect(req.original.get('number')).toBe(12); + } + count += 1; + }); await testConfig(); - fail('error should have thrown'); - } catch (e) { - expect(e.code).toBe(400); - expect(e.message).toBe('It should fail'); + await Parse.Config.save({ number: 13 }); + expect(count).toBe(2); } - }); + ); + + it_id('49883992-ce91-4797-85f9-7cce1f819407')(it)( + 'afterSave(Parse.Config) should throw error', + async () => { + Parse.Cloud.afterSave(Parse.Config, () => { + throw new Parse.Error(400, 'It should fail'); + }); + try { + await testConfig(); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(400); + expect(e.message).toBe('It should fail'); + } + } + ); }); describe('sendEmail', () => { diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index a16b52365a..de59b41bb7 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -1,6 +1,6 @@ const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') - .WinstonLoggerAdapter; +const WinstonLoggerAdapter = + require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; const fs = require('fs'); const Config = require('../lib/Config'); @@ -120,23 +120,26 @@ describe('Cloud Code Logger', () => { expect(truncatedString.length).toBe(1015); // truncate length + the string '... (truncated)' }); - it_id('4a009b1f-9203-49ca-8d48-5b45f4eedbdf')(it)('should truncate input and result of long lines', done => { - const longString = fs.readFileSync(loremFile, 'utf8'); - Parse.Cloud.define('aFunction', req => { - return req.params; - }); + it_id('4a009b1f-9203-49ca-8d48-5b45f4eedbdf')(it)( + 'should truncate input and result of long lines', + done => { + const longString = fs.readFileSync(loremFile, 'utf8'); + Parse.Cloud.define('aFunction', req => { + return req.params; + }); - Parse.Cloud.run('aFunction', { longString }) - .then(() => { - const log = spy.calls.mostRecent().args; - expect(log[0]).toEqual('info'); - expect(log[1]).toMatch( - /Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {.*?\(truncated\)$/m - ); - done(); - }) - .then(null, e => done.fail(e)); - }); + Parse.Cloud.run('aFunction', { longString }) + .then(() => { + const log = spy.calls.mostRecent().args; + expect(log[0]).toEqual('info'); + expect(log[1]).toMatch( + /Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {.*?\(truncated\)$/m + ); + done(); + }) + .then(null, e => done.fail(e)); + } + ); it_id('9857e15d-bb18-478d-8a67-fdaad3e89565')(it)('should log an afterSave', done => { Parse.Cloud.afterSave('MyObject', () => {}); @@ -189,41 +192,44 @@ describe('Cloud Code Logger', () => { }); }); - it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)('should log cloud function execution using the custom log level', async done => { - Parse.Cloud.define('aFunction', () => { - return 'it worked!'; - }); + it_id('8088de8a-7cba-4035-8b05-4a903307e674')(it)( + 'should log cloud function execution using the custom log level', + async done => { + Parse.Cloud.define('aFunction', () => { + return 'it worked!'; + }); - Parse.Cloud.define('bFunction', () => { - throw new Error('Failed'); - }); + Parse.Cloud.define('bFunction', () => { + throw new Error('Failed'); + }); - await Parse.Cloud.run('aFunction', { foo: 'bar' }).then(() => { - const log = spy.calls.allArgs().find(log => log[1].startsWith('Ran cloud function '))?.[0]; - expect(log).toEqual('info'); - }); + await Parse.Cloud.run('aFunction', { foo: 'bar' }).then(() => { + const log = spy.calls.allArgs().find(log => log[1].startsWith('Ran cloud function '))?.[0]; + expect(log).toEqual('info'); + }); - await reconfigureServer({ - silent: true, - logLevels: { - cloudFunctionSuccess: 'warn', - cloudFunctionError: 'info', - }, - }); + await reconfigureServer({ + silent: true, + logLevels: { + cloudFunctionSuccess: 'warn', + cloudFunctionError: 'info', + }, + }); - spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); + spy = spyOn(Config.get('test').loggerController.adapter, 'log').and.callThrough(); - try { - await Parse.Cloud.run('bFunction', { foo: 'bar' }); - throw new Error('bFunction should have failed'); - } catch { - const log = spy.calls - .allArgs() - .find(log => log[1].startsWith('Failed running cloud function bFunction for '))?.[0]; - expect(log).toEqual('info'); - done(); + try { + await Parse.Cloud.run('bFunction', { foo: 'bar' }); + throw new Error('bFunction should have failed'); + } catch { + const log = spy.calls + .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', () => {}); @@ -312,19 +318,22 @@ describe('Cloud Code Logger', () => { .then(null, e => done.fail(JSON.stringify(e))); }); - it_id('b86e8168-8370-4730-a4ba-24ca3016ad66')(it)('cloud function should obfuscate password', done => { - Parse.Cloud.define('testFunction', () => { - return 'verify code success'; - }); + it_id('b86e8168-8370-4730-a4ba-24ca3016ad66')(it)( + 'cloud function should obfuscate password', + done => { + Parse.Cloud.define('testFunction', () => { + return 'verify code success'; + }); - Parse.Cloud.run('testFunction', { username: 'hawk', password: '123456' }) - .then(() => { - const entry = spy.calls.mostRecent().args; - expect(entry[2].params.password).toMatch(/\*\*\*\*\*\*\*\*/); - done(); - }) - .then(null, e => done.fail(e)); - }); + Parse.Cloud.run('testFunction', { username: 'hawk', password: '123456' }) + .then(() => { + const entry = spy.calls.mostRecent().args; + expect(entry[2].params.password).toMatch(/\*\*\*\*\*\*\*\*/); + done(); + }) + .then(null, e => done.fail(e)); + } + ); it('should only log once for object not found', async () => { const config = Config.get('test'); diff --git a/spec/DefinedSchemas.spec.js b/spec/DefinedSchemas.spec.js index e3d6fd51fe..3d6f493e66 100644 --- a/spec/DefinedSchemas.spec.js +++ b/spec/DefinedSchemas.spec.js @@ -643,41 +643,44 @@ describe('DefinedSchemas', () => { expect(logger.error).toHaveBeenCalledWith(`Failed to run migrations: ${error.toString()}`); }); - it_id('a18bf4f2-25c8-4de3-b986-19cb1ab163b8')(it)('should perform migration in parallel without failing', async () => { - const server = await reconfigureServer(); - const logger = require('../lib/logger').logger; - spyOn(logger, 'error').and.callThrough(); - const migrationOptions = { - definitions: [ - { - className: 'Test', - fields: { aField: { type: 'String' } }, - indexes: { aField: { aField: 1 } }, - classLevelPermissions: { - create: { requiresAuthentication: true }, + it_id('a18bf4f2-25c8-4de3-b986-19cb1ab163b8')(it)( + 'should perform migration in parallel without failing', + async () => { + const server = await reconfigureServer(); + const logger = require('../lib/logger').logger; + spyOn(logger, 'error').and.callThrough(); + const migrationOptions = { + definitions: [ + { + className: 'Test', + fields: { aField: { type: 'String' } }, + indexes: { aField: { aField: 1 } }, + classLevelPermissions: { + create: { requiresAuthentication: true }, + }, }, - }, - ], - }; - - // Simulate parallel deployment - await Promise.all([ - new DefinedSchemas(migrationOptions, server.config).execute(), - new DefinedSchemas(migrationOptions, server.config).execute(), - new DefinedSchemas(migrationOptions, server.config).execute(), - new DefinedSchemas(migrationOptions, server.config).execute(), - new DefinedSchemas(migrationOptions, server.config).execute(), - ]); - - const testSchema = (await Parse.Schema.all()).find( - ({ className }) => className === migrationOptions.definitions[0].className - ); + ], + }; - expect(testSchema.indexes.aField).toEqual({ aField: 1 }); - expect(testSchema.fields.aField).toEqual({ type: 'String' }); - expect(testSchema.classLevelPermissions.create).toEqual({ requiresAuthentication: true }); - expect(logger.error).toHaveBeenCalledTimes(0); - }); + // Simulate parallel deployment + await Promise.all([ + new DefinedSchemas(migrationOptions, server.config).execute(), + new DefinedSchemas(migrationOptions, server.config).execute(), + new DefinedSchemas(migrationOptions, server.config).execute(), + new DefinedSchemas(migrationOptions, server.config).execute(), + new DefinedSchemas(migrationOptions, server.config).execute(), + ]); + + const testSchema = (await Parse.Schema.all()).find( + ({ className }) => className === migrationOptions.definitions[0].className + ); + + expect(testSchema.indexes.aField).toEqual({ aField: 1 }); + expect(testSchema.fields.aField).toEqual({ type: 'String' }); + expect(testSchema.classLevelPermissions.create).toEqual({ requiresAuthentication: true }); + expect(logger.error).toHaveBeenCalledTimes(0); + } + ); it('should not affect cacheAdapter', async () => { const server = await reconfigureServer(); diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index ec3d7b8ec0..c57ece9cfb 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -106,193 +106,205 @@ describe('Email Verification Token Expiration: ', () => { }); }); - it_id('f20dd3c2-87d9-4bc6-a51d-4ea2834acbcc')(it)('if user clicks on the email verify link before email verification token expiration then show the verify email success page', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('testEmailVerifyTokenValidity'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); + it_id('f20dd3c2-87d9-4bc6-a51d-4ea2834acbcc')(it)( + 'if user clicks on the email verify link before email verification token expiration then show the verify email success page', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', }) - .then(() => jasmine.timeout()) - .then(() => { - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' - ); + .then(() => { + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => jasmine.timeout()) + .then(() => { + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' + ); + done(); + }); + }) + .catch(error => { + jfail(error); done(); }); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + } + ); - it_id('94956799-c85e-4297-b879-e2d1f985394c')(it)('if user clicks on the email verify link before email verification token expiration then emailVerified should be true', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('testEmailVerifyTokenValidity'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); + it_id('94956799-c85e-4297-b879-e2d1f985394c')(it)( + 'if user clicks on the email verify link before email verification token expiration then emailVerified should be true', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', }) - .then(() => jasmine.timeout()) - .then(() => { - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - user - .fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(true); - done(); - }) - .catch(error => { - jfail(error); - done(); - }); + .then(() => { + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => jasmine.timeout()) + .then(() => { + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + user + .fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(true); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + }); + }) + .catch(error => { + jfail(error); + done(); }); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + } + ); - it_id('25f3f895-c987-431c-9841-17cb6aaf18b5')(it)('if user clicks on the email verify link before email verification token expiration then user should be able to login', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('testEmailVerifyTokenValidity'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); + it_id('25f3f895-c987-431c-9841-17cb6aaf18b5')(it)( + 'if user clicks on the email verify link before email verification token expiration then user should be able to login', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', }) - .then(() => jasmine.timeout()) - .then(() => { - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') - .then(user => { - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(true); - done(); - }) - .catch(error => { - jfail(error); - done(); - }); + .then(() => { + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => jasmine.timeout()) + .then(() => { + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') + .then(user => { + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + }); + }) + .catch(error => { + jfail(error); + done(); }); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + } + ); - it_id('c6a3e188-9065-4f50-842d-454d1e82f289')(it)('sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('sets_email_verify_token_expires_at'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => { - const config = Config.get('test'); - return config.database.find( - '_User', - { - username: 'sets_email_verify_token_expires_at', - }, - {}, - Auth.maintenance(config) - ); - }) - .then(results => { - expect(results.length).toBe(1); - const user = results[0]; - expect(typeof user).toBe('object'); - expect(user.emailVerified).toEqual(false); - expect(typeof user._email_verify_token).toBe('string'); - expect(typeof user._email_verify_token_expires_at).toBe('object'); - expect(sendEmailOptions).toBeDefined(); - done(); + it_id('c6a3e188-9065-4f50-842d-454d1e82f289')(it)( + 'sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', }) - .catch(error => { - jfail(error); - done(); - }); - }); + .then(() => { + user.setUsername('sets_email_verify_token_expires_at'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + const config = Config.get('test'); + return config.database.find( + '_User', + { + username: 'sets_email_verify_token_expires_at', + }, + {}, + Auth.maintenance(config) + ); + }) + .then(results => { + expect(results.length).toBe(1); + const user = results[0]; + expect(typeof user).toBe('object'); + expect(user.emailVerified).toEqual(false); + expect(typeof user._email_verify_token).toBe('string'); + expect(typeof user._email_verify_token_expires_at).toBe('object'); + expect(sendEmailOptions).toBeDefined(); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + } + ); it('can resend email using an expired token', async () => { const user = new Parse.User(); @@ -411,342 +423,309 @@ describe('Email Verification Token Expiration: ', () => { expect(verifySpy).toHaveBeenCalled(); }); - it_id('b3549300-bed7-4a5e-bed5-792dbfead960')(it)('can conditionally send emails and allow conditional login', async () => { - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - const verifyUserEmails = { - method(req) { - expect(Object.keys(req)).toEqual(['original', 'object', 'master', 'ip', 'installationId']); - if (req.object.get('username') === 'no_email') { + it_id('b3549300-bed7-4a5e-bed5-792dbfead960')(it)( + 'can conditionally send emails and allow conditional login', + async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const verifyUserEmails = { + method(req) { + expect(Object.keys(req)).toEqual([ + 'original', + 'object', + 'master', + 'ip', + 'installationId', + ]); + if (req.object.get('username') === 'no_email') { + return false; + } + return true; + }, + }; + const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough(); + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: verifyUserEmails.method, + preventLoginWithUnverifiedEmail: verifyUserEmails.method, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + }); + const user = new Parse.User(); + user.setUsername('no_email'); + user.setPassword('expiringToken'); + user.set('email', 'user@example.com'); + await user.signUp(); + expect(sendEmailOptions).toBeUndefined(); + expect(user.getSessionToken()).toBeDefined(); + expect(verifySpy).toHaveBeenCalledTimes(2); + const user2 = new Parse.User(); + user2.setUsername('email'); + user2.setPassword('expiringToken'); + user2.set('email', 'user2@example.com'); + await user2.signUp(); + await jasmine.timeout(); + expect(user2.getSessionToken()).toBeUndefined(); + expect(sendEmailOptions).toBeDefined(); + expect(verifySpy).toHaveBeenCalledTimes(5); + } + ); + + it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)( + 'can conditionally send user email verification', + async () => { + const emailAdapter = { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const sendVerificationEmail = { + method(req) { + expect(req.user).toBeDefined(); + expect(req.master).toBeDefined(); return false; - } - return true; - }, - }; - const verifySpy = spyOn(verifyUserEmails, 'method').and.callThrough(); - await reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: verifyUserEmails.method, - preventLoginWithUnverifiedEmail: verifyUserEmails.method, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }); - const user = new Parse.User(); - user.setUsername('no_email'); - user.setPassword('expiringToken'); - user.set('email', 'user@example.com'); - await user.signUp(); - expect(sendEmailOptions).toBeUndefined(); - expect(user.getSessionToken()).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(2); - const user2 = new Parse.User(); - user2.setUsername('email'); - user2.setPassword('expiringToken'); - user2.set('email', 'user2@example.com'); - await user2.signUp(); - await jasmine.timeout(); - expect(user2.getSessionToken()).toBeUndefined(); - expect(sendEmailOptions).toBeDefined(); - expect(verifySpy).toHaveBeenCalledTimes(5); - }); + }, + }; + const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough(); + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + sendUserEmailVerification: sendVerificationEmail.method, + }); + const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough(); + const newUser = new Parse.User(); + newUser.setUsername('unsets_email_verify_token_expires_at'); + newUser.setPassword('expiringToken'); + newUser.set('email', 'user@example.com'); + await newUser.signUp(); + await Parse.User.requestEmailVerification('user@example.com'); + await jasmine.timeout(); + expect(sendSpy).toHaveBeenCalledTimes(2); + expect(emailSpy).toHaveBeenCalledTimes(0); + } + ); - it_id('d812de87-33d1-495e-a6e8-3485f6dc3589')(it)('can conditionally send user email verification', async () => { - const emailAdapter = { - sendVerificationEmail: () => {}, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - const sendVerificationEmail = { - method(req) { - expect(req.user).toBeDefined(); - expect(req.master).toBeDefined(); - return false; - }, - }; - const sendSpy = spyOn(sendVerificationEmail, 'method').and.callThrough(); - await reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - sendUserEmailVerification: sendVerificationEmail.method, - }); - const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough(); - const newUser = new Parse.User(); - newUser.setUsername('unsets_email_verify_token_expires_at'); - newUser.setPassword('expiringToken'); - newUser.set('email', 'user@example.com'); - await newUser.signUp(); - await Parse.User.requestEmailVerification('user@example.com'); - await jasmine.timeout(); - expect(sendSpy).toHaveBeenCalledTimes(2); - expect(emailSpy).toHaveBeenCalledTimes(0); - }); - - it_id('d98babc1-feb8-4b5e-916c-57dc0a6ed9fb')(it)('provides full user object in email verification function on email and username change', async () => { - const emailAdapter = { - sendVerificationEmail: () => {}, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - const sendVerificationEmail = { - method(req) { - expect(req.user).toBeDefined(); - expect(req.user.id).toBeDefined(); - expect(req.user.get('createdAt')).toBeDefined(); - expect(req.user.get('updatedAt')).toBeDefined(); - expect(req.master).toBeDefined(); - return false; - }, - }; - await reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, - publicServerURL: 'http://localhost:8378/1', - sendUserEmailVerification: sendVerificationEmail.method, - }); - const user = new Parse.User(); - user.setPassword('password'); - user.setUsername('new@example.com'); - user.setEmail('user@example.com'); - await user.save(null, { useMasterKey: true }); - - // Update email and username - user.setUsername('new@example.com'); - user.setEmail('new@example.com'); - await user.save(null, { useMasterKey: true }); - }); - - it_id('a8c1f820-822f-4a37-9d08-a968cac8369d')(it)('beforeSave options do not change existing behaviour', async () => { - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - await reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }); - const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough(); - const newUser = new Parse.User(); - newUser.setUsername('unsets_email_verify_token_expires_at'); - newUser.setPassword('expiringToken'); - newUser.set('email', 'user@parse.com'); - await newUser.signUp(); - await jasmine.timeout(); - const response = await request({ - url: sendEmailOptions.link, - followRedirects: false, - }); - expect(response.status).toEqual(302); - const config = Config.get('test'); - const results = await config.database.find('_User', { - username: 'unsets_email_verify_token_expires_at', - }); + it_id('d98babc1-feb8-4b5e-916c-57dc0a6ed9fb')(it)( + 'provides full user object in email verification function on email and username change', + async () => { + const emailAdapter = { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const sendVerificationEmail = { + method(req) { + expect(req.user).toBeDefined(); + expect(req.user.id).toBeDefined(); + expect(req.user.get('createdAt')).toBeDefined(); + expect(req.user.get('updatedAt')).toBeDefined(); + expect(req.master).toBeDefined(); + return false; + }, + }; + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, + publicServerURL: 'http://localhost:8378/1', + sendUserEmailVerification: sendVerificationEmail.method, + }); + const user = new Parse.User(); + user.setPassword('password'); + user.setUsername('new@example.com'); + user.setEmail('user@example.com'); + await user.save(null, { useMasterKey: true }); - expect(results.length).toBe(1); - const user = results[0]; - expect(typeof user).toBe('object'); - expect(user.emailVerified).toEqual(true); - expect(typeof user._email_verify_token).toBe('undefined'); - expect(typeof user._email_verify_token_expires_at).toBe('undefined'); - expect(emailSpy).toHaveBeenCalled(); - }); + // Update email and username + user.setUsername('new@example.com'); + user.setEmail('new@example.com'); + await user.save(null, { useMasterKey: true }); + } + ); - it_id('36d277eb-ec7c-4a39-9108-435b68228741')(it)('unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('unsets_email_verify_token_expires_at'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => jasmine.timeout()) - .then(() => { - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - const config = Config.get('test'); - return config.database - .find('_User', { - username: 'unsets_email_verify_token_expires_at', - }) - .then(results => { - expect(results.length).toBe(1); - return results[0]; - }) - .then(user => { - expect(typeof user).toBe('object'); - expect(user.emailVerified).toEqual(true); - expect(typeof user._email_verify_token).toBe('undefined'); - expect(typeof user._email_verify_token_expires_at).toBe('undefined'); - done(); - }) - .catch(error => { - jfail(error); - done(); - }); - }); - }) - .catch(error => { - jfail(error); - done(); + it_id('a8c1f820-822f-4a37-9d08-a968cac8369d')(it)( + 'beforeSave options do not change existing behaviour', + async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + }); + const emailSpy = spyOn(emailAdapter, 'sendVerificationEmail').and.callThrough(); + const newUser = new Parse.User(); + newUser.setUsername('unsets_email_verify_token_expires_at'); + newUser.setPassword('expiringToken'); + newUser.set('email', 'user@parse.com'); + await newUser.signUp(); + await jasmine.timeout(); + const response = await request({ + url: sendEmailOptions.link, + followRedirects: false, + }); + expect(response.status).toEqual(302); + const config = Config.get('test'); + const results = await config.database.find('_User', { + username: 'unsets_email_verify_token_expires_at', }); - }); - it_id('4f444704-ec4b-4dff-b947-614b1c6971c4')(it)('clicking on the email verify link by an email VERIFIED user that was setup before enabling the expire email verify token should show email verify email success', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - const serverConfig = { - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }; + expect(results.length).toBe(1); + const user = results[0]; + expect(typeof user).toBe('object'); + expect(user.emailVerified).toEqual(true); + expect(typeof user._email_verify_token).toBe('undefined'); + expect(typeof user._email_verify_token_expires_at).toBe('undefined'); + expect(emailSpy).toHaveBeenCalled(); + } + ); - // setup server WITHOUT enabling the expire email verify token flag - reconfigureServer(serverConfig) - .then(() => { - user.setUsername('testEmailVerifyTokenValidity'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => jasmine.timeout()) - .then(() => { - return request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - return user.fetch(); - }); - }) - .then(() => { - expect(user.get('emailVerified')).toEqual(true); - // RECONFIGURE the server i.e., ENABLE the expire email verify token flag - serverConfig.emailVerifyTokenValidityDuration = 5; // 5 seconds - return reconfigureServer(serverConfig); + it_id('36d277eb-ec7c-4a39-9108-435b68228741')(it)( + 'unsets the _email_verify_token_expires_at and _email_verify_token fields in the User class if email verification is successful', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', }) - .then(() => { - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - const url = new URL(sendEmailOptions.link); - const token = url.searchParams.get('token'); - expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}` - ); + .then(() => { + user.setUsername('unsets_email_verify_token_expires_at'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => jasmine.timeout()) + .then(() => { + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + const config = Config.get('test'); + return config.database + .find('_User', { + username: 'unsets_email_verify_token_expires_at', + }) + .then(results => { + expect(results.length).toBe(1); + return results[0]; + }) + .then(user => { + expect(typeof user).toBe('object'); + expect(user.emailVerified).toEqual(true); + expect(typeof user._email_verify_token).toBe('undefined'); + expect(typeof user._email_verify_token_expires_at).toBe('undefined'); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + }); + }) + .catch(error => { + jfail(error); done(); }); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + } + ); - it('clicking on the email verify link by an email UNVERIFIED user that was setup before enabling the expire email verify token should show invalid verficiation link page', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - const serverConfig = { - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }; + it_id('4f444704-ec4b-4dff-b947-614b1c6971c4')(it)( + 'clicking on the email verify link by an email VERIFIED user that was setup before enabling the expire email verify token should show email verify email success', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const serverConfig = { + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }; - // setup server WITHOUT enabling the expire email verify token flag - reconfigureServer(serverConfig) - .then(() => { - user.setUsername('testEmailVerifyTokenValidity'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => { - // just get the user again - DO NOT email verify the user - return user.fetch(); - }) - .then(() => { - expect(user.get('emailVerified')).toEqual(false); - // RECONFIGURE the server i.e., ENABLE the expire email verify token flag - serverConfig.emailVerifyTokenValidityDuration = 5; // 5 seconds - return reconfigureServer(serverConfig); - }) - .then(() => { - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - const url = new URL(sendEmailOptions.link); - const token = url.searchParams.get('token'); - expect(response.text).toEqual( - `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}` - ); + // setup server WITHOUT enabling the expire email verify token flag + reconfigureServer(serverConfig) + .then(() => { + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => jasmine.timeout()) + .then(() => { + return request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + return user.fetch(); + }); + }) + .then(() => { + expect(user.get('emailVerified')).toEqual(true); + // RECONFIGURE the server i.e., ENABLE the expire email verify token flag + serverConfig.emailVerifyTokenValidityDuration = 5; // 5 seconds + return reconfigureServer(serverConfig); + }) + .then(() => { + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + const url = new URL(sendEmailOptions.link); + const token = url.searchParams.get('token'); + expect(response.text).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}` + ); + done(); + }); + }) + .catch(error => { + jfail(error); done(); }); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + } + ); - it_id('b6c87f35-d887-477d-bc86-a9217a424f53')(it)('setting the email on the user should set a new email verification token and new expiration date for the token when expire email verify token flag is set', done => { + it('clicking on the email verify link by an email UNVERIFIED user that was setup before enabling the expire email verify token should show invalid verficiation link page', done => { const user = new Parse.User(); - let userBeforeEmailReset; - let sendEmailOptions; const emailAdapter = { sendVerificationEmail: options => { @@ -759,153 +738,216 @@ describe('Email Verification Token Expiration: ', () => { appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds publicServerURL: 'http://localhost:8378/1', }; - - reconfigureServer(serverConfig) - .then(() => { - user.setUsername('newEmailVerifyTokenOnEmailReset'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => { - const config = Config.get('test'); - return config.database - .find('_User', { username: 'newEmailVerifyTokenOnEmailReset' }) - .then(results => { - return results[0]; - }); - }) - .then(userFromDb => { - expect(typeof userFromDb).toBe('object'); - userBeforeEmailReset = userFromDb; - - // trigger another token generation by setting the email - user.set('email', 'user@parse.com'); - return new Promise(resolve => { - // wait for half a sec to get a new expiration time - setTimeout(() => resolve(user.save()), 500); - }); - }) - .then(() => { - const config = Config.get('test'); - return config.database - .find( - '_User', - { username: 'newEmailVerifyTokenOnEmailReset' }, - {}, - Auth.maintenance(config) - ) - .then(results => { - return results[0]; - }); - }) - .then(userAfterEmailReset => { - expect(typeof userAfterEmailReset).toBe('object'); - expect(userBeforeEmailReset._email_verify_token).not.toEqual( - userAfterEmailReset._email_verify_token - ); - expect(userBeforeEmailReset._email_verify_token_expires_at).not.toEqual( - userAfterEmailReset._email_verify_token_expires_at - ); - expect(sendEmailOptions).toBeDefined(); - done(); - }) - .catch(error => { - jfail(error); - done(); - }); - }); - - it_id('28f2140d-48bd-44ac-a141-ba60ea8d9713')(it)('should send a new verification email when a resend is requested and the user is UNVERIFIED', done => { - const user = new Parse.User(); - let sendEmailOptions; - let sendVerificationEmailCallCount = 0; - let userBeforeRequest; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - sendVerificationEmailCallCount++; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('resends_verification_token'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => { - const config = Config.get('test'); - return config.database - .find('_User', { username: 'resends_verification_token' }) - .then(results => { - return results[0]; - }); - }) - .then(newUser => { - // store this user before we make our email request - userBeforeRequest = newUser; - - expect(sendVerificationEmailCallCount).toBe(1); - - return request({ - url: 'http://localhost:8378/1/verificationEmailRequest', - method: 'POST', - body: { - email: 'user@parse.com', - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - 'Content-Type': 'application/json', - }, - }); + + // setup server WITHOUT enabling the expire email verify token flag + reconfigureServer(serverConfig) + .then(() => { + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); }) - .then(response => { - expect(response.status).toBe(200); + .then(() => { + // just get the user again - DO NOT email verify the user + return user.fetch(); }) - .then(() => jasmine.timeout()) .then(() => { - expect(sendVerificationEmailCallCount).toBe(2); - expect(sendEmailOptions).toBeDefined(); - - // query for this user again - const config = Config.get('test'); - return config.database - .find('_User', { username: 'resends_verification_token' }, {}, Auth.maintenance(config)) - .then(results => { - return results[0]; - }); + expect(user.get('emailVerified')).toEqual(false); + // RECONFIGURE the server i.e., ENABLE the expire email verify token flag + serverConfig.emailVerifyTokenValidityDuration = 5; // 5 seconds + return reconfigureServer(serverConfig); }) - .then(userAfterRequest => { - // verify that our token & expiration has been changed for this new request - expect(typeof userAfterRequest).toBe('object'); - expect(userBeforeRequest._email_verify_token).not.toEqual( - userAfterRequest._email_verify_token - ); - expect(userBeforeRequest._email_verify_token_expires_at).not.toEqual( - userAfterRequest._email_verify_token_expires_at - ); - done(); + .then(() => { + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + const url = new URL(sendEmailOptions.link); + const token = url.searchParams.get('token'); + expect(response.text).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}` + ); + done(); + }); }) .catch(error => { - console.log(error); jfail(error); done(); }); }); + it_id('b6c87f35-d887-477d-bc86-a9217a424f53')(it)( + 'setting the email on the user should set a new email verification token and new expiration date for the token when expire email verify token flag is set', + done => { + const user = new Parse.User(); + let userBeforeEmailReset; + + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const serverConfig = { + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + }; + + reconfigureServer(serverConfig) + .then(() => { + user.setUsername('newEmailVerifyTokenOnEmailReset'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + const config = Config.get('test'); + return config.database + .find('_User', { username: 'newEmailVerifyTokenOnEmailReset' }) + .then(results => { + return results[0]; + }); + }) + .then(userFromDb => { + expect(typeof userFromDb).toBe('object'); + userBeforeEmailReset = userFromDb; + + // trigger another token generation by setting the email + user.set('email', 'user@parse.com'); + return new Promise(resolve => { + // wait for half a sec to get a new expiration time + setTimeout(() => resolve(user.save()), 500); + }); + }) + .then(() => { + const config = Config.get('test'); + return config.database + .find( + '_User', + { username: 'newEmailVerifyTokenOnEmailReset' }, + {}, + Auth.maintenance(config) + ) + .then(results => { + return results[0]; + }); + }) + .then(userAfterEmailReset => { + expect(typeof userAfterEmailReset).toBe('object'); + expect(userBeforeEmailReset._email_verify_token).not.toEqual( + userAfterEmailReset._email_verify_token + ); + expect(userBeforeEmailReset._email_verify_token_expires_at).not.toEqual( + userAfterEmailReset._email_verify_token_expires_at + ); + expect(sendEmailOptions).toBeDefined(); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + } + ); + + it_id('28f2140d-48bd-44ac-a141-ba60ea8d9713')(it)( + 'should send a new verification email when a resend is requested and the user is UNVERIFIED', + done => { + const user = new Parse.User(); + let sendEmailOptions; + let sendVerificationEmailCallCount = 0; + let userBeforeRequest; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + sendVerificationEmailCallCount++; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + user.setUsername('resends_verification_token'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + const config = Config.get('test'); + return config.database + .find('_User', { username: 'resends_verification_token' }) + .then(results => { + return results[0]; + }); + }) + .then(newUser => { + // store this user before we make our email request + userBeforeRequest = newUser; + + expect(sendVerificationEmailCallCount).toBe(1); + + return request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { + email: 'user@parse.com', + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + }) + .then(response => { + expect(response.status).toBe(200); + }) + .then(() => jasmine.timeout()) + .then(() => { + expect(sendVerificationEmailCallCount).toBe(2); + expect(sendEmailOptions).toBeDefined(); + + // query for this user again + const config = Config.get('test'); + return config.database + .find('_User', { username: 'resends_verification_token' }, {}, Auth.maintenance(config)) + .then(results => { + return results[0]; + }); + }) + .then(userAfterRequest => { + // verify that our token & expiration has been changed for this new request + expect(typeof userAfterRequest).toBe('object'); + expect(userBeforeRequest._email_verify_token).not.toEqual( + userAfterRequest._email_verify_token + ); + expect(userBeforeRequest._email_verify_token_expires_at).not.toEqual( + userAfterRequest._email_verify_token_expires_at + ); + done(); + }) + .catch(error => { + console.log(error); + jfail(error); + done(); + }); + } + ); + it('provides function arguments in verifyUserEmails on verificationEmailRequest', async () => { const user = new Parse.User(); user.setUsername('user'); @@ -914,7 +956,7 @@ describe('Email Verification Token Expiration: ', () => { await user.signUp(); const verifyUserEmails = { - method: async (params) => { + method: async params => { expect(params.object).toBeInstanceOf(Parse.User); expect(params.ip).toBeDefined(); expect(params.master).toBeDefined(); @@ -980,133 +1022,151 @@ describe('Email Verification Token Expiration: ', () => { done(); }); - it_id('0e66b7f6-2c07-4117-a8b9-605aa31a1e29')(it)('should match codes with emailVerifyTokenReuseIfValid', async done => { - let sendEmailOptions; - let sendVerificationEmailCallCount = 0; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - sendVerificationEmailCallCount++; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - await reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5 * 60, // 5 minutes - publicServerURL: 'http://localhost:8378/1', - emailVerifyTokenReuseIfValid: true, - }); - const user = new Parse.User(); - user.setUsername('resends_verification_token'); - user.setPassword('expiringToken'); - user.set('email', 'user@example.com'); - await user.signUp(); + it_id('0e66b7f6-2c07-4117-a8b9-605aa31a1e29')(it)( + 'should match codes with emailVerifyTokenReuseIfValid', + async done => { + let sendEmailOptions; + let sendVerificationEmailCallCount = 0; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + sendVerificationEmailCallCount++; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5 * 60, // 5 minutes + publicServerURL: 'http://localhost:8378/1', + emailVerifyTokenReuseIfValid: true, + }); + const user = new Parse.User(); + user.setUsername('resends_verification_token'); + user.setPassword('expiringToken'); + user.set('email', 'user@example.com'); + await user.signUp(); - const config = Config.get('test'); - const [userBeforeRequest] = await config.database.find('_User', { - username: 'resends_verification_token', - }, {}, Auth.maintenance(config)); - // store this user before we make our email request - expect(sendVerificationEmailCallCount).toBe(1); - await new Promise(resolve => { - setTimeout(() => { - resolve(); - }, 1000); - }); - const response = await request({ - url: 'http://localhost:8378/1/verificationEmailRequest', - method: 'POST', - body: { - email: 'user@example.com', - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - 'Content-Type': 'application/json', - }, - }); - await jasmine.timeout(); - expect(response.status).toBe(200); - expect(sendVerificationEmailCallCount).toBe(2); - expect(sendEmailOptions).toBeDefined(); + const config = Config.get('test'); + const [userBeforeRequest] = await config.database.find( + '_User', + { + username: 'resends_verification_token', + }, + {}, + Auth.maintenance(config) + ); + // store this user before we make our email request + expect(sendVerificationEmailCallCount).toBe(1); + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + const response = await request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { + email: 'user@example.com', + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }); + await jasmine.timeout(); + expect(response.status).toBe(200); + expect(sendVerificationEmailCallCount).toBe(2); + expect(sendEmailOptions).toBeDefined(); - const [userAfterRequest] = await config.database.find('_User', { - username: 'resends_verification_token', - }, {}, Auth.maintenance(config)); + const [userAfterRequest] = await config.database.find( + '_User', + { + username: 'resends_verification_token', + }, + {}, + Auth.maintenance(config) + ); - // Verify that token & expiration haven't been changed for this new request - expect(typeof userAfterRequest).toBe('object'); - expect(userBeforeRequest._email_verify_token).toBeDefined(); - expect(userBeforeRequest._email_verify_token).toEqual(userAfterRequest._email_verify_token); - expect(userBeforeRequest._email_verify_token_expires_at).toBeDefined(); - expect(userBeforeRequest._email_verify_token_expires_at).toEqual(userAfterRequest._email_verify_token_expires_at); - done(); - }); + // Verify that token & expiration haven't been changed for this new request + expect(typeof userAfterRequest).toBe('object'); + expect(userBeforeRequest._email_verify_token).toBeDefined(); + expect(userBeforeRequest._email_verify_token).toEqual(userAfterRequest._email_verify_token); + expect(userBeforeRequest._email_verify_token_expires_at).toBeDefined(); + expect(userBeforeRequest._email_verify_token_expires_at).toEqual( + userAfterRequest._email_verify_token_expires_at + ); + done(); + } + ); - it_id('1ed9a6c2-bebc-4813-af30-4f4a212544c2')(it)('should not send a new verification email when a resend is requested and the user is VERIFIED', done => { - const user = new Parse.User(); - let sendEmailOptions; - let sendVerificationEmailCallCount = 0; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - sendVerificationEmailCallCount++; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('no_new_verification_token_once_verified'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); - }) - .then(() => jasmine.timeout()) - .then(() => { - return request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - }); + it_id('1ed9a6c2-bebc-4813-af30-4f4a212544c2')(it)( + 'should not send a new verification email when a resend is requested and the user is VERIFIED', + done => { + const user = new Parse.User(); + let sendEmailOptions; + let sendVerificationEmailCallCount = 0; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + sendVerificationEmailCallCount++; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', }) - .then(() => { - expect(sendVerificationEmailCallCount).toBe(1); - - return request({ - url: 'http://localhost:8378/1/verificationEmailRequest', - method: 'POST', - body: { - email: 'user@parse.com', - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - 'Content-Type': 'application/json', - }, + .then(() => { + user.setUsername('no_new_verification_token_once_verified'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); }) - .then(fail, res => res) - .then(response => { - expect(response.status).toBe(400); - expect(sendVerificationEmailCallCount).toBe(1); - done(); + .then(() => jasmine.timeout()) + .then(() => { + return request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); }); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + }) + .then(() => { + expect(sendVerificationEmailCallCount).toBe(1); + + return request({ + url: 'http://localhost:8378/1/verificationEmailRequest', + method: 'POST', + body: { + email: 'user@parse.com', + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + }) + .then(fail, res => res) + .then(response => { + expect(response.status).toBe(400); + expect(sendVerificationEmailCallCount).toBe(1); + done(); + }); + }) + .catch(error => { + jfail(error); + done(); + }); + } + ); it('should not send a new verification email if this user does not exist', done => { let sendEmailOptions; @@ -1287,67 +1347,70 @@ describe('Email Verification Token Expiration: ', () => { }); }); - it_id('b082d387-4974-4d45-a0d9-0c85ca2d7cbf')(it)('emailVerified should be set to false after changing from an already verified email', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailVerifyToken', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('testEmailVerifyTokenValidity'); - user.setPassword('expiringToken'); - user.set('email', 'user@parse.com'); - return user.signUp(); + it_id('b082d387-4974-4d45-a0d9-0c85ca2d7cbf')(it)( + 'emailVerified should be set to false after changing from an already verified email', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, // 5 seconds + publicServerURL: 'http://localhost:8378/1', }) - .then(() => jasmine.timeout()) - .then(() => { - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') - .then(user => { - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(true); + .then(() => { + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => jasmine.timeout()) + .then(() => { + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') + .then(user => { + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); - user.set('email', 'newEmail@parse.com'); - return user.save(); - }) - .then(() => user.fetch()) - .then(user => { - expect(typeof user).toBe('object'); - expect(user.get('email')).toBe('newEmail@parse.com'); - expect(user.get('emailVerified')).toBe(false); + user.set('email', 'newEmail@parse.com'); + return user.save(); + }) + .then(() => user.fetch()) + .then(user => { + expect(typeof user).toBe('object'); + expect(user.get('email')).toBe('newEmail@parse.com'); + expect(user.get('emailVerified')).toBe(false); - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + done(); + }); + }) + .catch(error => { + jfail(error); done(); }); - }) - .catch(error => { - jfail(error); - done(); - }); + }); + }) + .catch(error => { + jfail(error); + done(); }); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + } + ); }); diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 30acf7d13c..287a89fb23 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -1,8 +1,8 @@ const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') - .WinstonLoggerAdapter; -const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') - .GridFSBucketAdapter; +const WinstonLoggerAdapter = + require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; +const GridFSBucketAdapter = + require('../lib/Adapters/Files/GridFSBucketAdapter').GridFSBucketAdapter; const Config = require('../lib/Config'); const FilesController = require('../lib/Controllers/FilesController').default; const databaseURI = 'mongodb://localhost:27017/parse'; @@ -26,9 +26,9 @@ describe('FilesController', () => { const gridFSAdapter = new GridFSBucketAdapter('mongodb://localhost:27017/parse'); gridFSAdapter.getFileLocation = (config, filename) => { return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename); - } + }; const filesController = new FilesController(gridFSAdapter); - const result = await filesController.expandFilesInObject(config, function () { }); + const result = await filesController.expandFilesInObject(config, function () {}); expect(result).toBeUndefined(); @@ -50,9 +50,9 @@ describe('FilesController', () => { gridFSAdapter.getFileLocation = async (config, filename) => { await Promise.resolve(); return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename); - } + }; const filesController = new FilesController(gridFSAdapter); - const result = await filesController.expandFilesInObject(config, function () { }); + const result = await filesController.expandFilesInObject(config, function () {}); expect(result).toBeUndefined(); @@ -76,7 +76,9 @@ describe('FilesController', () => { name: 'mock-name', __type: 'File', }; - gridFSAdapter.getFileLocation = jasmine.createSpy('getFileLocation').and.returnValue(Promise.resolve('mock-url')); + gridFSAdapter.getFileLocation = jasmine + .createSpy('getFileLocation') + .and.returnValue(Promise.resolve('mock-url')); const filesController = new FilesController(gridFSAdapter); const anObject = { aFile: fullFile }; @@ -93,7 +95,9 @@ describe('FilesController', () => { name: 'mock-name', __type: 'File', }; - gridFSAdapter.getFileLocation = jasmine.createSpy('getFileLocation').and.returnValue(Promise.resolve('mock-url')); + gridFSAdapter.getFileLocation = jasmine + .createSpy('getFileLocation') + .and.returnValue(Promise.resolve('mock-url')); const filesController = new FilesController(gridFSAdapter); const anObject = { aFile: fullFile }; @@ -102,7 +106,6 @@ describe('FilesController', () => { expect(anObject.aFile.url).toEqual('mock-url'); }); - it_only_db('mongo')('should pass databaseOptions to GridFSBucketAdapter', async () => { await reconfigureServer({ databaseURI: 'mongodb://localhost:27017/parse', diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index 7e9c84a59e..88da2bbd2d 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -1,5 +1,5 @@ -const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') - .GridFSBucketAdapter; +const GridFSBucketAdapter = + require('../lib/Adapters/Files/GridFSBucketAdapter').GridFSBucketAdapter; const { randomString } = require('../lib/cryptoUtils'); const databaseURI = 'mongodb://localhost:27017/parse'; const request = require('../lib/request'); diff --git a/spec/Idempotency.spec.js b/spec/Idempotency.spec.js index 14d0469b86..fd666d4360 100644 --- a/spec/Idempotency.spec.js +++ b/spec/Idempotency.spec.js @@ -50,52 +50,58 @@ describe('Idempotency', () => { }); // Tests - it_id('e25955fd-92eb-4b22-b8b7-38980e5cb223')(it)('should enforce idempotency for cloud code function', async () => { - let counter = 0; - Parse.Cloud.define('myFunction', () => { - counter++; - }); - const params = { - method: 'POST', - url: 'http://localhost:8378/1/functions/myFunction', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': Parse.masterKey, - 'X-Parse-Request-Id': 'abc-123', - }, - }; - expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(ttl); - await request(params); - await request(params).then(fail, e => { - expect(e.status).toEqual(400); - expect(e.data.error).toEqual('Duplicate request'); - }); - expect(counter).toBe(1); - }); + it_id('e25955fd-92eb-4b22-b8b7-38980e5cb223')(it)( + 'should enforce idempotency for cloud code function', + async () => { + let counter = 0; + Parse.Cloud.define('myFunction', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/functions/myFunction', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123', + }, + }; + expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(ttl); + await request(params); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual('Duplicate request'); + }); + expect(counter).toBe(1); + } + ); - it_id('be2fbe16-8178-485e-9a12-6fb541096480')(it)('should delete request entry after TTL', async () => { - let counter = 0; - Parse.Cloud.define('myFunction', () => { - counter++; - }); - const params = { - method: 'POST', - url: 'http://localhost:8378/1/functions/myFunction', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': Parse.masterKey, - 'X-Parse-Request-Id': 'abc-123', - }, - }; - await expectAsync(request(params)).toBeResolved(); - if (SIMULATE_TTL) { - await deleteRequestEntry('abc-123'); - } else { - await new Promise(resolve => setTimeout(resolve, maxTimeOut)); + it_id('be2fbe16-8178-485e-9a12-6fb541096480')(it)( + 'should delete request entry after TTL', + async () => { + let counter = 0; + Parse.Cloud.define('myFunction', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/functions/myFunction', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123', + }, + }; + await expectAsync(request(params)).toBeResolved(); + if (SIMULATE_TTL) { + await deleteRequestEntry('abc-123'); + } else { + await new Promise(resolve => setTimeout(resolve, maxTimeOut)); + } + await expectAsync(request(params)).toBeResolved(); + expect(counter).toBe(2); } - await expectAsync(request(params)).toBeResolved(); - expect(counter).toBe(2); - }); + ); it_only_db('postgres')( 'should delete request entry when postgress ttl function is called', @@ -123,140 +129,158 @@ describe('Idempotency', () => { } ); - it_id('e976d0cc-a57f-45d4-9472-b9b052db6490')(it)('should enforce idempotency for cloud code jobs', async () => { - let counter = 0; - Parse.Cloud.job('myJob', () => { - counter++; - }); - const params = { - method: 'POST', - url: 'http://localhost:8378/1/jobs/myJob', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': Parse.masterKey, - 'X-Parse-Request-Id': 'abc-123', - }, - }; - await expectAsync(request(params)).toBeResolved(); - await request(params).then(fail, e => { - expect(e.status).toEqual(400); - expect(e.data.error).toEqual('Duplicate request'); - }); - expect(counter).toBe(1); - }); - - it_id('7c84a3d4-e1b6-4a0d-99f1-af3cf1a6b3d8')(it)('should enforce idempotency for class object creation', async () => { - let counter = 0; - Parse.Cloud.afterSave('MyClass', () => { - counter++; - }); - const params = { - method: 'POST', - url: 'http://localhost:8378/1/classes/MyClass', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': Parse.masterKey, - 'X-Parse-Request-Id': 'abc-123', - }, - }; - await expectAsync(request(params)).toBeResolved(); - await request(params).then(fail, e => { - expect(e.status).toEqual(400); - expect(e.data.error).toEqual('Duplicate request'); - }); - expect(counter).toBe(1); - }); + it_id('e976d0cc-a57f-45d4-9472-b9b052db6490')(it)( + 'should enforce idempotency for cloud code jobs', + async () => { + let counter = 0; + Parse.Cloud.job('myJob', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/jobs/myJob', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123', + }, + }; + await expectAsync(request(params)).toBeResolved(); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual('Duplicate request'); + }); + expect(counter).toBe(1); + } + ); - it_id('a030f2dd-5d21-46ac-b53d-9d714f35d72a')(it)('should enforce idempotency for user object creation', async () => { - let counter = 0; - Parse.Cloud.afterSave('_User', () => { - counter++; - }); - const params = { - method: 'POST', - url: 'http://localhost:8378/1/users', - body: { - username: 'user', - password: 'pass', - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': Parse.masterKey, - 'X-Parse-Request-Id': 'abc-123', - }, - }; - await expectAsync(request(params)).toBeResolved(); - await request(params).then(fail, e => { - expect(e.status).toEqual(400); - expect(e.data.error).toEqual('Duplicate request'); - }); - expect(counter).toBe(1); - }); + it_id('7c84a3d4-e1b6-4a0d-99f1-af3cf1a6b3d8')(it)( + 'should enforce idempotency for class object creation', + async () => { + let counter = 0; + Parse.Cloud.afterSave('MyClass', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/classes/MyClass', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123', + }, + }; + await expectAsync(request(params)).toBeResolved(); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual('Duplicate request'); + }); + expect(counter).toBe(1); + } + ); - it_id('064c469b-091c-4ba9-9043-be461f26a3eb')(it)('should enforce idempotency for installation object creation', async () => { - let counter = 0; - Parse.Cloud.afterSave('_Installation', () => { - counter++; - }); - const params = { - method: 'POST', - url: 'http://localhost:8378/1/installations', - body: { - installationId: '1', - deviceType: 'ios', - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': Parse.masterKey, - 'X-Parse-Request-Id': 'abc-123', - }, - }; - await expectAsync(request(params)).toBeResolved(); - await request(params).then(fail, e => { - expect(e.status).toEqual(400); - expect(e.data.error).toEqual('Duplicate request'); - }); - expect(counter).toBe(1); - }); + it_id('a030f2dd-5d21-46ac-b53d-9d714f35d72a')(it)( + 'should enforce idempotency for user object creation', + async () => { + let counter = 0; + Parse.Cloud.afterSave('_User', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/users', + body: { + username: 'user', + password: 'pass', + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123', + }, + }; + await expectAsync(request(params)).toBeResolved(); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual('Duplicate request'); + }); + expect(counter).toBe(1); + } + ); - it_id('f11670b6-fa9c-4f21-a268-ae4b6bbff7fd')(it)('should not interfere with calls of different request ID', async () => { - let counter = 0; - Parse.Cloud.afterSave('MyClass', () => { - counter++; - }); - const promises = [...Array(100).keys()].map(() => { + it_id('064c469b-091c-4ba9-9043-be461f26a3eb')(it)( + 'should enforce idempotency for installation object creation', + async () => { + let counter = 0; + Parse.Cloud.afterSave('_Installation', () => { + counter++; + }); const params = { method: 'POST', - url: 'http://localhost:8378/1/classes/MyClass', + url: 'http://localhost:8378/1/installations', + body: { + installationId: '1', + deviceType: 'ios', + }, headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': Parse.masterKey, - 'X-Parse-Request-Id': uuid.v4(), + 'X-Parse-Request-Id': 'abc-123', }, }; - return request(params); - }); - await expectAsync(Promise.all(promises)).toBeResolved(); - expect(counter).toBe(100); - }); + await expectAsync(request(params)).toBeResolved(); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual('Duplicate request'); + }); + expect(counter).toBe(1); + } + ); - it_id('0ecd2cd2-dafb-4a2b-bb2b-9ad4c9aca777')(it)('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => { - spyOn(rest, 'create').and.rejectWith(new Parse.Error(0, 'some other error')); - Parse.Cloud.define('myFunction', () => {}); - const params = { - method: 'POST', - url: 'http://localhost:8378/1/functions/myFunction', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': Parse.masterKey, - 'X-Parse-Request-Id': 'abc-123', - }, - }; - await request(params).then(fail, e => { - expect(e.status).toEqual(400); - expect(e.data.error).toEqual('some other error'); - }); - }); + it_id('f11670b6-fa9c-4f21-a268-ae4b6bbff7fd')(it)( + 'should not interfere with calls of different request ID', + async () => { + let counter = 0; + Parse.Cloud.afterSave('MyClass', () => { + counter++; + }); + const promises = [...Array(100).keys()].map(() => { + const params = { + method: 'POST', + url: 'http://localhost:8378/1/classes/MyClass', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': uuid.v4(), + }, + }; + return request(params); + }); + await expectAsync(Promise.all(promises)).toBeResolved(); + expect(counter).toBe(100); + } + ); + + it_id('0ecd2cd2-dafb-4a2b-bb2b-9ad4c9aca777')(it)( + 'should re-throw any other error unchanged when writing request entry fails for any other reason', + async () => { + spyOn(rest, 'create').and.rejectWith(new Parse.Error(0, 'some other error')); + Parse.Cloud.define('myFunction', () => {}); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/functions/myFunction', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123', + }, + }; + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual('some other error'); + }); + } + ); it('should use default configuration when none is set', async () => { await setup({}); diff --git a/spec/JobSchedule.spec.js b/spec/JobSchedule.spec.js index 853eb20143..b4a75764dd 100644 --- a/spec/JobSchedule.spec.js +++ b/spec/JobSchedule.spec.js @@ -46,15 +46,17 @@ describe('JobSchedule', () => { }); it('should reject access when not using masterKey (/jobs)', done => { - request( - Object.assign({ url: Parse.serverURL + '/cloud_code/jobs' }, defaultOptions) - ).then(done.fail, () => done()); + request(Object.assign({ url: Parse.serverURL + '/cloud_code/jobs' }, defaultOptions)).then( + done.fail, + () => done() + ); }); it('should reject access when not using masterKey (/jobs/data)', done => { - request( - Object.assign({ url: Parse.serverURL + '/cloud_code/jobs/data' }, defaultOptions) - ).then(done.fail, () => done()); + request(Object.assign({ url: Parse.serverURL + '/cloud_code/jobs/data' }, defaultOptions)).then( + done.fail, + () => done() + ); }); it('should reject access when not using masterKey (PUT /jobs/id)', done => { diff --git a/spec/LoggerController.spec.js b/spec/LoggerController.spec.js index 37d477444c..eee9bba8a8 100644 --- a/spec/LoggerController.spec.js +++ b/spec/LoggerController.spec.js @@ -1,6 +1,6 @@ const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') - .WinstonLoggerAdapter; +const WinstonLoggerAdapter = + require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; describe('LoggerController', () => { it('can process an empty query without throwing', done => { diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js index b25ac25be5..a53072ef86 100644 --- a/spec/LogsRouter.spec.js +++ b/spec/LogsRouter.spec.js @@ -3,8 +3,8 @@ const request = require('../lib/request'); const LogsRouter = require('../lib/Routers/LogsRouter').LogsRouter; const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') - .WinstonLoggerAdapter; +const WinstonLoggerAdapter = + require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; const loggerController = new LoggerController(new WinstonLoggerAdapter()); @@ -75,50 +75,19 @@ describe_only(() => { /** * Verifies simple passwords in GET login requests with special characters are scrubbed from the verbose log */ - it_id('e36d6141-2a20-41d0-85fc-d1534c3e4bae')(it)('does scrub simple passwords on GET login', done => { - reconfigureServer({ - verbose: true, - }).then(function () { - request({ - headers: headers, - url: 'http://localhost:8378/1/login?username=test&password=simplepass.com', - }) - .catch(() => {}) - .then(() => { - request({ - url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', - headers: headers, - }).then(response => { - const body = response.data; - expect(response.status).toEqual(200); - // 4th entry is our actual GET request - expect(body[2].url).toEqual('/1/login?username=test&password=********'); - expect(body[2].message).toEqual( - 'REQUEST for [GET] /1/login?username=test&password=********: {}' - ); - done(); - }); - }); - }); - }); - - /** - * Verifies complex passwords in GET login requests with special characters are scrubbed from the verbose log - */ - it_id('24b277c5-250f-4a35-a449-2c8c519d4c03')(it)('does scrub complex passwords on GET login', done => { - reconfigureServer({ - verbose: true, - }) - .then(function () { - return request({ + it_id('e36d6141-2a20-41d0-85fc-d1534c3e4bae')(it)( + 'does scrub simple passwords on GET login', + done => { + reconfigureServer({ + verbose: true, + }).then(function () { + request({ headers: headers, - // using urlencoded password, 'simple @,/?:&=+$#pass.com' - url: - 'http://localhost:8378/1/login?username=test&password=simple%20%40%2C%2F%3F%3A%26%3D%2B%24%23pass.com', + url: 'http://localhost:8378/1/login?username=test&password=simplepass.com', }) .catch(() => {}) .then(() => { - return request({ + request({ url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', headers: headers, }).then(response => { @@ -132,42 +101,81 @@ describe_only(() => { done(); }); }); - }) - .catch(done.fail); - }); + }); + } + ); /** - * Verifies fields in POST login requests are NOT present in the verbose log + * Verifies complex passwords in GET login requests with special characters are scrubbed from the verbose log */ - it_id('33143ec9-b32d-467c-ba65-ff2bbefdaadd')(it)('does not have password field in POST login', done => { - reconfigureServer({ - verbose: true, - }).then(function () { - request({ - method: 'POST', - headers: headers, - url: 'http://localhost:8378/1/login', - body: { - username: 'test', - password: 'simplepass.com', - }, + it_id('24b277c5-250f-4a35-a449-2c8c519d4c03')(it)( + 'does scrub complex passwords on GET login', + done => { + reconfigureServer({ + verbose: true, }) - .catch(() => {}) - .then(() => { - request({ - url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', + .then(function () { + return request({ headers: headers, - }).then(response => { - const body = response.data; - expect(response.status).toEqual(200); - // 4th entry is our actual GET request - expect(body[2].url).toEqual('/1/login'); - expect(body[2].message).toEqual( - 'REQUEST for [POST] /1/login: {\n "username": "test",\n "password": "********"\n}' - ); - done(); + // using urlencoded password, 'simple @,/?:&=+$#pass.com' + url: 'http://localhost:8378/1/login?username=test&password=simple%20%40%2C%2F%3F%3A%26%3D%2B%24%23pass.com', + }) + .catch(() => {}) + .then(() => { + return request({ + url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', + headers: headers, + }).then(response => { + const body = response.data; + expect(response.status).toEqual(200); + // 4th entry is our actual GET request + expect(body[2].url).toEqual('/1/login?username=test&password=********'); + expect(body[2].message).toEqual( + 'REQUEST for [GET] /1/login?username=test&password=********: {}' + ); + done(); + }); + }); + }) + .catch(done.fail); + } + ); + + /** + * Verifies fields in POST login requests are NOT present in the verbose log + */ + it_id('33143ec9-b32d-467c-ba65-ff2bbefdaadd')(it)( + 'does not have password field in POST login', + done => { + reconfigureServer({ + verbose: true, + }).then(function () { + request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/login', + body: { + username: 'test', + password: 'simplepass.com', + }, + }) + .catch(() => {}) + .then(() => { + request({ + url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', + headers: headers, + }).then(response => { + const body = response.data; + expect(response.status).toEqual(200); + // 4th entry is our actual GET request + expect(body[2].url).toEqual('/1/login'); + expect(body[2].message).toEqual( + 'REQUEST for [POST] /1/login: {\n "username": "test",\n "password": "********"\n}' + ); + done(); + }); }); - }); - }); - }); + }); + } + ); }); diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 57dca22b0e..2ad29768bd 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -128,43 +128,49 @@ describe('middlewares', () => { const otherKeys = BodyKeys.filter( otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey' ); - it_id('f9abd7ac-b1f4-4607-b9b0-365ff0559d84')(it)(`it should pull ${bodyKey} into req.info`, done => { - AppCachePut(fakeReq.body._ApplicationId, { - masterKeyIps: ['0.0.0.0/0'], - }); - fakeReq.ip = '127.0.0.1'; - fakeReq.body[bodyKey] = keyValue; - middlewares.handleParseHeaders(fakeReq, fakeRes, () => { - expect(fakeReq.body[bodyKey]).toEqual(undefined); - expect(fakeReq.info[infoKey]).toEqual(keyValue); - - otherKeys.forEach(otherKey => { - expect(fakeReq.info[otherKey]).toEqual(undefined); + it_id('f9abd7ac-b1f4-4607-b9b0-365ff0559d84')(it)( + `it should pull ${bodyKey} into req.info`, + done => { + AppCachePut(fakeReq.body._ApplicationId, { + masterKeyIps: ['0.0.0.0/0'], }); + fakeReq.ip = '127.0.0.1'; + fakeReq.body[bodyKey] = keyValue; + middlewares.handleParseHeaders(fakeReq, fakeRes, () => { + expect(fakeReq.body[bodyKey]).toEqual(undefined); + expect(fakeReq.info[infoKey]).toEqual(keyValue); - done(); - }); - }); + otherKeys.forEach(otherKey => { + expect(fakeReq.info[otherKey]).toEqual(undefined); + }); + + done(); + }); + } + ); }); - it_id('4a0bce41-c536-4482-a873-12ed023380e2')(it)('should not succeed and log if the ip does not belong to masterKeyIps list', async () => { - const logger = require('../lib/logger').logger; - spyOn(logger, 'error').and.callFake(() => {}); - AppCachePut(fakeReq.body._ApplicationId, { - masterKey: 'masterKey', - masterKeyIps: ['10.0.0.1'], - }); - fakeReq.ip = '127.0.0.1'; - fakeReq.headers['x-parse-master-key'] = 'masterKey'; + it_id('4a0bce41-c536-4482-a873-12ed023380e2')(it)( + 'should not succeed and log if the ip does not belong to masterKeyIps list', + async () => { + const logger = require('../lib/logger').logger; + spyOn(logger, 'error').and.callFake(() => {}); + AppCachePut(fakeReq.body._ApplicationId, { + masterKey: 'masterKey', + masterKeyIps: ['10.0.0.1'], + }); + fakeReq.ip = '127.0.0.1'; + fakeReq.headers['x-parse-master-key'] = 'masterKey'; - const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e); + const error = await middlewares.handleParseHeaders(fakeReq, fakeRes, () => {}).catch(e => e); - expect(error).toBeDefined(); - expect(error.message).toEqual(`unauthorized`); - expect(logger.error).toHaveBeenCalledWith( - `Request using master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'masterKeyIps'.` - ); - }); + expect(error).toBeDefined(); + expect(error.message).toEqual(`unauthorized`); + expect(logger.error).toHaveBeenCalledWith( + `Request using master key rejected as the request IP address '127.0.0.1' is not set in Parse Server option 'masterKeyIps'.` + ); + } + ); it('should not succeed and log if the ip does not belong to maintenanceKeyIps list', async () => { const logger = require('../lib/logger').logger; @@ -185,27 +191,33 @@ describe('middlewares', () => { ); }); - it_id('2f7fadec-a87c-4626-90d1-65c75653aea9')(it)('should succeed if the ip does belong to masterKeyIps list', async () => { - AppCachePut(fakeReq.body._ApplicationId, { - masterKey: 'masterKey', - masterKeyIps: ['10.0.0.1'], - }); - fakeReq.ip = '10.0.0.1'; - fakeReq.headers['x-parse-master-key'] = 'masterKey'; - await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); - expect(fakeReq.auth.isMaster).toBe(true); - }); - - it_id('2b251fd4-d43c-48f4-ada9-c8458e40c12a')(it)('should allow any ip to use masterKey if masterKeyIps is empty', async () => { - AppCachePut(fakeReq.body._ApplicationId, { - masterKey: 'masterKey', - masterKeyIps: ['0.0.0.0/0'], - }); - fakeReq.ip = '10.0.0.1'; - fakeReq.headers['x-parse-master-key'] = 'masterKey'; - await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); - expect(fakeReq.auth.isMaster).toBe(true); - }); + it_id('2f7fadec-a87c-4626-90d1-65c75653aea9')(it)( + 'should succeed if the ip does belong to masterKeyIps list', + async () => { + AppCachePut(fakeReq.body._ApplicationId, { + masterKey: 'masterKey', + masterKeyIps: ['10.0.0.1'], + }); + fakeReq.ip = '10.0.0.1'; + fakeReq.headers['x-parse-master-key'] = 'masterKey'; + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); + } + ); + + it_id('2b251fd4-d43c-48f4-ada9-c8458e40c12a')(it)( + 'should allow any ip to use masterKey if masterKeyIps is empty', + async () => { + AppCachePut(fakeReq.body._ApplicationId, { + masterKey: 'masterKey', + masterKeyIps: ['0.0.0.0/0'], + }); + fakeReq.ip = '10.0.0.1'; + fakeReq.headers['x-parse-master-key'] = 'masterKey'; + await new Promise(resolve => middlewares.handleParseHeaders(fakeReq, fakeRes, resolve)); + expect(fakeReq.auth.isMaster).toBe(true); + } + ); it('can set trust proxy', async () => { const server = await reconfigureServer({ trustProxy: 1 }); diff --git a/spec/MongoSchemaCollectionAdapter.spec.js b/spec/MongoSchemaCollectionAdapter.spec.js index 8e376b9d1d..f89151d2bd 100644 --- a/spec/MongoSchemaCollectionAdapter.spec.js +++ b/spec/MongoSchemaCollectionAdapter.spec.js @@ -1,7 +1,7 @@ 'use strict'; -const MongoSchemaCollection = require('../lib/Adapters/Storage/Mongo/MongoSchemaCollection') - .default; +const MongoSchemaCollection = + require('../lib/Adapters/Storage/Mongo/MongoSchemaCollection').default; describe('MongoSchemaCollection', () => { it('can transform legacy _client_permissions keys to parse format', done => { diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index b026fc0961..304e0c4637 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -46,8 +46,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { it('preserves replica sets', () => { spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(fakeClient)); new MongoStorageAdapter({ - uri: - 'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415', + uri: 'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415', }).connect(); expect(MongoClient.connect).toHaveBeenCalledWith( 'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415', diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index 0aa5bb357b..896c4dbffa 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -607,8 +607,7 @@ describe('Pages Router', () => { it('responds to POST request with redirect response', async () => { await reconfigureServer(config); const response = await request({ - url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT', + url: 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT', followRedirects: false, method: 'POST', }); @@ -621,8 +620,7 @@ describe('Pages Router', () => { it('responds to GET request with content response', async () => { await reconfigureServer(config); const response = await request({ - url: - 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT', + url: 'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT', followRedirects: false, method: 'GET', }); @@ -717,144 +715,153 @@ describe('Pages Router', () => { ); }); - it_id('2845c2ea-23ba-45d2-a33f-63181d419bca')(it)('localizes end-to-end for verify email: success', async () => { - await reconfigureServer(config); - const sendVerificationEmail = spyOn( - config.emailAdapter, - 'sendVerificationEmail' - ).and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - await jasmine.timeout(); - - const link = sendVerificationEmail.calls.all()[0].args[0].link; - const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); - - const linkResponse = await request({ - url: linkWithLocale.toString(), - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - - const pagePath = pageResponse.calls.all()[0].args[0]; - expect(pagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.emailVerificationSuccess.defaultFile}`) - ); - }); - - it_id('f2272b94-b4ac-474f-8e47-1ca74de136f5')(it)('localizes end-to-end for verify email: invalid verification link - link send success', async () => { - await reconfigureServer(config); - const sendVerificationEmail = spyOn( - config.emailAdapter, - 'sendVerificationEmail' - ).and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - await jasmine.timeout(); - - const link = sendVerificationEmail.calls.all()[0].args[0].link; - const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); - linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); - - const linkResponse = await request({ - url: linkWithLocale.toString(), - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - - const appId = linkResponse.headers['x-parse-page-param-appid']; - const locale = linkResponse.headers['x-parse-page-param-locale']; - const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; - const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; - expect(appId).toBeDefined(); - expect(locale).toBe(exampleLocale); - expect(publicServerUrl).toBeDefined(); - expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`) - ); - - const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; - const formResponse = await request({ - url: formUrl, - method: 'POST', - body: { - locale, - username: 'exampleUsername', - }, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - followRedirects: false, - }); - expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain( - `/${locale}/${pages.emailVerificationSendSuccess.defaultFile}` - ); - }); - - it_id('1d46d36a-e455-4ae7-8717-e0d286e95f02')(it)('localizes end-to-end for verify email: invalid verification link - link send fail', async () => { - await reconfigureServer(config); - const sendVerificationEmail = spyOn( - config.emailAdapter, - 'sendVerificationEmail' - ).and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - await jasmine.timeout(); - - const link = sendVerificationEmail.calls.all()[0].args[0].link; - const linkWithLocale = new URL(link); - linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); - linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); - - const linkResponse = await request({ - url: linkWithLocale.toString(), - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - - const appId = linkResponse.headers['x-parse-page-param-appid']; - const locale = linkResponse.headers['x-parse-page-param-locale']; - const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; - await jasmine.timeout(); + it_id('2845c2ea-23ba-45d2-a33f-63181d419bca')(it)( + 'localizes end-to-end for verify email: success', + async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn( + config.emailAdapter, + 'sendVerificationEmail' + ).and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + await jasmine.timeout(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const pagePath = pageResponse.calls.all()[0].args[0]; + expect(pagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationSuccess.defaultFile}`) + ); + } + ); + + it_id('f2272b94-b4ac-474f-8e47-1ca74de136f5')(it)( + 'localizes end-to-end for verify email: invalid verification link - link send success', + async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn( + config.emailAdapter, + 'sendVerificationEmail' + ).and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + await jasmine.timeout(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const appId = linkResponse.headers['x-parse-page-param-appid']; + const locale = linkResponse.headers['x-parse-page-param-locale']; + const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; + expect(appId).toBeDefined(); + expect(locale).toBe(exampleLocale); + expect(publicServerUrl).toBeDefined(); + expect(invalidVerificationPagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`) + ); - const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; - expect(appId).toBeDefined(); - expect(locale).toBe(exampleLocale); - expect(publicServerUrl).toBeDefined(); - expect(invalidVerificationPagePath).toMatch( - new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`) - ); + const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + locale, + username: 'exampleUsername', + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(303); + expect(formResponse.text).toContain( + `/${locale}/${pages.emailVerificationSendSuccess.defaultFile}` + ); + } + ); + + it_id('1d46d36a-e455-4ae7-8717-e0d286e95f02')(it)( + 'localizes end-to-end for verify email: invalid verification link - link send fail', + async () => { + await reconfigureServer(config); + const sendVerificationEmail = spyOn( + config.emailAdapter, + 'sendVerificationEmail' + ).and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + await jasmine.timeout(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkWithLocale = new URL(link); + linkWithLocale.searchParams.append(pageParams.locale, exampleLocale); + linkWithLocale.searchParams.set(pageParams.token, 'invalidToken'); + + const linkResponse = await request({ + url: linkWithLocale.toString(), + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + + const appId = linkResponse.headers['x-parse-page-param-appid']; + const locale = linkResponse.headers['x-parse-page-param-locale']; + const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + await jasmine.timeout(); + + const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; + expect(appId).toBeDefined(); + expect(locale).toBe(exampleLocale); + expect(publicServerUrl).toBeDefined(); + expect(invalidVerificationPagePath).toMatch( + new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`) + ); - spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => - Promise.reject('failed to resend verification email') - ); + spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() => + Promise.reject('failed to resend verification email') + ); - const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; - const formResponse = await request({ - url: formUrl, - method: 'POST', - body: { - locale, - username: 'exampleUsername', - }, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - followRedirects: false, - }); - expect(formResponse.status).toEqual(303); - expect(formResponse.text).toContain( - `/${locale}/${pages.emailVerificationSendFail.defaultFile}` - ); - }); + const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`; + const formResponse = await request({ + url: formUrl, + method: 'POST', + body: { + locale, + username: 'exampleUsername', + }, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + followRedirects: false, + }); + expect(formResponse.status).toEqual(303); + expect(formResponse.text).toContain( + `/${locale}/${pages.emailVerificationSendFail.defaultFile}` + ); + } + ); it('localizes end-to-end for resend verification email: invalid link', async () => { await reconfigureServer(config); @@ -1155,29 +1162,32 @@ describe('Pages Router', () => { ); }); - it_id('81c1c28e-5dfd-4ffb-a09b-283156c08483')(it)('email verification works with custom endpoint', async () => { - config.pages.pagesEndpoint = 'customEndpoint'; - await reconfigureServer(config); - const sendVerificationEmail = spyOn( - config.emailAdapter, - 'sendVerificationEmail' - ).and.callThrough(); - const user = new Parse.User(); - user.setUsername('exampleUsername'); - user.setPassword('examplePassword'); - user.set('email', 'mail@example.com'); - await user.signUp(); - await jasmine.timeout(); - - const link = sendVerificationEmail.calls.all()[0].args[0].link; - const linkResponse = await request({ - url: link, - followRedirects: false, - }); - expect(linkResponse.status).toBe(200); - const pagePath = pageResponse.calls.all()[0].args[0]; - expect(pagePath).toMatch(new RegExp(`\/${pages.emailVerificationSuccess.defaultFile}`)); - }); + it_id('81c1c28e-5dfd-4ffb-a09b-283156c08483')(it)( + 'email verification works with custom endpoint', + async () => { + config.pages.pagesEndpoint = 'customEndpoint'; + await reconfigureServer(config); + const sendVerificationEmail = spyOn( + config.emailAdapter, + 'sendVerificationEmail' + ).and.callThrough(); + const user = new Parse.User(); + user.setUsername('exampleUsername'); + user.setPassword('examplePassword'); + user.set('email', 'mail@example.com'); + await user.signUp(); + await jasmine.timeout(); + + const link = sendVerificationEmail.calls.all()[0].args[0].link; + const linkResponse = await request({ + url: link, + followRedirects: false, + }); + expect(linkResponse.status).toBe(200); + const pagePath = pageResponse.calls.all()[0].args[0]; + expect(pagePath).toMatch(new RegExp(`\/${pages.emailVerificationSuccess.defaultFile}`)); + } + ); }); }); }); diff --git a/spec/Parse.Push.spec.js b/spec/Parse.Push.spec.js index 6303496de1..61dbdba103 100644 --- a/spec/Parse.Push.spec.js +++ b/spec/Parse.Push.spec.js @@ -126,71 +126,80 @@ describe('Parse.Push', () => { expect(sendToInstallationSpy.calls.count()).toEqual(10); }); - it_id('2a58e3c7-b6f3-4261-a384-6c893b2ac3f3')(it)('should properly send push with lowercaseIncrement', async () => { - await setup(); - const pushStatusId = await Parse.Push.send({ - where: { - deviceType: 'ios', - }, - data: { - badge: 'increment', - alert: 'Hello world!', - }, - }); - await pushCompleted(pushStatusId); - }); + it_id('2a58e3c7-b6f3-4261-a384-6c893b2ac3f3')(it)( + 'should properly send push with lowercaseIncrement', + async () => { + await setup(); + const pushStatusId = await Parse.Push.send({ + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', + }, + }); + await pushCompleted(pushStatusId); + } + ); - it_id('e21780b6-2cdd-467e-8013-81030f3288e9')(it)('should not allow clients to query _PushStatus', async () => { - await setup(); - const pushStatusId = await Parse.Push.send({ - where: { - deviceType: 'ios', - }, - data: { - badge: 'increment', - alert: 'Hello world!', - }, - }); - await pushCompleted(pushStatusId); - try { - await request({ + it_id('e21780b6-2cdd-467e-8013-81030f3288e9')(it)( + 'should not allow clients to query _PushStatus', + async () => { + await setup(); + const pushStatusId = await Parse.Push.send({ + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', + }, + }); + await pushCompleted(pushStatusId); + try { + await request({ + url: 'http://localhost:8378/1/classes/_PushStatus', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + }, + }); + fail(); + } catch (response) { + expect(response.data.error).toEqual('unauthorized'); + } + } + ); + + it_id('924cf5f5-f684-4925-978a-e52c0c457366')(it)( + 'should allow master key to query _PushStatus', + async () => { + await setup(); + const pushStatusId = await Parse.Push.send({ + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', + }, + }); + await pushCompleted(pushStatusId); + const response = await request({ url: 'http://localhost:8378/1/classes/_PushStatus', json: true, headers: { 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', }, }); - fail(); - } catch (response) { - expect(response.data.error).toEqual('unauthorized'); + const body = response.data; + expect(body.results.length).toEqual(1); + expect(body.results[0].query).toEqual('{"deviceType":"ios"}'); + expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}'); } - }); - - it_id('924cf5f5-f684-4925-978a-e52c0c457366')(it)('should allow master key to query _PushStatus', async () => { - await setup(); - const pushStatusId = await Parse.Push.send({ - where: { - deviceType: 'ios', - }, - data: { - badge: 'increment', - alert: 'Hello world!', - }, - }); - await pushCompleted(pushStatusId); - const response = await request({ - url: 'http://localhost:8378/1/classes/_PushStatus', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - }); - const body = response.data; - expect(body.results.length).toEqual(1); - expect(body.results[0].query).toEqual('{"deviceType":"ios"}'); - expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}'); - }); + ); it('should throw error if missing push configuration', async () => { await reconfigureServer({ push: null }); diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index d8abc65c06..b8a982ec45 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -7,7 +7,9 @@ const auth = require('../lib/Auth'); describe('Parse.ACL', () => { it('acl must be valid', () => { const user = new Parse.User(); - expect(() => user.setACL('ACL')).toThrow(new Parse.Error(Parse.Error.OTHER_CAUSE, 'ACL must be a Parse ACL.')); + expect(() => user.setACL('ACL')).toThrow( + new Parse.Error(Parse.Error.OTHER_CAUSE, 'ACL must be a Parse ACL.') + ); }); it('refresh object with acl', async done => { diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index a178a1b863..1c5d5ddbc7 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -163,90 +163,96 @@ describe('miscellaneous', function () { expect(numCreated).toBe(1); }); - it_id('be1b9ac7-5e5f-4e91-b044-2bd8fb7622ad')(it)('ensure that if people already have duplicate users, they can still sign up new users', async done => { - try { - await Parse.User.logOut(); - } catch (e) { - /* ignore */ + it_id('be1b9ac7-5e5f-4e91-b044-2bd8fb7622ad')(it)( + 'ensure that if people already have duplicate users, they can still sign up new users', + async done => { + try { + await Parse.User.logOut(); + } catch (e) { + /* ignore */ + } + const config = Config.get('test'); + // Remove existing data to clear out unique index + TestUtils.destroyAllDataPermanently() + .then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] })) + .then(() => config.database.adapter.createClass('_User', userSchema)) + .then(() => + config.database.adapter + .createObject('_User', userSchema, { objectId: 'x', username: 'u' }) + .catch(fail) + ) + .then(() => + config.database.adapter + .createObject('_User', userSchema, { objectId: 'y', username: 'u' }) + .catch(fail) + ) + // Create a new server to try to recreate the unique indexes + .then(reconfigureServer) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + return user.signUp().catch(fail); + }) + .then(() => { + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('u'); + return user.signUp(); + }) + .then(() => { + fail('should not have been able to sign up'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + done(); + }); } - const config = Config.get('test'); - // Remove existing data to clear out unique index - TestUtils.destroyAllDataPermanently() - .then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] })) - .then(() => config.database.adapter.createClass('_User', userSchema)) - .then(() => - config.database.adapter - .createObject('_User', userSchema, { objectId: 'x', username: 'u' }) - .catch(fail) - ) - .then(() => - config.database.adapter - .createObject('_User', userSchema, { objectId: 'y', username: 'u' }) - .catch(fail) - ) - // Create a new server to try to recreate the unique indexes - .then(reconfigureServer) - .catch(error => { - expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); - const user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('zxcv'); - return user.signUp().catch(fail); - }) - .then(() => { - const user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('u'); - return user.signUp(); - }) - .then(() => { - fail('should not have been able to sign up'); - done(); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - done(); - }); - }); - - it_id('d00f907e-41b9-40f6-8168-63e832199a8c')(it)('ensure that if people already have duplicate emails, they can still sign up new users', done => { - const config = Config.get('test'); - // Remove existing data to clear out unique index - TestUtils.destroyAllDataPermanently() - .then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] })) - .then(() => config.database.adapter.createClass('_User', userSchema)) - .then(() => - config.database.adapter.createObject('_User', userSchema, { - objectId: 'x', - email: 'a@b.c', + ); + + it_id('d00f907e-41b9-40f6-8168-63e832199a8c')(it)( + 'ensure that if people already have duplicate emails, they can still sign up new users', + done => { + const config = Config.get('test'); + // Remove existing data to clear out unique index + TestUtils.destroyAllDataPermanently() + .then(() => config.database.adapter.performInitialization({ VolatileClassesSchemas: [] })) + .then(() => config.database.adapter.createClass('_User', userSchema)) + .then(() => + config.database.adapter.createObject('_User', userSchema, { + objectId: 'x', + email: 'a@b.c', + }) + ) + .then(() => + config.database.adapter.createObject('_User', userSchema, { + objectId: 'y', + email: 'a@b.c', + }) + ) + .then(reconfigureServer) + .catch(() => { + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('qqq'); + user.setEmail('unique@unique.unique'); + return user.signUp().catch(fail); }) - ) - .then(() => - config.database.adapter.createObject('_User', userSchema, { - objectId: 'y', - email: 'a@b.c', + .then(() => { + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('www'); + user.setEmail('a@b.c'); + return user.signUp(); }) - ) - .then(reconfigureServer) - .catch(() => { - const user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('qqq'); - user.setEmail('unique@unique.unique'); - return user.signUp().catch(fail); - }) - .then(() => { - const user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('www'); - user.setEmail('a@b.c'); - return user.signUp(); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - done(); - }); - }); + .catch(error => { + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + done(); + }); + } + ); it('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', async done => { await reconfigureServer(); @@ -289,33 +295,36 @@ describe('miscellaneous', function () { }, fail); }); - it_id('33db6efe-7c02-496c-8595-0ef627a94103')(it)('increment with a user object', function (done) { - createTestUser() - .then(user => { - user.increment('foo'); - return user.save(); - }) - .then(() => { - return Parse.User.logIn('test', 'moon-y'); - }) - .then(user => { - expect(user.get('foo')).toEqual(1); - user.increment('foo'); - return user.save(); - }) - .then(() => Parse.User.logOut()) - .then(() => Parse.User.logIn('test', 'moon-y')) - .then( - user => { - expect(user.get('foo')).toEqual(2); - Parse.User.logOut().then(done); - }, - error => { - fail(JSON.stringify(error)); - done(); - } - ); - }); + it_id('33db6efe-7c02-496c-8595-0ef627a94103')(it)( + 'increment with a user object', + function (done) { + createTestUser() + .then(user => { + user.increment('foo'); + return user.save(); + }) + .then(() => { + return Parse.User.logIn('test', 'moon-y'); + }) + .then(user => { + expect(user.get('foo')).toEqual(1); + user.increment('foo'); + return user.save(); + }) + .then(() => Parse.User.logOut()) + .then(() => Parse.User.logIn('test', 'moon-y')) + .then( + user => { + expect(user.get('foo')).toEqual(2); + Parse.User.logOut().then(done); + }, + error => { + fail(JSON.stringify(error)); + done(); + } + ); + } + ); it_id('bef99522-bcfd-4f79-ba9e-3c3845550401')(it)('save various data types', function (done) { const obj = new TestObject(); @@ -951,155 +960,161 @@ describe('miscellaneous', function () { ); }); - it_id('e9e718a9-4465-4158-b13e-f146855a8892')(it)('return the updated fields on PUT', async () => { - const obj = new Parse.Object('GameScore'); - const pointer = new Parse.Object('Child'); - await pointer.save(); - obj.set( - 'point', - new Parse.GeoPoint({ - latitude: 37.4848, - longitude: -122.1483, - }) - ); - obj.set('array', ['obj1', 'obj2']); - obj.set('objects', { a: 'b' }); - obj.set('string', 'abc'); - obj.set('bool', true); - obj.set('number', 1); - obj.set('date', new Date()); - obj.set('pointer', pointer); - const headers = { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Installation-Id': 'yolo', - }; - const saveResponse = await request({ - method: 'POST', - headers: headers, - url: 'http://localhost:8378/1/classes/GameScore', - body: JSON.stringify({ - a: 'hello', - c: 1, - d: ['1'], - e: ['1'], - f: ['1', '2'], - ...obj.toJSON(), - }), - }); - expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']); - obj.id = saveResponse.data.objectId; - const response = await request({ - method: 'PUT', - headers: headers, - url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, - body: JSON.stringify({ - a: 'b', - c: { __op: 'Increment', amount: 2 }, - d: { __op: 'Add', objects: ['2'] }, - e: { __op: 'AddUnique', objects: ['1', '2'] }, - f: { __op: 'Remove', objects: ['2'] }, - selfThing: { - __type: 'Pointer', - className: 'GameScore', - objectId: obj.id, - }, - }), - }); - const body = response.data; - expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']); - expect(body.a).toBeUndefined(); - expect(body.c).toEqual(3); // 2+1 - expect(body.d.length).toBe(2); - expect(body.d.indexOf('1') > -1).toBe(true); - expect(body.d.indexOf('2') > -1).toBe(true); - expect(body.e.length).toBe(2); - expect(body.e.indexOf('1') > -1).toBe(true); - expect(body.e.indexOf('2') > -1).toBe(true); - expect(body.f.length).toBe(1); - expect(body.f.indexOf('1') > -1).toBe(true); - expect(body.selfThing).toBeUndefined(); - expect(body.updatedAt).not.toBeUndefined(); - }); - - it_id('ea358b59-03c0-45c9-abc7-1fdd67573029')(it)('should response should not change with triggers', async () => { - const obj = new Parse.Object('GameScore'); - const pointer = new Parse.Object('Child'); - Parse.Cloud.beforeSave('GameScore', request => { - return request.object; - }); - Parse.Cloud.afterSave('GameScore', request => { - return request.object; - }); - await pointer.save(); - obj.set( - 'point', - new Parse.GeoPoint({ - latitude: 37.4848, - longitude: -122.1483, - }) - ); - obj.set('array', ['obj1', 'obj2']); - obj.set('objects', { a: 'b' }); - obj.set('string', 'abc'); - obj.set('bool', true); - obj.set('number', 1); - obj.set('date', new Date()); - obj.set('pointer', pointer); - const headers = { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Installation-Id': 'yolo', - }; - const saveResponse = await request({ - method: 'POST', - headers: headers, - url: 'http://localhost:8378/1/classes/GameScore', - body: JSON.stringify({ - a: 'hello', - c: 1, - d: ['1'], - e: ['1'], - f: ['1', '2'], - ...obj.toJSON(), - }), - }); - expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']); - obj.id = saveResponse.data.objectId; - const response = await request({ - method: 'PUT', - headers: headers, - url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, - body: JSON.stringify({ - a: 'b', - c: { __op: 'Increment', amount: 2 }, - d: { __op: 'Add', objects: ['2'] }, - e: { __op: 'AddUnique', objects: ['1', '2'] }, - f: { __op: 'Remove', objects: ['2'] }, - selfThing: { - __type: 'Pointer', - className: 'GameScore', - objectId: obj.id, - }, - }), - }); - const body = response.data; - expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']); - expect(body.a).toBeUndefined(); - expect(body.c).toEqual(3); // 2+1 - expect(body.d.length).toBe(2); - expect(body.d.indexOf('1') > -1).toBe(true); - expect(body.d.indexOf('2') > -1).toBe(true); - expect(body.e.length).toBe(2); - expect(body.e.indexOf('1') > -1).toBe(true); - expect(body.e.indexOf('2') > -1).toBe(true); - expect(body.f.length).toBe(1); - expect(body.f.indexOf('1') > -1).toBe(true); - expect(body.selfThing).toBeUndefined(); - expect(body.updatedAt).not.toBeUndefined(); - }); + it_id('e9e718a9-4465-4158-b13e-f146855a8892')(it)( + 'return the updated fields on PUT', + async () => { + const obj = new Parse.Object('GameScore'); + const pointer = new Parse.Object('Child'); + await pointer.save(); + obj.set( + 'point', + new Parse.GeoPoint({ + latitude: 37.4848, + longitude: -122.1483, + }) + ); + obj.set('array', ['obj1', 'obj2']); + obj.set('objects', { a: 'b' }); + obj.set('string', 'abc'); + obj.set('bool', true); + obj.set('number', 1); + obj.set('date', new Date()); + obj.set('pointer', pointer); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Installation-Id': 'yolo', + }; + const saveResponse = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore', + body: JSON.stringify({ + a: 'hello', + c: 1, + d: ['1'], + e: ['1'], + f: ['1', '2'], + ...obj.toJSON(), + }), + }); + expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']); + obj.id = saveResponse.data.objectId; + const response = await request({ + method: 'PUT', + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, + body: JSON.stringify({ + a: 'b', + c: { __op: 'Increment', amount: 2 }, + d: { __op: 'Add', objects: ['2'] }, + e: { __op: 'AddUnique', objects: ['1', '2'] }, + f: { __op: 'Remove', objects: ['2'] }, + selfThing: { + __type: 'Pointer', + className: 'GameScore', + objectId: obj.id, + }, + }), + }); + const body = response.data; + expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']); + expect(body.a).toBeUndefined(); + expect(body.c).toEqual(3); // 2+1 + expect(body.d.length).toBe(2); + expect(body.d.indexOf('1') > -1).toBe(true); + expect(body.d.indexOf('2') > -1).toBe(true); + expect(body.e.length).toBe(2); + expect(body.e.indexOf('1') > -1).toBe(true); + expect(body.e.indexOf('2') > -1).toBe(true); + expect(body.f.length).toBe(1); + expect(body.f.indexOf('1') > -1).toBe(true); + expect(body.selfThing).toBeUndefined(); + expect(body.updatedAt).not.toBeUndefined(); + } + ); + + it_id('ea358b59-03c0-45c9-abc7-1fdd67573029')(it)( + 'should response should not change with triggers', + async () => { + const obj = new Parse.Object('GameScore'); + const pointer = new Parse.Object('Child'); + Parse.Cloud.beforeSave('GameScore', request => { + return request.object; + }); + Parse.Cloud.afterSave('GameScore', request => { + return request.object; + }); + await pointer.save(); + obj.set( + 'point', + new Parse.GeoPoint({ + latitude: 37.4848, + longitude: -122.1483, + }) + ); + obj.set('array', ['obj1', 'obj2']); + obj.set('objects', { a: 'b' }); + obj.set('string', 'abc'); + obj.set('bool', true); + obj.set('number', 1); + obj.set('date', new Date()); + obj.set('pointer', pointer); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Installation-Id': 'yolo', + }; + const saveResponse = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore', + body: JSON.stringify({ + a: 'hello', + c: 1, + d: ['1'], + e: ['1'], + f: ['1', '2'], + ...obj.toJSON(), + }), + }); + expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']); + obj.id = saveResponse.data.objectId; + const response = await request({ + method: 'PUT', + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, + body: JSON.stringify({ + a: 'b', + c: { __op: 'Increment', amount: 2 }, + d: { __op: 'Add', objects: ['2'] }, + e: { __op: 'AddUnique', objects: ['1', '2'] }, + f: { __op: 'Remove', objects: ['2'] }, + selfThing: { + __type: 'Pointer', + className: 'GameScore', + objectId: obj.id, + }, + }), + }); + const body = response.data; + expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']); + expect(body.a).toBeUndefined(); + expect(body.c).toEqual(3); // 2+1 + expect(body.d.length).toBe(2); + expect(body.d.indexOf('1') > -1).toBe(true); + expect(body.d.indexOf('2') > -1).toBe(true); + expect(body.e.length).toBe(2); + expect(body.e.indexOf('1') > -1).toBe(true); + expect(body.e.indexOf('2') > -1).toBe(true); + expect(body.f.length).toBe(1); + expect(body.f.indexOf('1') > -1).toBe(true); + expect(body.selfThing).toBeUndefined(); + expect(body.updatedAt).not.toBeUndefined(); + } + ); it('test cloud function error handling', done => { // Register a function which will fail @@ -1491,47 +1506,50 @@ describe('miscellaneous', function () { }); }); - it_id('b2cd9cf2-13fa-4acd-aaa9-6f81fc1858db')(it)('properly returns incremented values (#1554)', done => { - const headers = { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }; - const requestOptions = { - headers: headers, - url: 'http://localhost:8378/1/classes/AnObject', - json: true, - }; - const object = new Parse.Object('AnObject'); - - function runIncrement(amount) { - const options = Object.assign({}, requestOptions, { - body: { - key: { - __op: 'Increment', - amount: amount, + it_id('b2cd9cf2-13fa-4acd-aaa9-6f81fc1858db')(it)( + 'properly returns incremented values (#1554)', + done => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const requestOptions = { + headers: headers, + url: 'http://localhost:8378/1/classes/AnObject', + json: true, + }; + const object = new Parse.Object('AnObject'); + + function runIncrement(amount) { + const options = Object.assign({}, requestOptions, { + body: { + key: { + __op: 'Increment', + amount: amount, + }, }, - }, - url: 'http://localhost:8378/1/classes/AnObject/' + object.id, - method: 'PUT', - }); - return request(options).then(res => res.data); - } + url: 'http://localhost:8378/1/classes/AnObject/' + object.id, + method: 'PUT', + }); + return request(options).then(res => res.data); + } - object - .save() - .then(() => { - return runIncrement(1); - }) - .then(res => { - expect(res.key).toBe(1); - return runIncrement(-1); - }) - .then(res => { - expect(res.key).toBe(0); - done(); - }); - }); + object + .save() + .then(() => { + return runIncrement(1); + }) + .then(res => { + expect(res.key).toBe(1); + return runIncrement(-1); + }) + .then(res => { + expect(res.key).toBe(0); + done(); + }); + } + ); it('ignores _RevocableSession "header" send by JS SDK', done => { const object = new Parse.Object('AnObject'); diff --git a/spec/ParseCloudCodePublisher.spec.js b/spec/ParseCloudCodePublisher.spec.js index 3435d44bde..ef00960687 100644 --- a/spec/ParseCloudCodePublisher.spec.js +++ b/spec/ParseCloudCodePublisher.spec.js @@ -1,5 +1,5 @@ -const ParseCloudCodePublisher = require('../lib/LiveQuery/ParseCloudCodePublisher') - .ParseCloudCodePublisher; +const ParseCloudCodePublisher = + require('../lib/LiveQuery/ParseCloudCodePublisher').ParseCloudCodePublisher; const Parse = require('parse/node'); describe('ParseCloudCodePublisher', function () { diff --git a/spec/ParseConfigKey.spec.js b/spec/ParseConfigKey.spec.js index fe7556123b..f3a1a42419 100644 --- a/spec/ParseConfigKey.spec.js +++ b/spec/ParseConfigKey.spec.js @@ -11,53 +11,57 @@ describe('Config Keys', () => { }); it('recognizes invalid keys in root', async () => { - await expectAsync(reconfigureServer({ - ...defaultConfiguration, - invalidKey: 1, - })).toBeResolved(); - const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], ''); + await expectAsync( + reconfigureServer({ + ...defaultConfiguration, + invalidKey: 1, + }) + ).toBeResolved(); + const error = loggerErrorSpy.calls.all().reduce((s, call) => (s += call.args[0]), ''); expect(error).toMatch(invalidKeyErrorMessage); }); it('recognizes invalid keys in pages.customUrls', async () => { - await expectAsync(reconfigureServer({ - ...defaultConfiguration, - pages: { - customUrls: { - invalidKey: 1, - EmailVerificationSendFail: 1, - } - } - })).toBeResolved(); - const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], ''); + await expectAsync( + reconfigureServer({ + ...defaultConfiguration, + pages: { + customUrls: { + invalidKey: 1, + EmailVerificationSendFail: 1, + }, + }, + }) + ).toBeResolved(); + const error = loggerErrorSpy.calls.all().reduce((s, call) => (s += call.args[0]), ''); expect(error).toMatch(invalidKeyErrorMessage); expect(error).toMatch(`invalidKey`); expect(error).toMatch(`EmailVerificationSendFail`); }); it('recognizes invalid keys in liveQueryServerOptions', async () => { - await expectAsync(reconfigureServer({ - ...defaultConfiguration, - liveQueryServerOptions: { - invalidKey: 1, - MasterKey: 1, - } - })).toBeResolved(); - const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], ''); + await expectAsync( + reconfigureServer({ + ...defaultConfiguration, + liveQueryServerOptions: { + invalidKey: 1, + MasterKey: 1, + }, + }) + ).toBeResolved(); + const error = loggerErrorSpy.calls.all().reduce((s, call) => (s += call.args[0]), ''); expect(error).toMatch(invalidKeyErrorMessage); expect(error).toMatch(`MasterKey`); }); it('recognizes invalid keys in rateLimit', async () => { - await expectAsync(reconfigureServer({ - ...defaultConfiguration, - rateLimit: [ - { invalidKey: 1 }, - { RequestPath: 1 }, - { RequestTimeWindow: 1 }, - ] - })).toBeRejected(); - const error = loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], ''); + await expectAsync( + reconfigureServer({ + ...defaultConfiguration, + rateLimit: [{ invalidKey: 1 }, { RequestPath: 1 }, { RequestTimeWindow: 1 }], + }) + ).toBeRejected(); + const error = loggerErrorSpy.calls.all().reduce((s, call) => (s += call.args[0]), ''); expect(error).toMatch(invalidKeyErrorMessage); expect(error).toMatch('rateLimit\\[0\\]\\.invalidKey'); expect(error).toMatch('rateLimit\\[1\\]\\.RequestPath'); @@ -65,29 +69,37 @@ describe('Config Keys', () => { }); it('recognizes valid keys in default configuration', async () => { - await expectAsync(reconfigureServer({ - ...defaultConfiguration, - })).toBeResolved(); - expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage); + await expectAsync( + reconfigureServer({ + ...defaultConfiguration, + }) + ).toBeResolved(); + expect(loggerErrorSpy.calls.all().reduce((s, call) => (s += call.args[0]), '')).not.toMatch( + invalidKeyErrorMessage + ); }); it_only_db('mongo')('recognizes valid keys in databaseOptions (MongoDB)', async () => { - await expectAsync(reconfigureServer({ - databaseURI: 'mongodb://localhost:27017/parse', - filesAdapter: null, - databaseAdapter: null, - databaseOptions: { - retryWrites: true, - maxTimeMS: 1000, - maxStalenessSeconds: 10, - maxPoolSize: 10, - minPoolSize: 5, - connectTimeoutMS: 5000, - socketTimeoutMS: 5000, - autoSelectFamily: true, - autoSelectFamilyAttemptTimeout: 3000 - }, - })).toBeResolved(); - expect(loggerErrorSpy.calls.all().reduce((s, call) => s += call.args[0], '')).not.toMatch(invalidKeyErrorMessage); + await expectAsync( + reconfigureServer({ + databaseURI: 'mongodb://localhost:27017/parse', + filesAdapter: null, + databaseAdapter: null, + databaseOptions: { + retryWrites: true, + maxTimeMS: 1000, + maxStalenessSeconds: 10, + maxPoolSize: 10, + minPoolSize: 5, + connectTimeoutMS: 5000, + socketTimeoutMS: 5000, + autoSelectFamily: true, + autoSelectFamilyAttemptTimeout: 3000, + }, + }) + ).toBeResolved(); + expect(loggerErrorSpy.calls.all().reduce((s, call) => (s += call.args[0]), '')).not.toMatch( + invalidKeyErrorMessage + ); }); }); diff --git a/spec/ParseGeoPoint.spec.js b/spec/ParseGeoPoint.spec.js index f154f0048e..ac104c1a47 100644 --- a/spec/ParseGeoPoint.spec.js +++ b/spec/ParseGeoPoint.spec.js @@ -207,16 +207,19 @@ describe('Parse.GeoPoint testing', () => { done(); }); - it_id('05f1a454-56b1-4f2e-908e-408a9222cbae')(it)('geo max distance in km california', async () => { - await makeSomeGeoPoints(); - const sfo = new Parse.GeoPoint(37.6189722, -122.3748889); - const query = new Parse.Query(TestObject); - query.withinKilometers('location', sfo, 3700.0); - const results = await query.find(); - equal(results.length, 2); - equal(results[0].get('name'), 'San Francisco'); - equal(results[1].get('name'), 'Sacramento'); - }); + it_id('05f1a454-56b1-4f2e-908e-408a9222cbae')(it)( + 'geo max distance in km california', + async () => { + await makeSomeGeoPoints(); + const sfo = new Parse.GeoPoint(37.6189722, -122.3748889); + const query = new Parse.Query(TestObject); + query.withinKilometers('location', sfo, 3700.0); + const results = await query.find(); + equal(results.length, 2); + equal(results[0].get('name'), 'San Francisco'); + equal(results[1].get('name'), 'Sacramento'); + } + ); it('geo max distance in km bay area', async () => { await makeSomeGeoPoints(); @@ -246,16 +249,19 @@ describe('Parse.GeoPoint testing', () => { equal(results.length, 3); }); - it_id('9ee376ad-dd6c-4c17-ad28-c7899a4411f1')(it)('geo max distance in miles california', async () => { - await makeSomeGeoPoints(); - const sfo = new Parse.GeoPoint(37.6189722, -122.3748889); - const query = new Parse.Query(TestObject); - query.withinMiles('location', sfo, 2200.0); - const results = await query.find(); - equal(results.length, 2); - equal(results[0].get('name'), 'San Francisco'); - equal(results[1].get('name'), 'Sacramento'); - }); + it_id('9ee376ad-dd6c-4c17-ad28-c7899a4411f1')(it)( + 'geo max distance in miles california', + async () => { + await makeSomeGeoPoints(); + const sfo = new Parse.GeoPoint(37.6189722, -122.3748889); + const query = new Parse.Query(TestObject); + query.withinMiles('location', sfo, 2200.0); + const results = await query.find(); + equal(results.length, 2); + equal(results[0].get('name'), 'San Francisco'); + equal(results[1].get('name'), 'Sacramento'); + } + ); it('geo max distance in miles bay area', async () => { await makeSomeGeoPoints(); @@ -433,48 +439,51 @@ describe('Parse.GeoPoint testing', () => { }, done.fail); }); - it_id('0a248e11-3598-480a-9ab5-8a0b259258e4')(it)('supports withinPolygon Polygon object', done => { - const inbound = new Parse.GeoPoint(1.5, 1.5); - const onbound = new Parse.GeoPoint(10, 10); - const outbound = new Parse.GeoPoint(20, 20); - const obj1 = new Parse.Object('Polygon', { location: inbound }); - const obj2 = new Parse.Object('Polygon', { location: onbound }); - const obj3 = new Parse.Object('Polygon', { location: outbound }); - const polygon = { - __type: 'Polygon', - coordinates: [ - [0, 0], - [10, 0], - [10, 10], - [0, 10], - [0, 0], - ], - }; - Parse.Object.saveAll([obj1, obj2, obj3]) - .then(() => { - const where = { - location: { - $geoWithin: { - $polygon: polygon, + it_id('0a248e11-3598-480a-9ab5-8a0b259258e4')(it)( + 'supports withinPolygon Polygon object', + done => { + const inbound = new Parse.GeoPoint(1.5, 1.5); + const onbound = new Parse.GeoPoint(10, 10); + const outbound = new Parse.GeoPoint(20, 20); + const obj1 = new Parse.Object('Polygon', { location: inbound }); + const obj2 = new Parse.Object('Polygon', { location: onbound }); + const obj3 = new Parse.Object('Polygon', { location: outbound }); + const polygon = { + __type: 'Polygon', + coordinates: [ + [0, 0], + [10, 0], + [10, 10], + [0, 10], + [0, 0], + ], + }; + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: polygon, + }, }, - }, - }; - return request({ - method: 'POST', - url: Parse.serverURL + '/classes/Polygon', - body: { where, _method: 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey, - 'Content-Type': 'application/json', - }, - }); - }) - .then(resp => { - expect(resp.data.results.length).toBe(2); - done(); - }, done.fail); - }); + }; + return request({ + method: 'POST', + url: Parse.serverURL + '/classes/Polygon', + body: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + 'Content-Type': 'application/json', + }, + }); + }) + .then(resp => { + expect(resp.data.results.length).toBe(2); + done(); + }, done.fail); + } + ); it('invalid Polygon object withinPolygon', done => { const point = new Parse.GeoPoint(1.5, 1.5); @@ -752,38 +761,44 @@ describe('Parse.GeoPoint testing', () => { equal(count, 1); }); - it_id('0b073d31-0d41-41e7-bd60-f636ffb759dc')(it)('withinKilometers complex supports count', async () => { - const inside = new Parse.GeoPoint(10, 10); - const middle = new Parse.GeoPoint(20, 20); - const outside = new Parse.GeoPoint(30, 30); - const obj1 = new Parse.Object('TestObject', { location: inside }); - const obj2 = new Parse.Object('TestObject', { location: middle }); - const obj3 = new Parse.Object('TestObject', { location: outside }); + it_id('0b073d31-0d41-41e7-bd60-f636ffb759dc')(it)( + 'withinKilometers complex supports count', + async () => { + const inside = new Parse.GeoPoint(10, 10); + const middle = new Parse.GeoPoint(20, 20); + const outside = new Parse.GeoPoint(30, 30); + const obj1 = new Parse.Object('TestObject', { location: inside }); + const obj2 = new Parse.Object('TestObject', { location: middle }); + const obj3 = new Parse.Object('TestObject', { location: outside }); - await Parse.Object.saveAll([obj1, obj2, obj3]); + await Parse.Object.saveAll([obj1, obj2, obj3]); - const q1 = new Parse.Query(TestObject).withinKilometers('location', inside, 5); - const q2 = new Parse.Query(TestObject).withinKilometers('location', middle, 5); - const query = Parse.Query.or(q1, q2); - const count = await query.count(); + const q1 = new Parse.Query(TestObject).withinKilometers('location', inside, 5); + const q2 = new Parse.Query(TestObject).withinKilometers('location', middle, 5); + const query = Parse.Query.or(q1, q2); + const count = await query.count(); - equal(count, 2); - }); + equal(count, 2); + } + ); - it_id('26c9a13d-3d71-452e-a91c-9a4589be021c')(it)('fails to fetch geopoints that are specifically not at (0,0)', async () => { - const tmp = new TestObject({ - location: new Parse.GeoPoint({ latitude: 0, longitude: 0 }), - }); - const tmp2 = new TestObject({ - location: new Parse.GeoPoint({ - latitude: 49.2577142, - longitude: -123.1941149, - }), - }); - await Parse.Object.saveAll([tmp, tmp2]); - const query = new Parse.Query(TestObject); - query.notEqualTo('location', new Parse.GeoPoint({ latitude: 0, longitude: 0 })); - const results = await query.find(); - expect(results.length).toEqual(1); - }); + it_id('26c9a13d-3d71-452e-a91c-9a4589be021c')(it)( + 'fails to fetch geopoints that are specifically not at (0,0)', + async () => { + const tmp = new TestObject({ + location: new Parse.GeoPoint({ latitude: 0, longitude: 0 }), + }); + const tmp2 = new TestObject({ + location: new Parse.GeoPoint({ + latitude: 49.2577142, + longitude: -123.1941149, + }), + }); + await Parse.Object.saveAll([tmp, tmp2]); + const query = new Parse.Query(TestObject); + query.notEqualTo('location', new Parse.GeoPoint({ latitude: 0, longitude: 0 })); + const results = await query.find(); + expect(results.length).toEqual(1); + } + ); }); diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index e6719433ff..1ff6dcfd93 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -16,22 +16,21 @@ describe('a GlobalConfig', () => { return { objectId: '1' }; } ); - await config.database.adapter - .upsertOneObject( - '_GlobalConfig', - { - fields: { - objectId: { type: 'Number' }, - params: { type: 'Object' }, - masterKeyOnly: { type: 'Object' }, - }, + await config.database.adapter.upsertOneObject( + '_GlobalConfig', + { + fields: { + objectId: { type: 'Number' }, + params: { type: 'Object' }, + masterKeyOnly: { type: 'Object' }, }, - query, - { - params: { companies: ['US', 'DK'], counter: 20, internalParam: 'internal' }, - masterKeyOnly: { internalParam: true }, - } - ); + }, + query, + { + params: { companies: ['US', 'DK'], counter: 20, internalParam: 'internal' }, + masterKeyOnly: { internalParam: true }, + } + ); }); const headers = { @@ -111,28 +110,28 @@ describe('a GlobalConfig', () => { }); it_only_db('mongo')('can addUnique', async () => { - await Parse.Config.save({ companies: { __op: 'AddUnique', objects: ['PA', 'RS', 'E'] } }); + await Parse.Config.save({ companies: { __op: 'AddUnique', objects: ['PA', 'RS', 'E'] } }); const config = await Parse.Config.get(); const companies = config.get('companies'); expect(companies).toEqual(['US', 'DK', 'PA', 'RS', 'E']); }); it_only_db('mongo')('can add to array', async () => { - await Parse.Config.save({ companies: { __op: 'Add', objects: ['PA'] } }); + await Parse.Config.save({ companies: { __op: 'Add', objects: ['PA'] } }); const config = await Parse.Config.get(); const companies = config.get('companies'); expect(companies).toEqual(['US', 'DK', 'PA']); }); it_only_db('mongo')('can remove from array', async () => { - await Parse.Config.save({ companies: { __op: 'Remove', objects: ['US'] } }); + await Parse.Config.save({ companies: { __op: 'Remove', objects: ['US'] } }); const config = await Parse.Config.get(); const companies = config.get('companies'); expect(companies).toEqual(['DK']); }); it('can increment', async () => { - await Parse.Config.save({ counter: { __op: 'Increment', amount: 49 } }); + await Parse.Config.save({ counter: { __op: 'Increment', amount: 49 } }); const config = await Parse.Config.get(); const counter = config.get('counter'); expect(counter).toEqual(69); diff --git a/spec/ParseGraphQLSchema.spec.js b/spec/ParseGraphQLSchema.spec.js index 0b3d9a9007..199c52756c 100644 --- a/spec/ParseGraphQLSchema.spec.js +++ b/spec/ParseGraphQLSchema.spec.js @@ -500,77 +500,83 @@ describe('ParseGraphQLSchema', () => { }); }); describe('alias', () => { - it_id('45282d26-f4c7-4d2d-a7b6-cd8741d5322f')(it)('Should be able to define alias for get and find query', async () => { - const parseGraphQLSchema = new ParseGraphQLSchema({ - databaseController, - parseGraphQLController, - log: defaultLogger, - appId, - }); - - await parseGraphQLSchema.parseGraphQLController.updateGraphQLConfig({ - classConfigs: [ - { - className: 'Data', - query: { - get: true, - getAlias: 'precious_data', - find: true, - findAlias: 'data_results', + it_id('45282d26-f4c7-4d2d-a7b6-cd8741d5322f')(it)( + 'Should be able to define alias for get and find query', + async () => { + const parseGraphQLSchema = new ParseGraphQLSchema({ + databaseController, + parseGraphQLController, + log: defaultLogger, + appId, + }); + + await parseGraphQLSchema.parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: 'Data', + query: { + get: true, + getAlias: 'precious_data', + find: true, + findAlias: 'data_results', + }, }, - }, - ], - }); + ], + }); - const data = new Parse.Object('Data'); + const data = new Parse.Object('Data'); - await data.save(); + await data.save(); - await parseGraphQLSchema.schemaCache.clear(); - await parseGraphQLSchema.load(); + await parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLSchema.load(); - const queries1 = parseGraphQLSchema.graphQLQueries; + const queries1 = parseGraphQLSchema.graphQLQueries; - expect(Object.keys(queries1)).toContain('data_results'); - expect(Object.keys(queries1)).toContain('precious_data'); - }); + expect(Object.keys(queries1)).toContain('data_results'); + expect(Object.keys(queries1)).toContain('precious_data'); + } + ); - it_id('f04b46e3-a25d-401d-a315-3298cfee1df8')(it)('Should be able to define alias for mutation', async () => { - const parseGraphQLSchema = new ParseGraphQLSchema({ - databaseController, - parseGraphQLController, - log: defaultLogger, - appId, - }); + it_id('f04b46e3-a25d-401d-a315-3298cfee1df8')(it)( + 'Should be able to define alias for mutation', + async () => { + const parseGraphQLSchema = new ParseGraphQLSchema({ + databaseController, + parseGraphQLController, + log: defaultLogger, + appId, + }); - await parseGraphQLSchema.parseGraphQLController.updateGraphQLConfig({ - classConfigs: [ - { - className: 'Track', - mutation: { - create: true, - createAlias: 'addTrack', - update: true, - updateAlias: 'modifyTrack', - destroy: true, - destroyAlias: 'eraseTrack', + await parseGraphQLSchema.parseGraphQLController.updateGraphQLConfig({ + classConfigs: [ + { + className: 'Track', + mutation: { + create: true, + createAlias: 'addTrack', + update: true, + updateAlias: 'modifyTrack', + destroy: true, + destroyAlias: 'eraseTrack', + }, }, - }, - ], - }); + ], + }); - const data = new Parse.Object('Track'); + const data = new Parse.Object('Track'); - await data.save(); + await data.save(); - await parseGraphQLSchema.schemaCache.clear(); - await parseGraphQLSchema.load(); + await parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLSchema.load(); - const mutations = parseGraphQLSchema.graphQLMutations; + const mutations = parseGraphQLSchema.graphQLMutations; - expect(Object.keys(mutations)).toContain('addTrack'); - expect(Object.keys(mutations)).toContain('modifyTrack'); - expect(Object.keys(mutations)).toContain('eraseTrack'); - }); + expect(Object.keys(mutations)).toContain('addTrack'); + expect(Object.keys(mutations)).toContain('modifyTrack'); + expect(Object.keys(mutations)).toContain('eraseTrack'); + } + ); }); }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index e1353d8db2..953919c693 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -9,7 +9,8 @@ const { updateCLP } = require('./support/dev'); const pluralize = require('pluralize'); const { getMainDefinition } = require('@apollo/client/utilities'); -const createUploadLink = (...args) => import('apollo-upload-client/createUploadLink.mjs').then(({ default: fn }) => fn(...args)); +const createUploadLink = (...args) => + import('apollo-upload-client/createUploadLink.mjs').then(({ default: fn }) => fn(...args)); const { SubscriptionClient } = require('subscriptions-transport-ws'); const { WebSocketLink } = require('@apollo/client/link/ws'); const { mergeSchemas } = require('@graphql-tools/schema'); @@ -130,14 +131,17 @@ describe('ParseGraphQLServer', () => { set: () => {}, }; - it_id('0696675e-060f-414f-bc77-9d57f31807f5')(it)('should return schema and context with req\'s info, config and auth', async () => { - const options = await parseGraphQLServer._getGraphQLOptions(); - expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema); - const contextResponse = await options.context({ req, res }); - expect(contextResponse.info).toEqual(req.info); - expect(contextResponse.config).toEqual(req.config); - expect(contextResponse.auth).toEqual(req.auth); - }); + it_id('0696675e-060f-414f-bc77-9d57f31807f5')(it)( + "should return schema and context with req's info, config and auth", + async () => { + const options = await parseGraphQLServer._getGraphQLOptions(); + expect(options.schema).toEqual(parseGraphQLServer.parseGraphQLSchema.graphQLSchema); + const contextResponse = await options.context({ req, res }); + expect(contextResponse.info).toEqual(req.info); + expect(contextResponse.config).toEqual(req.config); + expect(contextResponse.auth).toEqual(req.auth); + } + ); it('should load GraphQL schema in every call', async () => { const originalLoad = parseGraphQLServer.parseGraphQLSchema.load; @@ -1395,618 +1399,518 @@ describe('ParseGraphQLServer', () => { await resetGraphQLCache(); }); - it_id('d6a23a2f-ca18-4b15-bc73-3e636f99e6bc')(it)('should only include types in the enabledForClasses list', async () => { - const schemaController = await parseServer.config.databaseController.loadSchema(); - await schemaController.addClassIfNotExists('SuperCar', { - foo: { type: 'String' }, - }); + it_id('d6a23a2f-ca18-4b15-bc73-3e636f99e6bc')(it)( + 'should only include types in the enabledForClasses list', + async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SuperCar', { + foo: { type: 'String' }, + }); - const graphQLConfig = { - enabledForClasses: ['SuperCar'], - }; - await parseGraphQLServer.setGraphQLConfig(graphQLConfig); - await resetGraphQLCache(); + const graphQLConfig = { + enabledForClasses: ['SuperCar'], + }; + await parseGraphQLServer.setGraphQLConfig(graphQLConfig); + await resetGraphQLCache(); - const { data } = await apolloClient.query({ - query: gql` - query UserType { - userType: __type(name: "User") { - fields { - name + const { data } = await apolloClient.query({ + query: gql` + query UserType { + userType: __type(name: "User") { + fields { + name + } } - } - superCarType: __type(name: "SuperCar") { - fields { - name + superCarType: __type(name: "SuperCar") { + fields { + name + } } } - } - `, - }); - expect(data.userType).toBeNull(); - expect(data.superCarType).toBeTruthy(); - }); - it_id('1db2aceb-d24e-4929-ba43-8dbb5d0395e1')(it)('should not include types in the disabledForClasses list', async () => { - const schemaController = await parseServer.config.databaseController.loadSchema(); - await schemaController.addClassIfNotExists('SuperCar', { - foo: { type: 'String' }, - }); + `, + }); + expect(data.userType).toBeNull(); + expect(data.superCarType).toBeTruthy(); + } + ); + it_id('1db2aceb-d24e-4929-ba43-8dbb5d0395e1')(it)( + 'should not include types in the disabledForClasses list', + async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SuperCar', { + foo: { type: 'String' }, + }); - const graphQLConfig = { - disabledForClasses: ['SuperCar'], - }; - await parseGraphQLServer.setGraphQLConfig(graphQLConfig); - await resetGraphQLCache(); + const graphQLConfig = { + disabledForClasses: ['SuperCar'], + }; + await parseGraphQLServer.setGraphQLConfig(graphQLConfig); + await resetGraphQLCache(); - const { data } = await apolloClient.query({ - query: gql` - query UserType { - userType: __type(name: "User") { - fields { - name + const { data } = await apolloClient.query({ + query: gql` + query UserType { + userType: __type(name: "User") { + fields { + name + } } - } - superCarType: __type(name: "SuperCar") { - fields { - name + superCarType: __type(name: "SuperCar") { + fields { + name + } } } - } - `, - }); - expect(data.superCarType).toBeNull(); - expect(data.userType).toBeTruthy(); - }); - it_id('85c2e02f-0239-4819-b66e-392e0125f6c5')(it)('should remove query operations when disabled', async () => { - const superCar = new Parse.Object('SuperCar'); - await superCar.save({ foo: 'bar' }); - const customer = new Parse.Object('Customer'); - await customer.save({ foo: 'bar' }); + `, + }); + expect(data.superCarType).toBeNull(); + expect(data.userType).toBeTruthy(); + } + ); + it_id('85c2e02f-0239-4819-b66e-392e0125f6c5')(it)( + 'should remove query operations when disabled', + async () => { + const superCar = new Parse.Object('SuperCar'); + await superCar.save({ foo: 'bar' }); + const customer = new Parse.Object('Customer'); + await customer.save({ foo: 'bar' }); - await expectAsync( - apolloClient.query({ - query: gql` - query GetSuperCar($id: ID!) { - superCar(id: $id) { - id + await expectAsync( + apolloClient.query({ + query: gql` + query GetSuperCar($id: ID!) { + superCar(id: $id) { + id + } } - } - `, - variables: { - id: superCar.id, - }, - }) - ).toBeResolved(); + `, + variables: { + id: superCar.id, + }, + }) + ).toBeResolved(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindCustomer { - customers { - count + await expectAsync( + apolloClient.query({ + query: gql` + query FindCustomer { + customers { + count + } } - } - `, - }) - ).toBeResolved(); + `, + }) + ).toBeResolved(); - const graphQLConfig = { - classConfigs: [ - { - className: 'SuperCar', - query: { - get: false, - find: true, + const graphQLConfig = { + classConfigs: [ + { + className: 'SuperCar', + query: { + get: false, + find: true, + }, }, - }, - { - className: 'Customer', - query: { - get: true, - find: false, + { + className: 'Customer', + query: { + get: true, + find: false, + }, }, - }, - ], - }; - await parseGraphQLServer.setGraphQLConfig(graphQLConfig); - await resetGraphQLCache(); + ], + }; + await parseGraphQLServer.setGraphQLConfig(graphQLConfig); + await resetGraphQLCache(); - await expectAsync( - apolloClient.query({ - query: gql` - query GetSuperCar($id: ID!) { - superCar(id: $id) { - id + await expectAsync( + apolloClient.query({ + query: gql` + query GetSuperCar($id: ID!) { + superCar(id: $id) { + id + } } - } - `, - variables: { - id: superCar.id, - }, - }) - ).toBeRejected(); - await expectAsync( - apolloClient.query({ - query: gql` - query GetCustomer($id: ID!) { - customer(id: $id) { - id + `, + variables: { + id: superCar.id, + }, + }) + ).toBeRejected(); + await expectAsync( + apolloClient.query({ + query: gql` + query GetCustomer($id: ID!) { + customer(id: $id) { + id + } } - } - `, - variables: { - id: customer.id, - }, - }) - ).toBeResolved(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindSuperCar { - superCars { - count + `, + variables: { + id: customer.id, + }, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars { + count + } } - } - `, - }) - ).toBeResolved(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindCustomer { - customers { - count + `, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindCustomer { + customers { + count + } } - } - `, - }) - ).toBeRejected(); - }); + `, + }) + ).toBeRejected(); + } + ); - it_id('972161a6-8108-4e99-a1a5-71d0267d26c2')(it)('should remove mutation operations, create, update and delete, when disabled', async () => { - const superCar1 = new Parse.Object('SuperCar'); - await superCar1.save({ foo: 'bar' }); - const customer1 = new Parse.Object('Customer'); - await customer1.save({ foo: 'bar' }); + it_id('972161a6-8108-4e99-a1a5-71d0267d26c2')(it)( + 'should remove mutation operations, create, update and delete, when disabled', + async () => { + const superCar1 = new Parse.Object('SuperCar'); + await superCar1.save({ foo: 'bar' }); + const customer1 = new Parse.Object('Customer'); + await customer1.save({ foo: 'bar' }); - await expectAsync( - apolloClient.query({ + await expectAsync( + apolloClient.query({ + query: gql` + mutation UpdateSuperCar($id: ID!, $foo: String!) { + updateSuperCar(input: { id: $id, fields: { foo: $foo } }) { + clientMutationId + } + } + `, + variables: { + id: superCar1.id, + foo: 'lah', + }, + }) + ).toBeResolved(); + + await expectAsync( + apolloClient.query({ + query: gql` + mutation DeleteCustomer($id: ID!) { + deleteCustomer(input: { id: $id }) { + clientMutationId + } + } + `, + variables: { + id: customer1.id, + }, + }) + ).toBeResolved(); + + const { data: customerData } = await apolloClient.query({ query: gql` - mutation UpdateSuperCar($id: ID!, $foo: String!) { - updateSuperCar(input: { id: $id, fields: { foo: $foo } }) { - clientMutationId + mutation CreateCustomer($foo: String!) { + createCustomer(input: { fields: { foo: $foo } }) { + customer { + id + } } } `, variables: { - id: superCar1.id, - foo: 'lah', + foo: 'rah', }, - }) - ).toBeResolved(); + }); + expect(customerData.createCustomer.customer).toBeTruthy(); - await expectAsync( - apolloClient.query({ + // used later + const customer2Id = customerData.createCustomer.customer.id; + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + mutation: { + create: true, + update: false, + destroy: true, + }, + }, + { + className: 'Customer', + mutation: { + create: false, + update: true, + destroy: false, + }, + }, + ], + }); + await resetGraphQLCache(); + + const { data: superCarData } = await apolloClient.query({ query: gql` - mutation DeleteCustomer($id: ID!) { - deleteCustomer(input: { id: $id }) { - clientMutationId + mutation CreateSuperCar($foo: String!) { + createSuperCar(input: { fields: { foo: $foo } }) { + superCar { + id + } } } `, variables: { - id: customer1.id, + foo: 'mah', }, - }) - ).toBeResolved(); + }); + expect(superCarData.createSuperCar).toBeTruthy(); + const superCar3Id = superCarData.createSuperCar.superCar.id; - const { data: customerData } = await apolloClient.query({ - query: gql` - mutation CreateCustomer($foo: String!) { - createCustomer(input: { fields: { foo: $foo } }) { - customer { - id + await expectAsync( + apolloClient.query({ + query: gql` + mutation UpdateSupercar($id: ID!, $foo: String!) { + updateSuperCar(input: { id: $id, fields: { foo: $foo } }) { + clientMutationId + } } - } - } - `, - variables: { - foo: 'rah', - }, - }); - expect(customerData.createCustomer.customer).toBeTruthy(); - - // used later - const customer2Id = customerData.createCustomer.customer.id; - - await parseGraphQLServer.setGraphQLConfig({ - classConfigs: [ - { - className: 'SuperCar', - mutation: { - create: true, - update: false, - destroy: true, - }, - }, - { - className: 'Customer', - mutation: { - create: false, - update: true, - destroy: false, + `, + variables: { + id: superCar3Id, }, - }, - ], - }); - await resetGraphQLCache(); - - const { data: superCarData } = await apolloClient.query({ - query: gql` - mutation CreateSuperCar($foo: String!) { - createSuperCar(input: { fields: { foo: $foo } }) { - superCar { - id - } - } - } - `, - variables: { - foo: 'mah', - }, - }); - expect(superCarData.createSuperCar).toBeTruthy(); - const superCar3Id = superCarData.createSuperCar.superCar.id; - - await expectAsync( - apolloClient.query({ - query: gql` - mutation UpdateSupercar($id: ID!, $foo: String!) { - updateSuperCar(input: { id: $id, fields: { foo: $foo } }) { - clientMutationId - } - } - `, - variables: { - id: superCar3Id, - }, - }) - ).toBeRejected(); + }) + ).toBeRejected(); - await expectAsync( - apolloClient.query({ - query: gql` - mutation DeleteSuperCar($id: ID!) { - deleteSuperCar(input: { id: $id }) { - clientMutationId + await expectAsync( + apolloClient.query({ + query: gql` + mutation DeleteSuperCar($id: ID!) { + deleteSuperCar(input: { id: $id }) { + clientMutationId + } } - } - `, - variables: { - id: superCar3Id, - }, - }) - ).toBeResolved(); + `, + variables: { + id: superCar3Id, + }, + }) + ).toBeResolved(); - await expectAsync( - apolloClient.query({ - query: gql` - mutation CreateCustomer($foo: String!) { - createCustomer(input: { fields: { foo: $foo } }) { - customer { - id + await expectAsync( + apolloClient.query({ + query: gql` + mutation CreateCustomer($foo: String!) { + createCustomer(input: { fields: { foo: $foo } }) { + customer { + id + } } } - } - `, - variables: { - foo: 'rah', - }, - }) - ).toBeRejected(); - await expectAsync( - apolloClient.query({ - query: gql` - mutation UpdateCustomer($id: ID!, $foo: String!) { - updateCustomer(input: { id: $id, fields: { foo: $foo } }) { - clientMutationId + `, + variables: { + foo: 'rah', + }, + }) + ).toBeRejected(); + await expectAsync( + apolloClient.query({ + query: gql` + mutation UpdateCustomer($id: ID!, $foo: String!) { + updateCustomer(input: { id: $id, fields: { foo: $foo } }) { + clientMutationId + } } - } - `, - variables: { - id: customer2Id, - foo: 'tah', - }, - }) - ).toBeResolved(); - await expectAsync( - apolloClient.query({ - query: gql` - mutation DeleteCustomer($id: ID!, $foo: String!) { - deleteCustomer(input: { id: $id }) { - clientMutationId + `, + variables: { + id: customer2Id, + foo: 'tah', + }, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + mutation DeleteCustomer($id: ID!, $foo: String!) { + deleteCustomer(input: { id: $id }) { + clientMutationId + } } - } - `, - variables: { - id: customer2Id, - }, - }) - ).toBeRejected(); - }); + `, + variables: { + id: customer2Id, + }, + }) + ).toBeRejected(); + } + ); - it_id('4af763b1-ff86-43c7-ba30-060a1c07e730')(it)('should only allow the supplied create and update fields for a class', async () => { - const schemaController = await parseServer.config.databaseController.loadSchema(); - await schemaController.addClassIfNotExists('SuperCar', { - engine: { type: 'String' }, - doors: { type: 'Number' }, - price: { type: 'String' }, - mileage: { type: 'Number' }, - }); + it_id('4af763b1-ff86-43c7-ba30-060a1c07e730')(it)( + 'should only allow the supplied create and update fields for a class', + async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SuperCar', { + engine: { type: 'String' }, + doors: { type: 'Number' }, + price: { type: 'String' }, + mileage: { type: 'Number' }, + }); - await parseGraphQLServer.setGraphQLConfig({ - classConfigs: [ - { - className: 'SuperCar', - type: { - inputFields: { - create: ['engine', 'doors', 'price'], - update: ['price', 'mileage'], + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + inputFields: { + create: ['engine', 'doors', 'price'], + update: ['price', 'mileage'], + }, }, }, - }, - ], - }); + ], + }); - await resetGraphQLCache(); + await resetGraphQLCache(); - await expectAsync( - apolloClient.query({ - query: gql` - mutation InvalidCreateSuperCar { - createSuperCar(input: { fields: { engine: "diesel", mileage: 1000 } }) { - superCar { - id + await expectAsync( + apolloClient.query({ + query: gql` + mutation InvalidCreateSuperCar { + createSuperCar(input: { fields: { engine: "diesel", mileage: 1000 } }) { + superCar { + id + } } } - } - `, - }) - ).toBeRejected(); - const { id: superCarId } = ( - await apolloClient.query({ - query: gql` - mutation ValidCreateSuperCar { - createSuperCar( - input: { fields: { engine: "diesel", doors: 5, price: "£10000" } } - ) { - superCar { - id + `, + }) + ).toBeRejected(); + const { id: superCarId } = ( + await apolloClient.query({ + query: gql` + mutation ValidCreateSuperCar { + createSuperCar( + input: { fields: { engine: "diesel", doors: 5, price: "£10000" } } + ) { + superCar { + id + } } } - } - `, - }) - ).data.createSuperCar.superCar; - - expect(superCarId).toBeTruthy(); - - await expectAsync( - apolloClient.query({ - query: gql` - mutation InvalidUpdateSuperCar($id: ID!) { - updateSuperCar(input: { id: $id, fields: { engine: "petrol" } }) { - clientMutationId - } - } - `, - variables: { - id: superCarId, - }, - }) - ).toBeRejected(); - - const updatedSuperCar = ( - await apolloClient.query({ - query: gql` - mutation ValidUpdateSuperCar($id: ID!) { - updateSuperCar(input: { id: $id, fields: { mileage: 2000 } }) { - clientMutationId - } - } - `, - variables: { - id: superCarId, - }, - }) - ).data.updateSuperCar; - expect(updatedSuperCar).toBeTruthy(); - }); - - it_id('fc9237e9-3e63-4b55-9c1d-e6269f613a93')(it)('should handle required fields from the Parse class', async () => { - const schemaController = await parseServer.config.databaseController.loadSchema(); - await schemaController.addClassIfNotExists('SuperCar', { - engine: { type: 'String', required: true }, - doors: { type: 'Number', required: true }, - price: { type: 'String' }, - mileage: { type: 'Number' }, - }); + `, + }) + ).data.createSuperCar.superCar; - await resetGraphQLCache(); + expect(superCarId).toBeTruthy(); - const { - data: { __type }, - } = await apolloClient.query({ - query: gql` - query requiredFields { - __type(name: "CreateSuperCarFieldsInput") { - inputFields { - name - type { - kind + await expectAsync( + apolloClient.query({ + query: gql` + mutation InvalidUpdateSuperCar($id: ID!) { + updateSuperCar(input: { id: $id, fields: { engine: "petrol" } }) { + clientMutationId } } - } - } - `, - }); - expect(__type.inputFields.find(o => o.name === 'price').type.kind).toEqual('SCALAR'); - expect(__type.inputFields.find(o => o.name === 'engine').type.kind).toEqual('NON_NULL'); - expect(__type.inputFields.find(o => o.name === 'doors').type.kind).toEqual('NON_NULL'); + `, + variables: { + id: superCarId, + }, + }) + ).toBeRejected(); - const { - data: { __type: __type2 }, - } = await apolloClient.query({ - query: gql` - query requiredFields { - __type(name: "SuperCar") { - fields { - name - type { - kind + const updatedSuperCar = ( + await apolloClient.query({ + query: gql` + mutation ValidUpdateSuperCar($id: ID!) { + updateSuperCar(input: { id: $id, fields: { mileage: 2000 } }) { + clientMutationId } } - } - } - `, - }); - expect(__type2.fields.find(o => o.name === 'price').type.kind).toEqual('SCALAR'); - expect(__type2.fields.find(o => o.name === 'engine').type.kind).toEqual('NON_NULL'); - expect(__type2.fields.find(o => o.name === 'doors').type.kind).toEqual('NON_NULL'); - }); - - it_id('83b6895a-7dfd-4e3b-a5ce-acdb1fa39705')(it)('should only allow the supplied output fields for a class', async () => { - const schemaController = await parseServer.config.databaseController.loadSchema(); - - await schemaController.addClassIfNotExists('SuperCar', { - engine: { type: 'String' }, - doors: { type: 'Number' }, - price: { type: 'String' }, - mileage: { type: 'Number' }, - insuranceClaims: { type: 'Number' }, - }); - - const superCar = await new Parse.Object('SuperCar').save({ - engine: 'petrol', - doors: 3, - price: '£7500', - mileage: 0, - insuranceCertificate: 'private-file.pdf', - }); - - await parseGraphQLServer.setGraphQLConfig({ - classConfigs: [ - { - className: 'SuperCar', - type: { - outputFields: ['engine', 'doors', 'price', 'mileage'], + `, + variables: { + id: superCarId, }, - }, - ], - }); - - await resetGraphQLCache(); + }) + ).data.updateSuperCar; + expect(updatedSuperCar).toBeTruthy(); + } + ); - await expectAsync( - apolloClient.query({ - query: gql` - query GetSuperCar($id: ID!) { - superCar(id: $id) { - id - objectId - engine - doors - price - mileage - insuranceCertificate - } - } - `, - variables: { - id: superCar.id, - }, - }) - ).toBeRejected(); - let getSuperCar = ( - await apolloClient.query({ - query: gql` - query GetSuperCar($id: ID!) { - superCar(id: $id) { - id - objectId - engine - doors - price - mileage - } - } - `, - variables: { - id: superCar.id, - }, - }) - ).data.superCar; - expect(getSuperCar).toBeTruthy(); + it_id('fc9237e9-3e63-4b55-9c1d-e6269f613a93')(it)( + 'should handle required fields from the Parse class', + async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + await schemaController.addClassIfNotExists('SuperCar', { + engine: { type: 'String', required: true }, + doors: { type: 'Number', required: true }, + price: { type: 'String' }, + mileage: { type: 'Number' }, + }); - await parseGraphQLServer.setGraphQLConfig({ - classConfigs: [ - { - className: 'SuperCar', - type: { - outputFields: [], - }, - }, - ], - }); + await resetGraphQLCache(); - await resetGraphQLCache(); - await expectAsync( - apolloClient.query({ + const { + data: { __type }, + } = await apolloClient.query({ query: gql` - query GetSuperCar($id: ID!) { - superCar(id: $id) { - engine + query requiredFields { + __type(name: "CreateSuperCarFieldsInput") { + inputFields { + name + type { + kind + } + } } } `, - variables: { - id: superCar.id, - }, - }) - ).toBeRejected(); - getSuperCar = ( - await apolloClient.query({ + }); + expect(__type.inputFields.find(o => o.name === 'price').type.kind).toEqual('SCALAR'); + expect(__type.inputFields.find(o => o.name === 'engine').type.kind).toEqual('NON_NULL'); + expect(__type.inputFields.find(o => o.name === 'doors').type.kind).toEqual('NON_NULL'); + + const { + data: { __type: __type2 }, + } = await apolloClient.query({ query: gql` - query GetSuperCar($id: ID!) { - superCar(id: $id) { - id - objectId + query requiredFields { + __type(name: "SuperCar") { + fields { + name + type { + kind + } + } } } `, - variables: { - id: superCar.id, - }, - }) - ).data.superCar; - expect(getSuperCar.objectId).toBe(superCar.id); - }); + }); + expect(__type2.fields.find(o => o.name === 'price').type.kind).toEqual('SCALAR'); + expect(__type2.fields.find(o => o.name === 'engine').type.kind).toEqual('NON_NULL'); + expect(__type2.fields.find(o => o.name === 'doors').type.kind).toEqual('NON_NULL'); + } + ); - it_id('67dfcf94-92fb-45a3-a012-3b22c81899ba')(it)('should only allow the supplied constraint fields for a class', async () => { - try { + it_id('83b6895a-7dfd-4e3b-a5ce-acdb1fa39705')(it)( + 'should only allow the supplied output fields for a class', + async () => { const schemaController = await parseServer.config.databaseController.loadSchema(); await schemaController.addClassIfNotExists('SuperCar', { - model: { type: 'String' }, engine: { type: 'String' }, doors: { type: 'Number' }, price: { type: 'String' }, mileage: { type: 'Number' }, - insuranceCertificate: { type: 'String' }, + insuranceClaims: { type: 'Number' }, }); - await new Parse.Object('SuperCar').save({ - model: 'McLaren', + const superCar = await new Parse.Object('SuperCar').save({ engine: 'petrol', doors: 3, price: '£7500', @@ -2019,7 +1923,7 @@ describe('ParseGraphQLServer', () => { { className: 'SuperCar', type: { - constraintFields: ['engine', 'doors', 'price'], + outputFields: ['engine', 'doors', 'price', 'mileage'], }, }, ], @@ -2030,196 +1934,323 @@ describe('ParseGraphQLServer', () => { await expectAsync( apolloClient.query({ query: gql` - query FindSuperCar { - superCars(where: { insuranceCertificate: { equalTo: "private-file.pdf" } }) { - count + query GetSuperCar($id: ID!) { + superCar(id: $id) { + id + objectId + engine + doors + price + mileage + insuranceCertificate } } `, + variables: { + id: superCar.id, + }, }) ).toBeRejected(); + let getSuperCar = ( + await apolloClient.query({ + query: gql` + query GetSuperCar($id: ID!) { + superCar(id: $id) { + id + objectId + engine + doors + price + mileage + } + } + `, + variables: { + id: superCar.id, + }, + }) + ).data.superCar; + expect(getSuperCar).toBeTruthy(); + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + outputFields: [], + }, + }, + ], + }); + await resetGraphQLCache(); await expectAsync( apolloClient.query({ query: gql` - query FindSuperCar { - superCars(where: { mileage: { equalTo: 0 } }) { - count + query GetSuperCar($id: ID!) { + superCar(id: $id) { + engine } } `, + variables: { + id: superCar.id, + }, }) ).toBeRejected(); - - await expectAsync( - apolloClient.query({ + getSuperCar = ( + await apolloClient.query({ query: gql` - query FindSuperCar { - superCars(where: { engine: { equalTo: "petrol" } }) { - count + query GetSuperCar($id: ID!) { + superCar(id: $id) { + id + objectId } } `, + variables: { + id: superCar.id, + }, }) - ).toBeResolved(); - } catch (e) { - handleError(e); + ).data.superCar; + expect(getSuperCar.objectId).toBe(superCar.id); } - }); - - it_id('a3bdbd5d-8779-42fe-91a1-7a7f90a6177b')(it)('should only allow the supplied sort fields for a class', async () => { - const schemaController = await parseServer.config.databaseController.loadSchema(); + ); - await schemaController.addClassIfNotExists('SuperCar', { - engine: { type: 'String' }, - doors: { type: 'Number' }, - price: { type: 'String' }, - mileage: { type: 'Number' }, - }); + it_id('67dfcf94-92fb-45a3-a012-3b22c81899ba')(it)( + 'should only allow the supplied constraint fields for a class', + async () => { + try { + const schemaController = await parseServer.config.databaseController.loadSchema(); + + await schemaController.addClassIfNotExists('SuperCar', { + model: { type: 'String' }, + engine: { type: 'String' }, + doors: { type: 'Number' }, + price: { type: 'String' }, + mileage: { type: 'Number' }, + insuranceCertificate: { type: 'String' }, + }); - await new Parse.Object('SuperCar').save({ - engine: 'petrol', - doors: 3, - price: '£7500', - mileage: 0, - }); + await new Parse.Object('SuperCar').save({ + model: 'McLaren', + engine: 'petrol', + doors: 3, + price: '£7500', + mileage: 0, + insuranceCertificate: 'private-file.pdf', + }); - await parseGraphQLServer.setGraphQLConfig({ - classConfigs: [ - { - className: 'SuperCar', - type: { - sortFields: [ - { - field: 'doors', - asc: true, - desc: true, - }, - { - field: 'price', - asc: true, - desc: true, - }, - { - field: 'mileage', - asc: true, - desc: false, + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + constraintFields: ['engine', 'doors', 'price'], }, - ], - }, - }, - ], - }); + }, + ], + }); - await resetGraphQLCache(); + await resetGraphQLCache(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindSuperCar { - superCars(order: [engine_ASC]) { - edges { - node { - id + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(where: { insuranceCertificate: { equalTo: "private-file.pdf" } }) { + count } } - } - } - `, - }) - ).toBeRejected(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindSuperCar { - superCars(order: [engine_DESC]) { - edges { - node { - id + `, + }) + ).toBeRejected(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(where: { mileage: { equalTo: 0 } }) { + count + } + } + `, + }) + ).toBeRejected(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(where: { engine: { equalTo: "petrol" } }) { + count + } + } + `, + }) + ).toBeResolved(); + } catch (e) { + handleError(e); + } + } + ); + + it_id('a3bdbd5d-8779-42fe-91a1-7a7f90a6177b')(it)( + 'should only allow the supplied sort fields for a class', + async () => { + const schemaController = await parseServer.config.databaseController.loadSchema(); + + await schemaController.addClassIfNotExists('SuperCar', { + engine: { type: 'String' }, + doors: { type: 'Number' }, + price: { type: 'String' }, + mileage: { type: 'Number' }, + }); + + await new Parse.Object('SuperCar').save({ + engine: 'petrol', + doors: 3, + price: '£7500', + mileage: 0, + }); + + await parseGraphQLServer.setGraphQLConfig({ + classConfigs: [ + { + className: 'SuperCar', + type: { + sortFields: [ + { + field: 'doors', + asc: true, + desc: true, + }, + { + field: 'price', + asc: true, + desc: true, + }, + { + field: 'mileage', + asc: true, + desc: false, + }, + ], + }, + }, + ], + }); + + await resetGraphQLCache(); + + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(order: [engine_ASC]) { + edges { + node { + id + } } } } - } - `, - }) - ).toBeRejected(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindSuperCar { - superCars(order: [mileage_DESC]) { - edges { - node { - id + `, + }) + ).toBeRejected(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(order: [engine_DESC]) { + edges { + node { + id + } } } } - } - `, - }) - ).toBeRejected(); + `, + }) + ).toBeRejected(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(order: [mileage_DESC]) { + edges { + node { + id + } + } + } + } + `, + }) + ).toBeRejected(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindSuperCar { - superCars(order: [mileage_ASC]) { - edges { - node { - id + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(order: [mileage_ASC]) { + edges { + node { + id + } } } } - } - `, - }) - ).toBeResolved(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindSuperCar { - superCars(order: [doors_ASC]) { - edges { - node { - id + `, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(order: [doors_ASC]) { + edges { + node { + id + } } } } - } - `, - }) - ).toBeResolved(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindSuperCar { - superCars(order: [price_DESC]) { - edges { - node { - id + `, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(order: [price_DESC]) { + edges { + node { + id + } } } } - } - `, - }) - ).toBeResolved(); - await expectAsync( - apolloClient.query({ - query: gql` - query FindSuperCar { - superCars(order: [price_ASC, doors_DESC]) { - edges { - node { - id + `, + }) + ).toBeResolved(); + await expectAsync( + apolloClient.query({ + query: gql` + query FindSuperCar { + superCars(order: [price_ASC, doors_DESC]) { + edges { + node { + id + } } } } - } - `, - }) - ).toBeResolved(); - }); + `, + }) + ).toBeResolved(); + } + ); }); describe('Relay Spec', () => { @@ -3488,12 +3519,10 @@ describe('ParseGraphQLServer', () => { }, }, }); - result.data.createClass.class.schemaFields = result.data.createClass.class.schemaFields.sort( - (a, b) => (a.name > b.name ? 1 : -1) - ); - result.data.updateClass.class.schemaFields = result.data.updateClass.class.schemaFields.sort( - (a, b) => (a.name > b.name ? 1 : -1) - ); + result.data.createClass.class.schemaFields = + result.data.createClass.class.schemaFields.sort((a, b) => (a.name > b.name ? 1 : -1)); + result.data.updateClass.class.schemaFields = + result.data.updateClass.class.schemaFields.sort((a, b) => (a.name > b.name ? 1 : -1)); expect(result).toEqual({ data: { createClass: { @@ -3824,12 +3853,10 @@ describe('ParseGraphQLServer', () => { }, }, }); - result.data.createClass.class.schemaFields = result.data.createClass.class.schemaFields.sort( - (a, b) => (a.name > b.name ? 1 : -1) - ); - result.data.deleteClass.class.schemaFields = result.data.deleteClass.class.schemaFields.sort( - (a, b) => (a.name > b.name ? 1 : -1) - ); + result.data.createClass.class.schemaFields = + result.data.createClass.class.schemaFields.sort((a, b) => (a.name > b.name ? 1 : -1)); + result.data.deleteClass.class.schemaFields = + result.data.deleteClass.class.schemaFields.sort((a, b) => (a.name > b.name ? 1 : -1)); expect(result).toEqual({ data: { createClass: { @@ -4921,50 +4948,53 @@ describe('ParseGraphQLServer', () => { ).toEqual(['someValue1', 'someValue2']); }); - it_id('accc59be-fd13-46c5-a103-ec63f2ad6670')(it)('should support full text search', async () => { - try { - const obj = new Parse.Object('FullTextSearchTest'); - obj.set('field1', 'Parse GraphQL Server'); - obj.set('field2', 'It rocks!'); - await obj.save(); + it_id('accc59be-fd13-46c5-a103-ec63f2ad6670')(it)( + 'should support full text search', + async () => { + try { + const obj = new Parse.Object('FullTextSearchTest'); + obj.set('field1', 'Parse GraphQL Server'); + obj.set('field2', 'It rocks!'); + await obj.save(); - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const result = await apolloClient.query({ - query: gql` - query FullTextSearchTests($where: FullTextSearchTestWhereInput) { - fullTextSearchTests(where: $where) { - edges { - node { - objectId + const result = await apolloClient.query({ + query: gql` + query FullTextSearchTests($where: FullTextSearchTestWhereInput) { + fullTextSearchTests(where: $where) { + edges { + node { + objectId + } } } } - } - `, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, }, - }, - variables: { - where: { - field1: { - text: { - search: { - term: 'graphql', + variables: { + where: { + field1: { + text: { + search: { + term: 'graphql', + }, }, }, }, }, - }, - }); + }); - expect(result.data.fullTextSearchTests.edges[0].node.objectId).toEqual(obj.id); - } catch (e) { - handleError(e); + expect(result.data.fullTextSearchTests.edges[0].node.objectId).toEqual(obj.id); + } catch (e) { + handleError(e); + } } - }); + ); it('should support in query key', async () => { try { @@ -5032,194 +5062,200 @@ describe('ParseGraphQLServer', () => { } }); - it_id('0fd03d3c-a2c8-4fac-95cc-2391a3032ca2')(it)('should support order, skip and first arguments', async () => { - const promises = []; - for (let i = 0; i < 100; i++) { - const obj = new Parse.Object('SomeClass'); - obj.set('someField', `someValue${i < 10 ? '0' : ''}${i}`); - obj.set('numberField', i % 3); - promises.push(obj.save()); - } - await Promise.all(promises); - - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - - const result = await apolloClient.query({ - query: gql` - query FindSomeObjects( - $where: SomeClassWhereInput - $order: [SomeClassOrder!] - $skip: Int - $first: Int - ) { - find: someClasses(where: $where, order: $order, skip: $skip, first: $first) { - edges { - node { - someField - } - } - } - } - `, - variables: { - where: { - someField: { - matchesRegex: '^someValue', - }, - }, - order: ['numberField_DESC', 'someField_ASC'], - skip: 4, - first: 2, - }, - }); - - expect(result.data.find.edges.map(obj => obj.node.someField)).toEqual([ - 'someValue14', - 'someValue17', - ]); - }); - - it_id('588a70c6-2932-4d3b-a838-a74c59d8cffb')(it)('should support pagination', async () => { - const numberArray = (first, last) => { - const array = []; - for (let i = first; i <= last; i++) { - array.push(i); + it_id('0fd03d3c-a2c8-4fac-95cc-2391a3032ca2')(it)( + 'should support order, skip and first arguments', + async () => { + const promises = []; + for (let i = 0; i < 100; i++) { + const obj = new Parse.Object('SomeClass'); + obj.set('someField', `someValue${i < 10 ? '0' : ''}${i}`); + obj.set('numberField', i % 3); + promises.push(obj.save()); } - return array; - }; - - const promises = []; - for (let i = 0; i < 100; i++) { - const obj = new Parse.Object('SomeClass'); - obj.set('numberField', i); - promises.push(obj.save()); - } - await Promise.all(promises); + await Promise.all(promises); - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const find = async ({ skip, after, first, before, last } = {}) => { - return await apolloClient.query({ + const result = await apolloClient.query({ query: gql` query FindSomeObjects( + $where: SomeClassWhereInput $order: [SomeClassOrder!] $skip: Int - $after: String $first: Int - $before: String - $last: Int ) { - someClasses( - order: $order - skip: $skip - after: $after - first: $first - before: $before - last: $last - ) { + find: someClasses(where: $where, order: $order, skip: $skip, first: $first) { edges { - cursor node { - numberField + someField } } - count - pageInfo { - hasPreviousPage - startCursor - endCursor - hasNextPage - } } } `, variables: { - order: ['numberField_ASC'], - skip, - after, - first, - before, - last, + where: { + someField: { + matchesRegex: '^someValue', + }, + }, + order: ['numberField_DESC', 'someField_ASC'], + skip: 4, + first: 2, }, }); - }; - let result = await find(); - expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( - numberArray(0, 99) - ); - expect(result.data.someClasses.count).toEqual(100); - expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(false); - expect(result.data.someClasses.pageInfo.startCursor).toEqual( - result.data.someClasses.edges[0].cursor - ); - expect(result.data.someClasses.pageInfo.endCursor).toEqual( - result.data.someClasses.edges[99].cursor - ); - expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(false); + expect(result.data.find.edges.map(obj => obj.node.someField)).toEqual([ + 'someValue14', + 'someValue17', + ]); + } + ); - result = await find({ first: 10 }); - expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( - numberArray(0, 9) - ); - expect(result.data.someClasses.count).toEqual(100); - expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(false); - expect(result.data.someClasses.pageInfo.startCursor).toEqual( - result.data.someClasses.edges[0].cursor - ); - expect(result.data.someClasses.pageInfo.endCursor).toEqual( - result.data.someClasses.edges[9].cursor - ); - expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); + it_id('588a70c6-2932-4d3b-a838-a74c59d8cffb')(it)( + 'should support pagination', + async () => { + const numberArray = (first, last) => { + const array = []; + for (let i = first; i <= last; i++) { + array.push(i); + } + return array; + }; + + const promises = []; + for (let i = 0; i < 100; i++) { + const obj = new Parse.Object('SomeClass'); + obj.set('numberField', i); + promises.push(obj.save()); + } + await Promise.all(promises); - result = await find({ - first: 10, - after: result.data.someClasses.pageInfo.endCursor, - }); - expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( - numberArray(10, 19) - ); - expect(result.data.someClasses.count).toEqual(100); - expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(true); - expect(result.data.someClasses.pageInfo.startCursor).toEqual( - result.data.someClasses.edges[0].cursor - ); - expect(result.data.someClasses.pageInfo.endCursor).toEqual( - result.data.someClasses.edges[9].cursor - ); - expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - result = await find({ last: 10 }); - expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( - numberArray(90, 99) - ); - expect(result.data.someClasses.count).toEqual(100); - expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(true); - expect(result.data.someClasses.pageInfo.startCursor).toEqual( - result.data.someClasses.edges[0].cursor - ); - expect(result.data.someClasses.pageInfo.endCursor).toEqual( - result.data.someClasses.edges[9].cursor - ); - expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(false); + const find = async ({ skip, after, first, before, last } = {}) => { + return await apolloClient.query({ + query: gql` + query FindSomeObjects( + $order: [SomeClassOrder!] + $skip: Int + $after: String + $first: Int + $before: String + $last: Int + ) { + someClasses( + order: $order + skip: $skip + after: $after + first: $first + before: $before + last: $last + ) { + edges { + cursor + node { + numberField + } + } + count + pageInfo { + hasPreviousPage + startCursor + endCursor + hasNextPage + } + } + } + `, + variables: { + order: ['numberField_ASC'], + skip, + after, + first, + before, + last, + }, + }); + }; + + let result = await find(); + expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( + numberArray(0, 99) + ); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(false); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[99].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(false); + + result = await find({ first: 10 }); + expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( + numberArray(0, 9) + ); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(false); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[9].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); + + result = await find({ + first: 10, + after: result.data.someClasses.pageInfo.endCursor, + }); + expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( + numberArray(10, 19) + ); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(true); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[9].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); + + result = await find({ last: 10 }); + expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( + numberArray(90, 99) + ); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(true); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[9].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(false); - result = await find({ - last: 10, - before: result.data.someClasses.pageInfo.startCursor, - }); - expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( - numberArray(80, 89) - ); - expect(result.data.someClasses.count).toEqual(100); - expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(true); - expect(result.data.someClasses.pageInfo.startCursor).toEqual( - result.data.someClasses.edges[0].cursor - ); - expect(result.data.someClasses.pageInfo.endCursor).toEqual( - result.data.someClasses.edges[9].cursor - ); - expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); - }); + result = await find({ + last: 10, + before: result.data.someClasses.pageInfo.startCursor, + }); + expect(result.data.someClasses.edges.map(edge => edge.node.numberField)).toEqual( + numberArray(80, 89) + ); + expect(result.data.someClasses.count).toEqual(100); + expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual(true); + expect(result.data.someClasses.pageInfo.startCursor).toEqual( + result.data.someClasses.edges[0].cursor + ); + expect(result.data.someClasses.pageInfo.endCursor).toEqual( + result.data.someClasses.edges[9].cursor + ); + expect(result.data.someClasses.pageInfo.hasNextPage).toEqual(true); + } + ); it_id('4f6a5f20-9642-4cf0-b31d-e739672a9096')(it)('should support count', async () => { await prepareData(); @@ -5324,108 +5360,114 @@ describe('ParseGraphQLServer', () => { expect(result.data.find.count).toEqual(2); }); - it_id('942b57be-ca8a-4a5b-8104-2adef8743b1a')(it)('should respect max limit', async () => { - parseServer = await global.reconfigureServer({ - maxLimit: 10, - }); + it_id('942b57be-ca8a-4a5b-8104-2adef8743b1a')(it)( + 'should respect max limit', + async () => { + parseServer = await global.reconfigureServer({ + maxLimit: 10, + }); - const promises = []; - for (let i = 0; i < 100; i++) { - const obj = new Parse.Object('SomeClass'); - promises.push(obj.save()); - } - await Promise.all(promises); + const promises = []; + for (let i = 0; i < 100; i++) { + const obj = new Parse.Object('SomeClass'); + promises.push(obj.save()); + } + await Promise.all(promises); - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const result = await apolloClient.query({ - query: gql` - query FindSomeObjects($limit: Int) { - find: someClasses(where: { id: { exists: true } }, first: $limit) { - edges { - node { - id + const result = await apolloClient.query({ + query: gql` + query FindSomeObjects($limit: Int) { + find: someClasses(where: { id: { exists: true } }, first: $limit) { + edges { + node { + id + } } + count } - count } - } - `, - variables: { - limit: 50, - }, - context: { - headers: { - 'X-Parse-Master-Key': 'test', + `, + variables: { + limit: 50, }, - }, - }); + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + }, + }); - expect(result.data.find.edges.length).toEqual(10); - expect(result.data.find.count).toEqual(100); - }); + expect(result.data.find.edges.length).toEqual(10); + expect(result.data.find.count).toEqual(100); + } + ); - it_id('952634f0-0ad5-4a08-8da2-187c1bd9ee94')(it)('should support keys argument', async () => { - await prepareData(); + it_id('952634f0-0ad5-4a08-8da2-187c1bd9ee94')(it)( + 'should support keys argument', + async () => { + await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const result1 = await apolloClient.query({ - query: gql` - query FindSomeObject($where: GraphQLClassWhereInput) { - find: graphQLClasses(where: $where) { - edges { - node { - someField + const result1 = await apolloClient.query({ + query: gql` + query FindSomeObject($where: GraphQLClassWhereInput) { + find: graphQLClasses(where: $where) { + edges { + node { + someField + } } } } - } - `, - variables: { - where: { - id: { equalTo: object3.id }, + `, + variables: { + where: { + id: { equalTo: object3.id }, + }, }, - }, - context: { - headers: { - 'X-Parse-Session-Token': user1.getSessionToken(), + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, }, - }, - }); + }); - const result2 = await apolloClient.query({ - query: gql` - query FindSomeObject($where: GraphQLClassWhereInput) { - find: graphQLClasses(where: $where) { - edges { - node { - someField - pointerToUser { - username + const result2 = await apolloClient.query({ + query: gql` + query FindSomeObject($where: GraphQLClassWhereInput) { + find: graphQLClasses(where: $where) { + edges { + node { + someField + pointerToUser { + username + } } } } } - } - `, - variables: { - where: { - id: { equalTo: object3.id }, + `, + variables: { + where: { + id: { equalTo: object3.id }, + }, }, - }, - context: { - headers: { - 'X-Parse-Session-Token': user1.getSessionToken(), + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, }, - }, - }); + }); - expect(result1.data.find.edges[0].node.someField).toBeDefined(); - expect(result1.data.find.edges[0].node.pointerToUser).toBeUndefined(); - expect(result2.data.find.edges[0].node.someField).toBeDefined(); - expect(result2.data.find.edges[0].node.pointerToUser).toBeDefined(); - }); + expect(result1.data.find.edges[0].node.someField).toBeDefined(); + expect(result1.data.find.edges[0].node.pointerToUser).toBeUndefined(); + expect(result2.data.find.edges[0].node.someField).toBeDefined(); + expect(result2.data.find.edges[0].node.pointerToUser).toBeDefined(); + } + ); it('should support include argument', async () => { await prepareData(); @@ -5771,66 +5813,69 @@ describe('ParseGraphQLServer', () => { ).toEqual([object3.id, object1.id, object2.id]); }); - it_id('47a6adf3-1cb4-4d92-b74c-e480363f9cb5')(it)('should support including relation', async () => { - await prepareData(); + it_id('47a6adf3-1cb4-4d92-b74c-e480363f9cb5')(it)( + 'should support including relation', + async () => { + await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const result1 = await apolloClient.query({ - query: gql` - query FindRoles { - roles { - edges { - node { - name + const result1 = await apolloClient.query({ + query: gql` + query FindRoles { + roles { + edges { + node { + name + } } } } - } - `, - variables: {}, - context: { - headers: { - 'X-Parse-Session-Token': user1.getSessionToken(), + `, + variables: {}, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, }, - }, - }); + }); - const result2 = await apolloClient.query({ - query: gql` - query FindRoles { - roles { - edges { - node { - name - users { - edges { - node { - username + const result2 = await apolloClient.query({ + query: gql` + query FindRoles { + roles { + edges { + node { + name + users { + edges { + node { + username + } } } } } } } - } - `, - variables: {}, - context: { - headers: { - 'X-Parse-Session-Token': user1.getSessionToken(), + `, + variables: {}, + context: { + headers: { + 'X-Parse-Session-Token': user1.getSessionToken(), + }, }, - }, - }); + }); - expect(result1.data.roles.edges[0].node.name).toBeDefined(); - expect(result1.data.roles.edges[0].node.users).toBeUndefined(); - expect(result1.data.roles.edges[0].node.roles).toBeUndefined(); - expect(result2.data.roles.edges[0].node.name).toBeDefined(); - expect(result2.data.roles.edges[0].node.users).toBeDefined(); - expect(result2.data.roles.edges[0].node.users.edges[0].node.username).toBeDefined(); - expect(result2.data.roles.edges[0].node.roles).toBeUndefined(); - }); + expect(result1.data.roles.edges[0].node.name).toBeDefined(); + expect(result1.data.roles.edges[0].node.users).toBeUndefined(); + expect(result1.data.roles.edges[0].node.roles).toBeUndefined(); + expect(result2.data.roles.edges[0].node.name).toBeDefined(); + expect(result2.data.roles.edges[0].node.users).toBeDefined(); + expect(result2.data.roles.edges[0].node.users.edges[0].node.username).toBeDefined(); + expect(result2.data.roles.edges[0].node.roles).toBeUndefined(); + } + ); }); }); @@ -6676,161 +6721,164 @@ describe('ParseGraphQLServer', () => { }); }); - it_id('f722e98e-1fd7-45c5-ade3-5177e3d542e8')(it)('should unset fields when null used on update/create', async () => { - const customerSchema = new Parse.Schema('Customer'); - customerSchema.addString('aString'); - customerSchema.addBoolean('aBoolean'); - customerSchema.addDate('aDate'); - customerSchema.addArray('aArray'); - customerSchema.addGeoPoint('aGeoPoint'); - customerSchema.addPointer('aPointer', 'Customer'); - customerSchema.addObject('aObject'); - customerSchema.addPolygon('aPolygon'); - await customerSchema.save(); + it_id('f722e98e-1fd7-45c5-ade3-5177e3d542e8')(it)( + 'should unset fields when null used on update/create', + async () => { + const customerSchema = new Parse.Schema('Customer'); + customerSchema.addString('aString'); + customerSchema.addBoolean('aBoolean'); + customerSchema.addDate('aDate'); + customerSchema.addArray('aArray'); + customerSchema.addGeoPoint('aGeoPoint'); + customerSchema.addPointer('aPointer', 'Customer'); + customerSchema.addObject('aObject'); + customerSchema.addPolygon('aPolygon'); + await customerSchema.save(); - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - const cus = new Parse.Object('Customer'); - await cus.save({ aString: 'hello' }); - - const fields = { - aString: "i'm string", - aBoolean: true, - aDate: new Date().toISOString(), - aArray: ['hello', 1], - aGeoPoint: { latitude: 30, longitude: 30 }, - aPointer: { link: cus.id }, - aObject: { prop: { subprop: 1 }, prop2: 'test' }, - aPolygon: [ - { latitude: 30, longitude: 30 }, - { latitude: 31, longitude: 31 }, - { latitude: 32, longitude: 32 }, - { latitude: 30, longitude: 30 }, - ], - }; - const nullFields = Object.keys(fields).reduce((acc, k) => ({ ...acc, [k]: null }), {}); - const result = await apolloClient.mutate({ - mutation: gql` - mutation CreateCustomer($input: CreateCustomerInput!) { - createCustomer(input: $input) { - customer { - id - aString - aBoolean - aDate - aArray { - ... on Element { - value + const cus = new Parse.Object('Customer'); + await cus.save({ aString: 'hello' }); + + const fields = { + aString: "i'm string", + aBoolean: true, + aDate: new Date().toISOString(), + aArray: ['hello', 1], + aGeoPoint: { latitude: 30, longitude: 30 }, + aPointer: { link: cus.id }, + aObject: { prop: { subprop: 1 }, prop2: 'test' }, + aPolygon: [ + { latitude: 30, longitude: 30 }, + { latitude: 31, longitude: 31 }, + { latitude: 32, longitude: 32 }, + { latitude: 30, longitude: 30 }, + ], + }; + const nullFields = Object.keys(fields).reduce((acc, k) => ({ ...acc, [k]: null }), {}); + const result = await apolloClient.mutate({ + mutation: gql` + mutation CreateCustomer($input: CreateCustomerInput!) { + createCustomer(input: $input) { + customer { + id + aString + aBoolean + aDate + aArray { + ... on Element { + value + } } - } - aGeoPoint { - longitude - latitude - } - aPointer { - objectId - } - aObject - aPolygon { - longitude - latitude - } - } - } - } - `, - variables: { - input: { fields }, - }, - }); - const { - data: { - createCustomer: { - customer: { aPointer, aArray, id, ...otherFields }, - }, - }, - } = result; - expect(id).toBeDefined(); - delete otherFields.__typename; - delete otherFields.aGeoPoint.__typename; - otherFields.aPolygon.forEach(v => { - delete v.__typename; - }); - expect({ - ...otherFields, - aPointer: { link: aPointer.objectId }, - aArray: aArray.map(({ value }) => value), - }).toEqual(fields); - - const updated = await apolloClient.mutate({ - mutation: gql` - mutation UpdateCustomer($input: UpdateCustomerInput!) { - updateCustomer(input: $input) { - customer { - aString - aBoolean - aDate - aArray { - ... on Element { - value + aGeoPoint { + longitude + latitude + } + aPointer { + objectId + } + aObject + aPolygon { + longitude + latitude } - } - aGeoPoint { - longitude - latitude - } - aPointer { - objectId - } - aObject - aPolygon { - longitude - latitude } } } - } - `, - variables: { - input: { fields: nullFields, id }, - }, - }); - const { - data: { - updateCustomer: { customer }, - }, - } = updated; - delete customer.__typename; - expect(Object.keys(customer).length).toEqual(8); - Object.keys(customer).forEach(k => { - expect(customer[k]).toBeNull(); - }); - try { - const queryResult = await apolloClient.query({ - query: gql` - query getEmptyCustomer($where: CustomerWhereInput!) { - customers(where: $where) { - edges { - node { - id + `, + variables: { + input: { fields }, + }, + }); + const { + data: { + createCustomer: { + customer: { aPointer, aArray, id, ...otherFields }, + }, + }, + } = result; + expect(id).toBeDefined(); + delete otherFields.__typename; + delete otherFields.aGeoPoint.__typename; + otherFields.aPolygon.forEach(v => { + delete v.__typename; + }); + expect({ + ...otherFields, + aPointer: { link: aPointer.objectId }, + aArray: aArray.map(({ value }) => value), + }).toEqual(fields); + + const updated = await apolloClient.mutate({ + mutation: gql` + mutation UpdateCustomer($input: UpdateCustomerInput!) { + updateCustomer(input: $input) { + customer { + aString + aBoolean + aDate + aArray { + ... on Element { + value + } + } + aGeoPoint { + longitude + latitude + } + aPointer { + objectId + } + aObject + aPolygon { + longitude + latitude } } } } `, variables: { - where: Object.keys(fields).reduce( - (acc, k) => ({ ...acc, [k]: { exists: false } }), - {} - ), + input: { fields: nullFields, id }, + }, + }); + const { + data: { + updateCustomer: { customer }, }, + } = updated; + delete customer.__typename; + expect(Object.keys(customer).length).toEqual(8); + Object.keys(customer).forEach(k => { + expect(customer[k]).toBeNull(); }); + try { + const queryResult = await apolloClient.query({ + query: gql` + query getEmptyCustomer($where: CustomerWhereInput!) { + customers(where: $where) { + edges { + node { + id + } + } + } + } + `, + variables: { + where: Object.keys(fields).reduce( + (acc, k) => ({ ...acc, [k]: { exists: false } }), + {} + ), + }, + }); - expect(queryResult.data.customers.edges.length).toEqual(1); - } catch (e) { - console.error(JSON.stringify(e)); + expect(queryResult.data.customers.edges.length).toEqual(1); + } catch (e) { + console.error(JSON.stringify(e)); + } } - }); + ); }); describe('Files Mutations', () => { @@ -9074,232 +9122,235 @@ describe('ParseGraphQLServer', () => { expect(result2.companies.edges[0].node.objectId).toEqual(company1.id); }); - it_id('f4312f2c-90bb-4583-b033-02078ae0ce84')(it)('should support relational where query', async () => { - const president = new Parse.Object('President'); - president.set('name', 'James'); - await president.save(); + it_id('f4312f2c-90bb-4583-b033-02078ae0ce84')(it)( + 'should support relational where query', + async () => { + const president = new Parse.Object('President'); + president.set('name', 'James'); + await president.save(); - const employee = new Parse.Object('Employee'); - employee.set('name', 'John'); - await employee.save(); + const employee = new Parse.Object('Employee'); + employee.set('name', 'John'); + await employee.save(); - const company1 = new Parse.Object('Company'); - company1.set('name', 'imACompany1'); - await company1.save(); + const company1 = new Parse.Object('Company'); + company1.set('name', 'imACompany1'); + await company1.save(); - const company2 = new Parse.Object('Company'); - company2.set('name', 'imACompany2'); - company2.relation('employees').add([employee]); - await company2.save(); + const company2 = new Parse.Object('Company'); + company2.set('name', 'imACompany2'); + company2.relation('employees').add([employee]); + await company2.save(); - const country = new Parse.Object('Country'); - country.set('name', 'imACountry'); - country.relation('companies').add([company1, company2]); - await country.save(); + const country = new Parse.Object('Country'); + country.set('name', 'imACountry'); + country.relation('companies').add([company1, company2]); + await country.save(); - const country2 = new Parse.Object('Country'); - country2.set('name', 'imACountry2'); - country2.relation('companies').add([company1]); - await country2.save(); + const country2 = new Parse.Object('Country'); + country2.set('name', 'imACountry2'); + country2.relation('companies').add([company1]); + await country2.save(); - const country3 = new Parse.Object('Country'); - country3.set('name', 'imACountry3'); - country3.set('president', president); - await country3.save(); + const country3 = new Parse.Object('Country'); + country3.set('name', 'imACountry3'); + country3.set('president', president); + await country3.save(); - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); + await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); - let { - data: { - countries: { edges: result }, - }, - } = await apolloClient.query({ - query: gql` - query findCountry($where: CountryWhereInput) { - countries(where: $where) { - edges { - node { - id - objectId - companies { - edges { - node { - id - objectId - name + let { + data: { + countries: { edges: result }, + }, + } = await apolloClient.query({ + query: gql` + query findCountry($where: CountryWhereInput) { + countries(where: $where) { + edges { + node { + id + objectId + companies { + edges { + node { + id + objectId + name + } } } } } } } - } - `, - variables: { - where: { - companies: { - have: { - employees: { have: { name: { equalTo: 'John' } } }, + `, + variables: { + where: { + companies: { + have: { + employees: { have: { name: { equalTo: 'John' } } }, + }, }, }, }, - }, - }); - expect(result.length).toEqual(1); - result = result[0].node; - expect(result.objectId).toEqual(country.id); - expect(result.companies.edges.length).toEqual(2); + }); + expect(result.length).toEqual(1); + result = result[0].node; + expect(result.objectId).toEqual(country.id); + expect(result.companies.edges.length).toEqual(2); - const { - data: { - countries: { edges: result2 }, - }, - } = await apolloClient.query({ - query: gql` - query findCountry($where: CountryWhereInput) { - countries(where: $where) { - edges { - node { - id - objectId - companies { - edges { - node { - id - objectId - name + const { + data: { + countries: { edges: result2 }, + }, + } = await apolloClient.query({ + query: gql` + query findCountry($where: CountryWhereInput) { + countries(where: $where) { + edges { + node { + id + objectId + companies { + edges { + node { + id + objectId + name + } } } } } } } - } - `, - variables: { - where: { - companies: { - have: { - OR: [ - { name: { equalTo: 'imACompany1' } }, - { name: { equalTo: 'imACompany2' } }, - ], + `, + variables: { + where: { + companies: { + have: { + OR: [ + { name: { equalTo: 'imACompany1' } }, + { name: { equalTo: 'imACompany2' } }, + ], + }, }, }, }, - }, - }); - expect(result2.length).toEqual(2); + }); + expect(result2.length).toEqual(2); - const { - data: { - countries: { edges: result3 }, - }, - } = await apolloClient.query({ - query: gql` - query findCountry($where: CountryWhereInput) { - countries(where: $where) { - edges { - node { - id - name + const { + data: { + countries: { edges: result3 }, + }, + } = await apolloClient.query({ + query: gql` + query findCountry($where: CountryWhereInput) { + countries(where: $where) { + edges { + node { + id + name + } } } } - } - `, - variables: { - where: { - companies: { exists: false }, + `, + variables: { + where: { + companies: { exists: false }, + }, }, - }, - }); - expect(result3.length).toEqual(1); - expect(result3[0].node.name).toEqual('imACountry3'); + }); + expect(result3.length).toEqual(1); + expect(result3[0].node.name).toEqual('imACountry3'); - const { - data: { - countries: { edges: result4 }, - }, - } = await apolloClient.query({ - query: gql` - query findCountry($where: CountryWhereInput) { - countries(where: $where) { - edges { - node { - id - name + const { + data: { + countries: { edges: result4 }, + }, + } = await apolloClient.query({ + query: gql` + query findCountry($where: CountryWhereInput) { + countries(where: $where) { + edges { + node { + id + name + } } } } - } - `, - variables: { - where: { - president: { exists: false }, + `, + variables: { + where: { + president: { exists: false }, + }, }, - }, - }); - expect(result4.length).toEqual(2); - const { - data: { - countries: { edges: result5 }, - }, - } = await apolloClient.query({ - query: gql` - query findCountry($where: CountryWhereInput) { - countries(where: $where) { - edges { - node { - id - name + }); + expect(result4.length).toEqual(2); + const { + data: { + countries: { edges: result5 }, + }, + } = await apolloClient.query({ + query: gql` + query findCountry($where: CountryWhereInput) { + countries(where: $where) { + edges { + node { + id + name + } } } } - } - `, - variables: { - where: { - president: { exists: true }, + `, + variables: { + where: { + president: { exists: true }, + }, }, - }, - }); - expect(result5.length).toEqual(1); - const { - data: { - countries: { edges: result6 }, - }, - } = await apolloClient.query({ - query: gql` - query findCountry($where: CountryWhereInput) { - countries(where: $where) { - edges { - node { - id - objectId - name + }); + expect(result5.length).toEqual(1); + const { + data: { + countries: { edges: result6 }, + }, + } = await apolloClient.query({ + query: gql` + query findCountry($where: CountryWhereInput) { + countries(where: $where) { + edges { + node { + id + objectId + name + } } } } - } - `, - variables: { - where: { - companies: { - haveNot: { - OR: [ - { name: { equalTo: 'imACompany1' } }, - { name: { equalTo: 'imACompany2' } }, - ], + `, + variables: { + where: { + companies: { + haveNot: { + OR: [ + { name: { equalTo: 'imACompany1' } }, + { name: { equalTo: 'imACompany2' } }, + ], + }, }, }, }, - }, - }); - expect(result6.length).toEqual(1); - expect(result6.length).toEqual(1); - expect(result6[0].node.name).toEqual('imACountry3'); - }); + }); + expect(result6.length).toEqual(1); + expect(result6.length).toEqual(1); + expect(result6[0].node.name).toEqual('imACountry3'); + } + ); it('should support files', async () => { try { diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index 8d0d0f9cdc..43d287bf6a 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -187,76 +187,82 @@ describe('Hooks', () => { }); }); - it_id('f7ad092f-81dc-4729-afd1-3b02db2f0948')(it)('should fail trying to create two times the same function', done => { - Parse.Hooks.createFunction('my_new_function', 'http://url.com') - .then(() => jasmine.timeout()) - .then( - () => { - return Parse.Hooks.createFunction('my_new_function', 'http://url.com'); - }, - () => { - fail('should create a new function'); - } - ) - .then( - () => { - fail('should not be able to create the same function'); - }, - err => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.message).toBe('function name: my_new_function already exists'); + it_id('f7ad092f-81dc-4729-afd1-3b02db2f0948')(it)( + 'should fail trying to create two times the same function', + done => { + Parse.Hooks.createFunction('my_new_function', 'http://url.com') + .then(() => jasmine.timeout()) + .then( + () => { + return Parse.Hooks.createFunction('my_new_function', 'http://url.com'); + }, + () => { + fail('should create a new function'); } - return Parse.Hooks.removeFunction('my_new_function'); - } - ) - .then( - () => { - done(); - }, - err => { - jfail(err); - done(); - } - ); - }); + ) + .then( + () => { + fail('should not be able to create the same function'); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.message).toBe('function name: my_new_function already exists'); + } + return Parse.Hooks.removeFunction('my_new_function'); + } + ) + .then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); + } + ); - it_id('4db8c249-9174-4e8e-b959-55c8ea959a02')(it)('should fail trying to create two times the same trigger', done => { - Parse.Hooks.createTrigger('MyClass', 'beforeSave', 'http://url.com') - .then( - () => { - return Parse.Hooks.createTrigger('MyClass', 'beforeSave', 'http://url.com'); - }, - () => { - fail('should create a new trigger'); - } - ) - .then( - () => { - fail('should not be able to create the same trigger'); - }, - err => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.message).toBe('class MyClass already has trigger beforeSave'); + it_id('4db8c249-9174-4e8e-b959-55c8ea959a02')(it)( + 'should fail trying to create two times the same trigger', + done => { + Parse.Hooks.createTrigger('MyClass', 'beforeSave', 'http://url.com') + .then( + () => { + return Parse.Hooks.createTrigger('MyClass', 'beforeSave', 'http://url.com'); + }, + () => { + fail('should create a new trigger'); } - return Parse.Hooks.removeTrigger('MyClass', 'beforeSave'); - } - ) - .then( - () => { - done(); - }, - err => { - jfail(err); - done(); - } - ); - }); + ) + .then( + () => { + fail('should not be able to create the same trigger'); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.message).toBe('class MyClass already has trigger beforeSave'); + } + return Parse.Hooks.removeTrigger('MyClass', 'beforeSave'); + } + ) + .then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); + } + ); it("should fail trying to update a function that don't exist", done => { Parse.Hooks.updateFunction('A_COOL_FUNCTION', 'http://url.com') @@ -358,164 +364,102 @@ describe('Hooks', () => { }); }); - it_id('96d99414-b739-4e36-b3f4-8135e0be83ea')(it)('should create hooks and properly preload them', done => { - const promises = []; - for (let i = 0; i < 5; i++) { - promises.push( - Parse.Hooks.createTrigger('MyClass' + i, 'beforeSave', 'http://url.com/beforeSave/' + i) - ); - promises.push(Parse.Hooks.createFunction('AFunction' + i, 'http://url.com/function' + i)); - } + it_id('96d99414-b739-4e36-b3f4-8135e0be83ea')(it)( + 'should create hooks and properly preload them', + done => { + const promises = []; + for (let i = 0; i < 5; i++) { + promises.push( + Parse.Hooks.createTrigger('MyClass' + i, 'beforeSave', 'http://url.com/beforeSave/' + i) + ); + promises.push(Parse.Hooks.createFunction('AFunction' + i, 'http://url.com/function' + i)); + } - Promise.all(promises) - .then( - function () { - for (let i = 0; i < 5; i++) { - // Delete everything from memory, as the server just started - triggers.removeTrigger('beforeSave', 'MyClass' + i, Parse.applicationId); - triggers.removeFunction('AFunction' + i, Parse.applicationId); - expect( - triggers.getTrigger('MyClass' + i, 'beforeSave', Parse.applicationId) - ).toBeUndefined(); - expect(triggers.getFunction('AFunction' + i, Parse.applicationId)).toBeUndefined(); + Promise.all(promises) + .then( + function () { + for (let i = 0; i < 5; i++) { + // Delete everything from memory, as the server just started + triggers.removeTrigger('beforeSave', 'MyClass' + i, Parse.applicationId); + triggers.removeFunction('AFunction' + i, Parse.applicationId); + expect( + triggers.getTrigger('MyClass' + i, 'beforeSave', Parse.applicationId) + ).toBeUndefined(); + expect(triggers.getFunction('AFunction' + i, Parse.applicationId)).toBeUndefined(); + } + const hooksController = new HooksController( + Parse.applicationId, + Config.get('test').database + ); + return hooksController.load(); + }, + err => { + jfail(err); + fail('Should properly create all hooks'); + done(); } - const hooksController = new HooksController( - Parse.applicationId, - Config.get('test').database - ); - return hooksController.load(); - }, - err => { - jfail(err); - fail('Should properly create all hooks'); - done(); - } - ) - .then( - function () { - for (let i = 0; i < 5; i++) { - expect( - triggers.getTrigger('MyClass' + i, 'beforeSave', Parse.applicationId) - ).not.toBeUndefined(); - expect(triggers.getFunction('AFunction' + i, Parse.applicationId)).not.toBeUndefined(); + ) + .then( + function () { + for (let i = 0; i < 5; i++) { + expect( + triggers.getTrigger('MyClass' + i, 'beforeSave', Parse.applicationId) + ).not.toBeUndefined(); + expect( + triggers.getFunction('AFunction' + i, Parse.applicationId) + ).not.toBeUndefined(); + } + done(); + }, + err => { + jfail(err); + fail('should properly load all hooks'); + done(); } - done(); - }, - err => { - jfail(err); - fail('should properly load all hooks'); - done(); - } - ); - }); - - it_id('fe7d41eb-e570-4804-ac1f-8b6c407fdafe')(it)('should run the function on the test server', done => { - app.post('/SomeFunction', function (req, res) { - res.json({ success: 'OK!' }); - }); + ); + } + ); - Parse.Hooks.createFunction('SOME_TEST_FUNCTION', hookServerURL + '/SomeFunction') - .then( - function () { - return Parse.Cloud.run('SOME_TEST_FUNCTION'); - }, - err => { - jfail(err); - fail('Should not fail creating a function'); - done(); - } - ) - .then( - function (res) { - expect(res).toBe('OK!'); - done(); - }, - err => { - jfail(err); - fail('Should not fail calling a function'); - done(); - } - ); - }); + it_id('fe7d41eb-e570-4804-ac1f-8b6c407fdafe')(it)( + 'should run the function on the test server', + done => { + app.post('/SomeFunction', function (req, res) { + res.json({ success: 'OK!' }); + }); - it_id('63985b4c-a212-4a86-aa0e-eb4600bb485b')(it)('should run the function on the test server (error handling)', done => { - app.post('/SomeFunctionError', function (req, res) { - res.json({ error: { code: 1337, error: 'hacking that one!' } }); - }); - // The function is deleted as the DB is dropped between calls - Parse.Hooks.createFunction('SOME_TEST_FUNCTION', hookServerURL + '/SomeFunctionError') - .then( - function () { - return Parse.Cloud.run('SOME_TEST_FUNCTION'); - }, - err => { - jfail(err); - fail('Should not fail creating a function'); - done(); - } - ) - .then( - function () { - fail('Should not succeed calling that function'); - done(); - }, - err => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(Parse.Error.SCRIPT_FAILED); - expect(err.message.code).toEqual(1337); - expect(err.message.error).toEqual('hacking that one!'); + Parse.Hooks.createFunction('SOME_TEST_FUNCTION', hookServerURL + '/SomeFunction') + .then( + function () { + return Parse.Cloud.run('SOME_TEST_FUNCTION'); + }, + err => { + jfail(err); + fail('Should not fail creating a function'); + done(); } - done(); - } - ); - }); - - it_id('bacc1754-2a3a-4a7a-8d0e-f80af36da1ef')(it)('should provide X-Parse-Webhook-Key when defined', done => { - app.post('/ExpectingKey', function (req, res) { - if (req.get('X-Parse-Webhook-Key') === 'hook') { - res.json({ success: 'correct key provided' }); - } else { - res.json({ error: 'incorrect key provided' }); - } - }); - - Parse.Hooks.createFunction('SOME_TEST_FUNCTION', hookServerURL + '/ExpectingKey') - .then( - function () { - return Parse.Cloud.run('SOME_TEST_FUNCTION'); - }, - err => { - jfail(err); - fail('Should not fail creating a function'); - done(); - } - ) - .then( - function (res) { - expect(res).toBe('correct key provided'); - done(); - }, - err => { - jfail(err); - fail('Should not fail calling a function'); - done(); - } - ); - }); + ) + .then( + function (res) { + expect(res).toBe('OK!'); + done(); + }, + err => { + jfail(err); + fail('Should not fail calling a function'); + done(); + } + ); + } + ); - it_id('eeb67946-42c6-4581-89af-2abb4927913e')(it)('should not pass X-Parse-Webhook-Key if not provided', done => { - reconfigureServer({ webhookKey: undefined }).then(() => { - app.post('/ExpectingKeyAlso', function (req, res) { - if (req.get('X-Parse-Webhook-Key') === 'hook') { - res.json({ success: 'correct key provided' }); - } else { - res.json({ error: 'incorrect key provided' }); - } + it_id('63985b4c-a212-4a86-aa0e-eb4600bb485b')(it)( + 'should run the function on the test server (error handling)', + done => { + app.post('/SomeFunctionError', function (req, res) { + res.json({ error: { code: 1337, error: 'hacking that one!' } }); }); - - Parse.Hooks.createFunction('SOME_TEST_FUNCTION', hookServerURL + '/ExpectingKeyAlso') + // The function is deleted as the DB is dropped between calls + Parse.Hooks.createFunction('SOME_TEST_FUNCTION', hookServerURL + '/SomeFunctionError') .then( function () { return Parse.Cloud.run('SOME_TEST_FUNCTION'); @@ -536,105 +480,197 @@ describe('Hooks', () => { expect(err).not.toBe(null); if (err) { expect(err.code).toBe(Parse.Error.SCRIPT_FAILED); - expect(err.message).toEqual('incorrect key provided'); + expect(err.message.code).toEqual(1337); + expect(err.message.error).toEqual('hacking that one!'); } done(); } ); - }); - }); + } + ); - it_id('21decb65-4b93-4791-85a3-ab124a9ea3ac')(it)('should run the beforeSave hook on the test server', done => { - let triggerCount = 0; - app.post('/BeforeSaveSome', function (req, res) { - triggerCount++; - const object = req.body.object; - object.hello = 'world'; - // Would need parse cloud express to set much more - // But this should override the key upon return - res.json({ success: object }); - }); - // The function is deleted as the DB is dropped between calls - Parse.Hooks.createTrigger('SomeRandomObject', 'beforeSave', hookServerURL + '/BeforeSaveSome') - .then(function () { - const obj = new Parse.Object('SomeRandomObject'); - return obj.save(); - }) - .then(function (res) { - expect(triggerCount).toBe(1); - return res.fetch(); - }) - .then(function (res) { - expect(res.get('hello')).toEqual('world'); - done(); - }) - .catch(err => { - jfail(err); - fail('Should not fail creating a function'); - done(); + it_id('bacc1754-2a3a-4a7a-8d0e-f80af36da1ef')(it)( + 'should provide X-Parse-Webhook-Key when defined', + done => { + app.post('/ExpectingKey', function (req, res) { + if (req.get('X-Parse-Webhook-Key') === 'hook') { + res.json({ success: 'correct key provided' }); + } else { + res.json({ error: 'incorrect key provided' }); + } }); - }); - it_id('52e3152b-5514-4418-9e76-1f394368b8fb')(it)('beforeSave hooks should correctly handle responses containing entire object', done => { - app.post('/BeforeSaveSome2', function (req, res) { - const object = Parse.Object.fromJSON(req.body.object); - object.set('hello', 'world'); - res.json({ success: object }); - }); - Parse.Hooks.createTrigger('SomeRandomObject2', 'beforeSave', hookServerURL + '/BeforeSaveSome2') - .then(function () { - const obj = new Parse.Object('SomeRandomObject2'); - return obj.save(); - }) - .then(function (res) { - return res.save(); - }) - .then(function (res) { - expect(res.get('hello')).toEqual('world'); - done(); - }) - .catch(err => { - fail(`Should not fail: ${JSON.stringify(err)}`); - done(); - }); - }); + Parse.Hooks.createFunction('SOME_TEST_FUNCTION', hookServerURL + '/ExpectingKey') + .then( + function () { + return Parse.Cloud.run('SOME_TEST_FUNCTION'); + }, + err => { + jfail(err); + fail('Should not fail creating a function'); + done(); + } + ) + .then( + function (res) { + expect(res).toBe('correct key provided'); + done(); + }, + err => { + jfail(err); + fail('Should not fail calling a function'); + done(); + } + ); + } + ); + + it_id('eeb67946-42c6-4581-89af-2abb4927913e')(it)( + 'should not pass X-Parse-Webhook-Key if not provided', + done => { + reconfigureServer({ webhookKey: undefined }).then(() => { + app.post('/ExpectingKeyAlso', function (req, res) { + if (req.get('X-Parse-Webhook-Key') === 'hook') { + res.json({ success: 'correct key provided' }); + } else { + res.json({ error: 'incorrect key provided' }); + } + }); - it_id('d27a7587-abb5-40d5-9805-051ee91de474')(it)('should run the afterSave hook on the test server', done => { - let triggerCount = 0; - let newObjectId; - app.post('/AfterSaveSome', function (req, res) { - triggerCount++; - const obj = new Parse.Object('AnotherObject'); - obj.set('foo', 'bar'); - obj.save().then(function (obj) { - newObjectId = obj.id; - res.json({ success: {} }); + Parse.Hooks.createFunction('SOME_TEST_FUNCTION', hookServerURL + '/ExpectingKeyAlso') + .then( + function () { + return Parse.Cloud.run('SOME_TEST_FUNCTION'); + }, + err => { + jfail(err); + fail('Should not fail creating a function'); + done(); + } + ) + .then( + function () { + fail('Should not succeed calling that function'); + done(); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(Parse.Error.SCRIPT_FAILED); + expect(err.message).toEqual('incorrect key provided'); + } + done(); + } + ); }); - }); - // The function is deleted as the DB is dropped between calls - Parse.Hooks.createTrigger('SomeRandomObject', 'afterSave', hookServerURL + '/AfterSaveSome') - .then(function () { - const obj = new Parse.Object('SomeRandomObject'); - return obj.save(); - }) - .then(function () { - return new Promise(resolve => { - setTimeout(() => { - expect(triggerCount).toBe(1); - new Parse.Query('AnotherObject').get(newObjectId).then(r => resolve(r)); - }, 500); + } + ); + + it_id('21decb65-4b93-4791-85a3-ab124a9ea3ac')(it)( + 'should run the beforeSave hook on the test server', + done => { + let triggerCount = 0; + app.post('/BeforeSaveSome', function (req, res) { + triggerCount++; + const object = req.body.object; + object.hello = 'world'; + // Would need parse cloud express to set much more + // But this should override the key upon return + res.json({ success: object }); + }); + // The function is deleted as the DB is dropped between calls + Parse.Hooks.createTrigger('SomeRandomObject', 'beforeSave', hookServerURL + '/BeforeSaveSome') + .then(function () { + const obj = new Parse.Object('SomeRandomObject'); + return obj.save(); + }) + .then(function (res) { + expect(triggerCount).toBe(1); + return res.fetch(); + }) + .then(function (res) { + expect(res.get('hello')).toEqual('world'); + done(); + }) + .catch(err => { + jfail(err); + fail('Should not fail creating a function'); + done(); }); - }) - .then(function (res) { - expect(res.get('foo')).toEqual('bar'); - done(); - }) - .catch(err => { - jfail(err); - fail('Should not fail creating a function'); - done(); + } + ); + + it_id('52e3152b-5514-4418-9e76-1f394368b8fb')(it)( + 'beforeSave hooks should correctly handle responses containing entire object', + done => { + app.post('/BeforeSaveSome2', function (req, res) { + const object = Parse.Object.fromJSON(req.body.object); + object.set('hello', 'world'); + res.json({ success: object }); }); - }); + Parse.Hooks.createTrigger( + 'SomeRandomObject2', + 'beforeSave', + hookServerURL + '/BeforeSaveSome2' + ) + .then(function () { + const obj = new Parse.Object('SomeRandomObject2'); + return obj.save(); + }) + .then(function (res) { + return res.save(); + }) + .then(function (res) { + expect(res.get('hello')).toEqual('world'); + done(); + }) + .catch(err => { + fail(`Should not fail: ${JSON.stringify(err)}`); + done(); + }); + } + ); + + it_id('d27a7587-abb5-40d5-9805-051ee91de474')(it)( + 'should run the afterSave hook on the test server', + done => { + let triggerCount = 0; + let newObjectId; + app.post('/AfterSaveSome', function (req, res) { + triggerCount++; + const obj = new Parse.Object('AnotherObject'); + obj.set('foo', 'bar'); + obj.save().then(function (obj) { + newObjectId = obj.id; + res.json({ success: {} }); + }); + }); + // The function is deleted as the DB is dropped between calls + Parse.Hooks.createTrigger('SomeRandomObject', 'afterSave', hookServerURL + '/AfterSaveSome') + .then(function () { + const obj = new Parse.Object('SomeRandomObject'); + return obj.save(); + }) + .then(function () { + return new Promise(resolve => { + setTimeout(() => { + expect(triggerCount).toBe(1); + new Parse.Query('AnotherObject').get(newObjectId).then(r => resolve(r)); + }, 500); + }); + }) + .then(function (res) { + expect(res.get('foo')).toEqual('bar'); + done(); + }) + .catch(err => { + jfail(err); + fail('Should not fail creating a function'); + done(); + }); + } + ); }); describe('triggers', () => { diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index c03a727b4a..8dd91c5a05 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -856,58 +856,61 @@ describe('Installations', () => { }); }); - it_id('22311bc7-3f4f-42c1-a958-57083929e80d')(it)('update is linking two existing objects w/ increment', done => { - const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; - let input = { - installationId: installId, - deviceType: 'ios', - }; - rest - .create(config, auth.nobody(config), '_Installation', input) - .then(() => { - input = { - deviceToken: t, - deviceType: 'ios', - }; - return rest.create(config, auth.nobody(config), '_Installation', input); - }) - .then(() => - database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}) - ) - .then(results => { - expect(results.length).toEqual(1); - input = { - deviceToken: t, - installationId: installId, - deviceType: 'ios', - score: { - __op: 'Increment', - amount: 1, - }, - }; - return rest.update( - config, - auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, - input - ); - }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) - .then(results => { - expect(results.length).toEqual(1); - expect(results[0].installationId).toEqual(installId); - expect(results[0].deviceToken).toEqual(t); - expect(results[0].deviceType).toEqual('ios'); - expect(results[0].score).toEqual(1); - done(); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + it_id('22311bc7-3f4f-42c1-a958-57083929e80d')(it)( + 'update is linking two existing objects w/ increment', + done => { + const installId = '12345678-abcd-abcd-abcd-123456789abc'; + const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + let input = { + installationId: installId, + deviceType: 'ios', + }; + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => { + input = { + deviceToken: t, + deviceType: 'ios', + }; + return rest.create(config, auth.nobody(config), '_Installation', input); + }) + .then(() => + database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}) + ) + .then(results => { + expect(results.length).toEqual(1); + input = { + deviceToken: t, + installationId: installId, + deviceType: 'ios', + score: { + __op: 'Increment', + amount: 1, + }, + }; + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); + }) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].installationId).toEqual(installId); + expect(results[0].deviceToken).toEqual(t); + expect(results[0].deviceType).toEqual('ios'); + expect(results[0].score).toEqual(1); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + } + ); it('update is linking two existing with installation id', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; @@ -969,70 +972,73 @@ describe('Installations', () => { }); }); - it_id('f2975078-eab7-4287-a932-288842e3cfb9')(it)('update is linking two existing with installation id w/ op', done => { - const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; - let input = { - installationId: installId, - deviceType: 'ios', - }; - let installObj; - let tokenObj; - rest - .create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) - .then(results => { - expect(results.length).toEqual(1); - installObj = results[0]; - input = { - deviceToken: t, - deviceType: 'ios', - }; - return rest.create(config, auth.nobody(config), '_Installation', input); - }) - .then(() => - database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}) - ) - .then(results => { - expect(results.length).toEqual(1); - tokenObj = results[0]; - input = { - installationId: installId, - deviceToken: t, - deviceType: 'ios', - score: { - __op: 'Increment', - amount: 1, - }, - }; - return rest.update( - config, - auth.nobody(config), - '_Installation', - { objectId: installObj.objectId }, - input - ); - }) - .then(() => - database.adapter.find( - '_Installation', - installationSchema, - { objectId: tokenObj.objectId }, - {} + it_id('f2975078-eab7-4287-a932-288842e3cfb9')(it)( + 'update is linking two existing with installation id w/ op', + done => { + const installId = '12345678-abcd-abcd-abcd-123456789abc'; + const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + let input = { + installationId: installId, + deviceType: 'ios', + }; + let installObj; + let tokenObj; + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + installObj = results[0]; + input = { + deviceToken: t, + deviceType: 'ios', + }; + return rest.create(config, auth.nobody(config), '_Installation', input); + }) + .then(() => + database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {}) ) - ) - .then(results => { - expect(results.length).toEqual(1); - expect(results[0].installationId).toEqual(installId); - expect(results[0].deviceToken).toEqual(t); - expect(results[0].score).toEqual(1); - done(); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + .then(results => { + expect(results.length).toEqual(1); + tokenObj = results[0]; + input = { + installationId: installId, + deviceToken: t, + deviceType: 'ios', + score: { + __op: 'Increment', + amount: 1, + }, + }; + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: installObj.objectId }, + input + ); + }) + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { objectId: tokenObj.objectId }, + {} + ) + ) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].installationId).toEqual(installId); + expect(results[0].deviceToken).toEqual(t); + expect(results[0].score).toEqual(1); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + } + ); it('ios merge existing same token no installation id', done => { // Test creating installation when there is an existing object with the diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 6294c609a1..85335bb127 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -878,101 +878,107 @@ describe('ParseLiveQuery', function () { await expectAsync(query.subscribe()).toBeRejectedWith(new Error('Invalid session token')); }); - it_id('4ccc9508-ae6a-46ec-932a-9f5e49ab3b9e')(it)('handle invalid websocket payload length', async done => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - websocketTimeout: 100, - }); - const object = new TestObject(); - await object.save(); - - const query = new Parse.Query(TestObject); - query.equalTo('objectId', object.id); - const subscription = await query.subscribe(); - - // All control frames must have a payload length of 125 bytes or less. - // https://tools.ietf.org/html/rfc6455#section-5.5 - // - // 0x89 = 10001001 = ping - // 0xfe = 11111110 = first bit is masking the remaining 7 are 1111110 or 126 the payload length - // https://tools.ietf.org/html/rfc6455#section-5.2 - const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); - client.socket._socket.write(Buffer.from([0x89, 0xfe])); - - subscription.on('update', async object => { - expect(object.get('foo')).toBe('bar'); - done(); - }); - // Wait for Websocket timeout to reconnect - setTimeout(async () => { - object.set({ foo: 'bar' }); + it_id('4ccc9508-ae6a-46ec-932a-9f5e49ab3b9e')(it)( + 'handle invalid websocket payload length', + async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + websocketTimeout: 100, + }); + const object = new TestObject(); await object.save(); - }, 1000); - }); - - it_id('39a9191f-26dd-4e05-a379-297a67928de8')(it)('should execute live query update on email validation', async done => { - const emailAdapter = { - sendVerificationEmail: () => {}, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - - await reconfigureServer({ - maintenanceKey: 'test2', - liveQuery: { - classNames: [Parse.User], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - websocketTimeout: 100, - appName: 'liveQueryEmailValidation', - verifyUserEmails: true, - emailAdapter: emailAdapter, - emailVerifyTokenValidityDuration: 20, // 0.5 second - publicServerURL: 'http://localhost:8378/1', - }).then(() => { - const user = new Parse.User(); - user.set('password', 'asdf'); - user.set('email', 'asdf@example.com'); - user.set('username', 'zxcv'); - user - .signUp() - .then(() => { - const config = Config.get('test'); - return config.database.find( - '_User', - { - username: 'zxcv', - }, - {}, - Auth.maintenance(config) - ); - }) - .then(async results => { - const foundUser = results[0]; - const query = new Parse.Query('_User'); - query.equalTo('objectId', foundUser.objectId); - const subscription = await query.subscribe(); - - subscription.on('update', async object => { - expect(object).toBeDefined(); - expect(object.get('emailVerified')).toBe(true); - done(); - }); - const userController = new UserController(emailAdapter, 'test', { - verifyUserEmails: true, + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + + // All control frames must have a payload length of 125 bytes or less. + // https://tools.ietf.org/html/rfc6455#section-5.5 + // + // 0x89 = 10001001 = ping + // 0xfe = 11111110 = first bit is masking the remaining 7 are 1111110 or 126 the payload length + // https://tools.ietf.org/html/rfc6455#section-5.2 + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + client.socket._socket.write(Buffer.from([0x89, 0xfe])); + + subscription.on('update', async object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + // Wait for Websocket timeout to reconnect + setTimeout(async () => { + object.set({ foo: 'bar' }); + await object.save(); + }, 1000); + } + ); + + it_id('39a9191f-26dd-4e05-a379-297a67928de8')(it)( + 'should execute live query update on email validation', + async done => { + const emailAdapter = { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + + await reconfigureServer({ + maintenanceKey: 'test2', + liveQuery: { + classNames: [Parse.User], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + websocketTimeout: 100, + appName: 'liveQueryEmailValidation', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 20, // 0.5 second + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + const user = new Parse.User(); + user.set('password', 'asdf'); + user.set('email', 'asdf@example.com'); + user.set('username', 'zxcv'); + user + .signUp() + .then(() => { + const config = Config.get('test'); + return config.database.find( + '_User', + { + username: 'zxcv', + }, + {}, + Auth.maintenance(config) + ); + }) + .then(async results => { + const foundUser = results[0]; + const query = new Parse.Query('_User'); + query.equalTo('objectId', foundUser.objectId); + const subscription = await query.subscribe(); + + subscription.on('update', async object => { + expect(object).toBeDefined(); + expect(object.get('emailVerified')).toBe(true); + done(); + }); + + const userController = new UserController(emailAdapter, 'test', { + verifyUserEmails: true, + }); + userController.verifyEmail(foundUser._email_verify_token); }); - userController.verifyEmail(foundUser._email_verify_token); - }); - }); - }); + }); + } + ); it('should not broadcast event to client with invalid session token - avisory GHSA-2xm2-xj2q-qgpj', async done => { await reconfigureServer({ diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index 4c0f5bb4c2..58bb02c2d1 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -302,7 +302,9 @@ describe('Parse.Object testing', () => { it('invalid key name', function (done) { const item = new Parse.Object('Item'); - expect(() => item.set({ 'foo^bar': 'baz' })).toThrow(new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: "foo^bar"')); + expect(() => item.set({ 'foo^bar': 'baz' })).toThrow( + new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: "foo^bar"') + ); item.save({ 'foo^bar': 'baz' }).then(fail, () => done()); }); @@ -570,7 +572,10 @@ describe('Parse.Object testing', () => { it_only_db('mongo')('can increment array nested fields', async () => { const obj = new TestObject(); - obj.set('items', [ { value: 'a', count: 5 }, { value: 'b', count: 1 } ]); + obj.set('items', [ + { value: 'a', count: 5 }, + { value: 'b', count: 1 }, + ]); await obj.save(); obj.increment('items.0.count', 15); obj.increment('items.1.count', 4); @@ -2122,15 +2127,15 @@ describe('Parse.Object testing', () => { it('should not change the json field to array in afterSave', async () => { Parse.Cloud.beforeSave('failingJSONTestCase', req => { - expect(req.object.get('jsonField')).toEqual({ '123': 'test' }); + expect(req.object.get('jsonField')).toEqual({ 123: 'test' }); }); Parse.Cloud.afterSave('failingJSONTestCase', req => { - expect(req.object.get('jsonField')).toEqual({ '123': 'test' }); + expect(req.object.get('jsonField')).toEqual({ 123: 'test' }); }); const object = new Parse.Object('failingJSONTestCase'); - object.set('jsonField', { '123': 'test' }); + object.set('jsonField', { 123: 'test' }); await object.save(); }); @@ -2141,15 +2146,15 @@ describe('Parse.Object testing', () => { { field: 'boolean', value: true }, { field: 'array', value: [0, 1, 2] }, { field: 'array', value: [1, 2, 3] }, - { field: 'array', value: [{ '0': 'a' }, 2, 3] }, + { field: 'array', value: [{ 0: 'a' }, 2, 3] }, { field: 'object', value: { key: 'value' } }, { field: 'object', value: { key1: 'value1', key2: 'value2' } }, { field: 'object', value: { key1: 1, key2: 2 } }, { field: 'object', value: { '1x1': 1 } }, - { field: 'object', value: { '1x1': 1, '2': 2 } }, - { field: 'object', value: { '0': 0 } }, - { field: 'object', value: { '1': 1 } }, - { field: 'object', value: { '0': { '0': 'a', '1': 'b' } } }, + { field: 'object', value: { '1x1': 1, 2: 2 } }, + { field: 'object', value: { 0: 0 } }, + { field: 'object', value: { 1: 1 } }, + { field: 'object', value: { 0: { 0: 'a', 1: 'b' } } }, { field: 'date', value: new Date() }, { field: 'file', diff --git a/spec/ParsePubSub.spec.js b/spec/ParsePubSub.spec.js index 53bdd0b674..063c35728d 100644 --- a/spec/ParsePubSub.spec.js +++ b/spec/ParsePubSub.spec.js @@ -28,8 +28,8 @@ describe('ParsePubSub', function () { }); const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') - .EventEmitterPubSub; + const EventEmitterPubSub = + require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; expect(RedisPubSub.createPublisher).toHaveBeenCalledWith({ redisURL: 'redisURL', redisOptions: { socket_keepalive: true }, @@ -41,8 +41,8 @@ describe('ParsePubSub', function () { ParsePubSub.createPublisher({}); const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') - .EventEmitterPubSub; + const EventEmitterPubSub = + require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; expect(RedisPubSub.createPublisher).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createPublisher).toHaveBeenCalled(); }); @@ -54,8 +54,8 @@ describe('ParsePubSub', function () { }); const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') - .EventEmitterPubSub; + const EventEmitterPubSub = + require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith({ redisURL: 'redisURL', redisOptions: { socket_keepalive: true }, @@ -67,8 +67,8 @@ describe('ParsePubSub', function () { ParsePubSub.createSubscriber({}); const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') - .EventEmitterPubSub; + const EventEmitterPubSub = + require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createSubscriber).toHaveBeenCalled(); }); @@ -89,8 +89,8 @@ describe('ParsePubSub', function () { expect(adapter.createSubscriber).toHaveBeenCalled(); const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') - .EventEmitterPubSub; + const EventEmitterPubSub = + require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled(); expect(RedisPubSub.createPublisher).not.toHaveBeenCalled(); @@ -117,8 +117,8 @@ describe('ParsePubSub', function () { expect(adapter.createSubscriber).toHaveBeenCalled(); const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') - .EventEmitterPubSub; + const EventEmitterPubSub = + require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled(); expect(RedisPubSub.createPublisher).not.toHaveBeenCalled(); diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 902b34d9d3..70609c39c6 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -296,112 +296,125 @@ describe('Parse.Query Aggregate testing', () => { .catch(done.fail); }); - it_id('c7695018-03de-49e4-8a72-d4d956f70deb')(it_exclude_dbs(['postgres']))('group and multiply transform', done => { - const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); - const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); - const pipeline = [ - { - $group: { - _id: null, - total: { $sum: { $multiply: ['$quantity', '$price'] } }, + it_id('c7695018-03de-49e4-8a72-d4d956f70deb')(it_exclude_dbs(['postgres']))( + 'group and multiply transform', + done => { + const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); + const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); + const pipeline = [ + { + $group: { + _id: null, + total: { $sum: { $multiply: ['$quantity', '$price'] } }, + }, }, - }, - ]; - Parse.Object.saveAll([obj1, obj2]) - .then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }) - .then(results => { - expect(results.length).toEqual(1); - expect(results[0].total).toEqual(45); - done(); - }); - }); + ]; + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].total).toEqual(45); + done(); + }); + } + ); - it_id('2d278175-7594-4b29-bef4-04c778b7a42f')(it_exclude_dbs(['postgres']))('project and multiply transform', done => { - const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); - const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); - const pipeline = [ - { - $match: { quantity: { $exists: true } }, - }, - { - $project: { - name: 1, - total: { $multiply: ['$quantity', '$price'] }, + it_id('2d278175-7594-4b29-bef4-04c778b7a42f')(it_exclude_dbs(['postgres']))( + 'project and multiply transform', + done => { + const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); + const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); + const pipeline = [ + { + $match: { quantity: { $exists: true } }, }, - }, - ]; - Parse.Object.saveAll([obj1, obj2]) - .then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }) - .then(results => { - expect(results.length).toEqual(2); - if (results[0].name === 'item a') { + { + $project: { + name: 1, + total: { $multiply: ['$quantity', '$price'] }, + }, + }, + ]; + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(2); + if (results[0].name === 'item a') { + expect(results[0].total).toEqual(20); + expect(results[1].total).toEqual(25); + } else { + expect(results[0].total).toEqual(25); + expect(results[1].total).toEqual(20); + } + done(); + }); + } + ); + + it_id('9c9d9318-3a9e-4c2a-8a09-d3aa52c7505b')(it_exclude_dbs(['postgres']))( + 'project without objectId transform', + done => { + const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); + const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); + const pipeline = [ + { + $match: { quantity: { $exists: true } }, + }, + { + $project: { + _id: 0, + total: { $multiply: ['$quantity', '$price'] }, + }, + }, + { + $sort: { total: 1 }, + }, + ]; + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(2); expect(results[0].total).toEqual(20); + expect(results[0].objectId).toEqual(undefined); expect(results[1].total).toEqual(25); - } else { - expect(results[0].total).toEqual(25); - expect(results[1].total).toEqual(20); - } - done(); - }); - }); + expect(results[1].objectId).toEqual(undefined); + done(); + }); + } + ); - it_id('9c9d9318-3a9e-4c2a-8a09-d3aa52c7505b')(it_exclude_dbs(['postgres']))('project without objectId transform', done => { - const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); - const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); - const pipeline = [ - { - $match: { quantity: { $exists: true } }, - }, - { - $project: { - _id: 0, - total: { $multiply: ['$quantity', '$price'] }, + it_id('f92c82ac-1993-4758-b718-45689dfc4154')(it_exclude_dbs(['postgres']))( + 'project updatedAt only transform', + done => { + const pipeline = [ + { + $project: { _id: 0, updatedAt: 1 }, }, - }, - { - $sort: { total: 1 }, - }, - ]; - Parse.Object.saveAll([obj1, obj2]) - .then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }) - .then(results => { - expect(results.length).toEqual(2); - expect(results[0].total).toEqual(20); - expect(results[0].objectId).toEqual(undefined); - expect(results[1].total).toEqual(25); - expect(results[1].objectId).toEqual(undefined); + ]; + const query = new Parse.Query(TestObject); + query.aggregate(pipeline).then(results => { + expect(results.length).toEqual(4); + for (let i = 0; i < results.length; i++) { + const item = results[i]; + expect(Object.prototype.hasOwnProperty.call(item, 'updatedAt')).toEqual(true); + expect(Object.prototype.hasOwnProperty.call(item, 'objectId')).toEqual(false); + } done(); }); - }); - - it_id('f92c82ac-1993-4758-b718-45689dfc4154')(it_exclude_dbs(['postgres']))('project updatedAt only transform', done => { - const pipeline = [ - { - $project: { _id: 0, updatedAt: 1 }, - }, - ]; - const query = new Parse.Query(TestObject); - query.aggregate(pipeline).then(results => { - expect(results.length).toEqual(4); - for (let i = 0; i < results.length; i++) { - const item = results[i]; - expect(Object.prototype.hasOwnProperty.call(item, 'updatedAt')).toEqual(true); - expect(Object.prototype.hasOwnProperty.call(item, 'objectId')).toEqual(false); - } - done(); - }); - }); + } + ); - it_id('99566b1d-778d-4444-9deb-c398108e659d')(it_exclude_dbs(['postgres']))('can group by any date field (it does not work if you have dirty data)', + it_id('99566b1d-778d-4444-9deb-c398108e659d')(it_exclude_dbs(['postgres']))( + 'can group by any date field (it does not work if you have dirty data)', done => { // rows in your collection with non date data in the field that is supposed to be a date const obj1 = new TestObject({ dateField2019: new Date(1990, 11, 1) }); @@ -658,22 +671,25 @@ describe('Parse.Query Aggregate testing', () => { }); }); - it_id('d98c8c20-6dac-4d74-8228-85a1ae46a7d0')(it)('should aggregate with Date object (directAccess)', async () => { - const rest = require('../lib/rest'); - const auth = require('../lib/Auth'); - const TestObject = Parse.Object.extend('TestObject'); - const date = new Date(); - await new TestObject({ date: date }).save(null, { useMasterKey: true }); - const config = Config.get(Parse.applicationId); - const resp = await rest.find( - config, - auth.master(config), - 'TestObject', - {}, - { pipeline: [{ $match: { date: { $lte: new Date() } } }] } - ); - expect(resp.results.length).toBe(1); - }); + it_id('d98c8c20-6dac-4d74-8228-85a1ae46a7d0')(it)( + 'should aggregate with Date object (directAccess)', + async () => { + const rest = require('../lib/rest'); + const auth = require('../lib/Auth'); + const TestObject = Parse.Object.extend('TestObject'); + const date = new Date(); + await new TestObject({ date: date }).save(null, { useMasterKey: true }); + const config = Config.get(Parse.applicationId); + const resp = await rest.find( + config, + auth.master(config), + 'TestObject', + {}, + { pipeline: [{ $match: { date: { $lte: new Date() } } }] } + ); + expect(resp.results.length).toBe(1); + } + ); it_id('3d73d23a-fce1-4ac0-972a-50f6a550f348')(it)('match comparison query', done => { const options = Object.assign({}, masterKeyOptions, { @@ -817,14 +833,17 @@ describe('Parse.Query Aggregate testing', () => { }); }); - it_id('3a1e2cdc-52c7-4060-bc90-b06d557d85ce')(it_exclude_dbs(['postgres']))('match exists query', done => { - const pipeline = [{ $match: { score: { $exists: true } } }]; - const query = new Parse.Query(TestObject); - query.aggregate(pipeline).then(results => { - expect(results.length).toEqual(4); - done(); - }); - }); + it_id('3a1e2cdc-52c7-4060-bc90-b06d557d85ce')(it_exclude_dbs(['postgres']))( + 'match exists query', + done => { + const pipeline = [{ $match: { score: { $exists: true } } }]; + const query = new Parse.Query(TestObject); + query.aggregate(pipeline).then(results => { + expect(results.length).toEqual(4); + done(); + }); + } + ); it_id('0adea3f4-73f7-4b48-a7dd-c764ceb947ec')(it)('match date query - createdAt', done => { const obj1 = new TestObject(); @@ -882,78 +901,84 @@ describe('Parse.Query Aggregate testing', () => { }); }); - it_id('802ffc99-861b-4b72-90a6-0c666a2e3fd8')(it_exclude_dbs(['postgres']))('match pointer with operator query', done => { - const pointer = new PointerObject(); + it_id('802ffc99-861b-4b72-90a6-0c666a2e3fd8')(it_exclude_dbs(['postgres']))( + 'match pointer with operator query', + done => { + const pointer = new PointerObject(); - const obj1 = new TestObject({ pointer }); - const obj2 = new TestObject({ pointer }); - const obj3 = new TestObject(); + const obj1 = new TestObject({ pointer }); + const obj2 = new TestObject({ pointer }); + const obj3 = new TestObject(); - Parse.Object.saveAll([pointer, obj1, obj2, obj3]) - .then(() => { - const pipeline = [{ $match: { pointer: { $exists: true } } }]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }) - .then(results => { - expect(results.length).toEqual(2); - expect(results[0].pointer.objectId).toEqual(pointer.id); - expect(results[1].pointer.objectId).toEqual(pointer.id); - expect(results.some(result => result.objectId === obj1.id)).toEqual(true); - expect(results.some(result => result.objectId === obj2.id)).toEqual(true); - done(); - }); - }); + Parse.Object.saveAll([pointer, obj1, obj2, obj3]) + .then(() => { + const pipeline = [{ $match: { pointer: { $exists: true } } }]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(2); + expect(results[0].pointer.objectId).toEqual(pointer.id); + expect(results[1].pointer.objectId).toEqual(pointer.id); + expect(results.some(result => result.objectId === obj1.id)).toEqual(true); + expect(results.some(result => result.objectId === obj2.id)).toEqual(true); + done(); + }); + } + ); - it_id('28090280-7c3e-47f8-8bf6-bebf8566a36c')(it_exclude_dbs(['postgres']))('match null values', async () => { - const obj1 = new Parse.Object('MyCollection'); - obj1.set('language', 'en'); - obj1.set('otherField', 1); - const obj2 = new Parse.Object('MyCollection'); - obj2.set('language', 'en'); - obj2.set('otherField', 2); - const obj3 = new Parse.Object('MyCollection'); - obj3.set('language', null); - obj3.set('otherField', 3); - const obj4 = new Parse.Object('MyCollection'); - obj4.set('language', null); - obj4.set('otherField', 4); - const obj5 = new Parse.Object('MyCollection'); - obj5.set('language', 'pt'); - obj5.set('otherField', 5); - const obj6 = new Parse.Object('MyCollection'); - obj6.set('language', 'pt'); - obj6.set('otherField', 6); - await Parse.Object.saveAll([obj1, obj2, obj3, obj4, obj5, obj6]); - - expect( - ( - await new Parse.Query('MyCollection').aggregate([ - { - $match: { - language: { $in: [null, 'en'] }, + it_id('28090280-7c3e-47f8-8bf6-bebf8566a36c')(it_exclude_dbs(['postgres']))( + 'match null values', + async () => { + const obj1 = new Parse.Object('MyCollection'); + obj1.set('language', 'en'); + obj1.set('otherField', 1); + const obj2 = new Parse.Object('MyCollection'); + obj2.set('language', 'en'); + obj2.set('otherField', 2); + const obj3 = new Parse.Object('MyCollection'); + obj3.set('language', null); + obj3.set('otherField', 3); + const obj4 = new Parse.Object('MyCollection'); + obj4.set('language', null); + obj4.set('otherField', 4); + const obj5 = new Parse.Object('MyCollection'); + obj5.set('language', 'pt'); + obj5.set('otherField', 5); + const obj6 = new Parse.Object('MyCollection'); + obj6.set('language', 'pt'); + obj6.set('otherField', 6); + await Parse.Object.saveAll([obj1, obj2, obj3, obj4, obj5, obj6]); + + expect( + ( + await new Parse.Query('MyCollection').aggregate([ + { + $match: { + language: { $in: [null, 'en'] }, + }, }, - }, - ]) - ) - .map(value => value.otherField) - .sort() - ).toEqual([1, 2, 3, 4]); - - expect( - ( - await new Parse.Query('MyCollection').aggregate([ - { - $match: { - $or: [{ language: 'en' }, { language: null }], + ]) + ) + .map(value => value.otherField) + .sort() + ).toEqual([1, 2, 3, 4]); + + expect( + ( + await new Parse.Query('MyCollection').aggregate([ + { + $match: { + $or: [{ language: 'en' }, { language: null }], + }, }, - }, - ]) - ) - .map(value => value.otherField) - .sort() - ).toEqual([1, 2, 3, 4]); - }); + ]) + ) + .map(value => value.otherField) + .sort() + ).toEqual([1, 2, 3, 4]); + } + ); it_id('df63d1f5-7c37-4ed9-8bc5-20d82f29f509')(it)('project query', done => { const options = Object.assign({}, masterKeyOptions, { @@ -1153,34 +1178,40 @@ describe('Parse.Query Aggregate testing', () => { }); }); - it_id('91e6cb94-2837-44b7-b057-0c4965057caa')(it)('distinct class does not exist return empty', done => { - const options = Object.assign({}, masterKeyOptions, { - body: { distinct: 'unknown' }, - }); - get(Parse.serverURL + '/aggregate/UnknownClass', options) - .then(resp => { - expect(resp.results.length).toBe(0); - done(); - }) - .catch(done.fail); - }); + it_id('91e6cb94-2837-44b7-b057-0c4965057caa')(it)( + 'distinct class does not exist return empty', + done => { + const options = Object.assign({}, masterKeyOptions, { + body: { distinct: 'unknown' }, + }); + get(Parse.serverURL + '/aggregate/UnknownClass', options) + .then(resp => { + expect(resp.results.length).toBe(0); + done(); + }) + .catch(done.fail); + } + ); - it_id('bd15daaf-8dc7-458c-81e2-170026f4a8a7')(it)('distinct field does not exist return empty', done => { - const options = Object.assign({}, masterKeyOptions, { - body: { distinct: 'unknown' }, - }); - const obj = new TestObject(); - obj - .save() - .then(() => { - return get(Parse.serverURL + '/aggregate/TestObject', options); - }) - .then(resp => { - expect(resp.results.length).toBe(0); - done(); - }) - .catch(done.fail); - }); + it_id('bd15daaf-8dc7-458c-81e2-170026f4a8a7')(it)( + 'distinct field does not exist return empty', + done => { + const options = Object.assign({}, masterKeyOptions, { + body: { distinct: 'unknown' }, + }); + const obj = new TestObject(); + obj + .save() + .then(() => { + return get(Parse.serverURL + '/aggregate/TestObject', options); + }) + .then(resp => { + expect(resp.results.length).toBe(0); + done(); + }) + .catch(done.fail); + } + ); it_id('21988fce-8326-425f-82f0-cd444ca3671b')(it)('distinct array', done => { const options = Object.assign({}, masterKeyOptions, { @@ -1256,108 +1287,114 @@ describe('Parse.Query Aggregate testing', () => { .catch(done.fail); }); - it_id('d9c19419-e99d-4d9f-b7f3-418e49ee47dd')(it)('does not return sensitive hidden properties', done => { - const options = Object.assign({}, masterKeyOptions, { - body: { - $match: { - score: { - $gt: 5, + it_id('d9c19419-e99d-4d9f-b7f3-418e49ee47dd')(it)( + 'does not return sensitive hidden properties', + done => { + const options = Object.assign({}, masterKeyOptions, { + body: { + $match: { + score: { + $gt: 5, + }, }, }, - }, - }); - - const username = 'leaky_user'; - const score = 10; - - const user = new Parse.User(); - user.setUsername(username); - user.setPassword('password'); - user.set('score', score); - user - .signUp() - .then(function () { - return get(Parse.serverURL + '/aggregate/_User', options); - }) - .then(function (resp) { - expect(resp.results.length).toBe(1); - const result = resp.results[0]; - - // verify server-side keys are not present... - expect(result._hashed_password).toBe(undefined); - expect(result._wperm).toBe(undefined); - expect(result._rperm).toBe(undefined); - expect(result._acl).toBe(undefined); - expect(result._created_at).toBe(undefined); - expect(result._updated_at).toBe(undefined); - - // verify createdAt, updatedAt and others are present - expect(result.createdAt).not.toBe(undefined); - expect(result.updatedAt).not.toBe(undefined); - expect(result.objectId).not.toBe(undefined); - expect(result.username).toBe(username); - expect(result.score).toBe(score); - - done(); - }) - .catch(function (err) { - fail(err); }); - }); - it_id('0a23e791-e9b5-457a-9bf9-9c5ecf406f42')(it_exclude_dbs(['postgres']))('aggregate allow multiple of same stage', async done => { - await reconfigureServer({ silent: false }); - const pointer1 = new TestObject({ value: 1 }); - const pointer2 = new TestObject({ value: 2 }); - const pointer3 = new TestObject({ value: 3 }); + const username = 'leaky_user'; + const score = 10; + + const user = new Parse.User(); + user.setUsername(username); + user.setPassword('password'); + user.set('score', score); + user + .signUp() + .then(function () { + return get(Parse.serverURL + '/aggregate/_User', options); + }) + .then(function (resp) { + expect(resp.results.length).toBe(1); + const result = resp.results[0]; + + // verify server-side keys are not present... + expect(result._hashed_password).toBe(undefined); + expect(result._wperm).toBe(undefined); + expect(result._rperm).toBe(undefined); + expect(result._acl).toBe(undefined); + expect(result._created_at).toBe(undefined); + expect(result._updated_at).toBe(undefined); + + // verify createdAt, updatedAt and others are present + expect(result.createdAt).not.toBe(undefined); + expect(result.updatedAt).not.toBe(undefined); + expect(result.objectId).not.toBe(undefined); + expect(result.username).toBe(username); + expect(result.score).toBe(score); - const obj1 = new TestObject({ pointer: pointer1, name: 'Hello' }); - const obj2 = new TestObject({ pointer: pointer2, name: 'Hello' }); - const obj3 = new TestObject({ pointer: pointer3, name: 'World' }); + done(); + }) + .catch(function (err) { + fail(err); + }); + } + ); - const options = Object.assign({}, masterKeyOptions, { - body: { - pipeline: [ - { - $match: { name: 'Hello' }, - }, - { - // Transform className$objectId to objectId and store in new field tempPointer - $project: { - tempPointer: { $substr: ['$_p_pointer', 11, -1] }, // Remove TestObject$ + it_id('0a23e791-e9b5-457a-9bf9-9c5ecf406f42')(it_exclude_dbs(['postgres']))( + 'aggregate allow multiple of same stage', + async done => { + await reconfigureServer({ silent: false }); + const pointer1 = new TestObject({ value: 1 }); + const pointer2 = new TestObject({ value: 2 }); + const pointer3 = new TestObject({ value: 3 }); + + const obj1 = new TestObject({ pointer: pointer1, name: 'Hello' }); + const obj2 = new TestObject({ pointer: pointer2, name: 'Hello' }); + const obj3 = new TestObject({ pointer: pointer3, name: 'World' }); + + const options = Object.assign({}, masterKeyOptions, { + body: { + pipeline: [ + { + $match: { name: 'Hello' }, }, - }, - { - // Left Join, replace objectId stored in tempPointer with an actual object - $lookup: { - from: 'test_TestObject', - localField: 'tempPointer', - foreignField: '_id', - as: 'tempPointer', + { + // Transform className$objectId to objectId and store in new field tempPointer + $project: { + tempPointer: { $substr: ['$_p_pointer', 11, -1] }, // Remove TestObject$ + }, }, - }, - { - // lookup returns an array, Deconstructs an array field to objects - $unwind: { - path: '$tempPointer', + { + // Left Join, replace objectId stored in tempPointer with an actual object + $lookup: { + from: 'test_TestObject', + localField: 'tempPointer', + foreignField: '_id', + as: 'tempPointer', + }, }, - }, - { - $match: { 'tempPointer.value': 2 }, - }, - ], - }, - }); - Parse.Object.saveAll([pointer1, pointer2, pointer3, obj1, obj2, obj3]) - .then(() => { - return get(Parse.serverURL + '/aggregate/TestObject', options); - }) - .then(resp => { - expect(resp.results.length).toEqual(1); - expect(resp.results[0].tempPointer.value).toEqual(2); - done(); + { + // lookup returns an array, Deconstructs an array field to objects + $unwind: { + path: '$tempPointer', + }, + }, + { + $match: { 'tempPointer.value': 2 }, + }, + ], + }, }); - }); + Parse.Object.saveAll([pointer1, pointer2, pointer3, obj1, obj2, obj3]) + .then(() => { + return get(Parse.serverURL + '/aggregate/TestObject', options); + }) + .then(resp => { + expect(resp.results.length).toEqual(1); + expect(resp.results[0].tempPointer.value).toEqual(2); + done(); + }); + } + ); it_only_db('mongo')('aggregate geoNear with location query', async () => { // Create geo index which is required for `geoNear` query diff --git a/spec/ParseQuery.FullTextSearch.spec.js b/spec/ParseQuery.FullTextSearch.spec.js index 11760ec161..e32fab5de2 100644 --- a/spec/ParseQuery.FullTextSearch.spec.js +++ b/spec/ParseQuery.FullTextSearch.spec.js @@ -76,70 +76,88 @@ describe('Parse.Query Full Text Search testing', () => { expect(resp.length).toBe(2); }); - it_id('7d3da216-9582-40ee-a2fe-8316feaf5c0c')(it)('fullTextSearch: $diacriticSensitive', async () => { - await fullTextHelper(); - const query = new Parse.Query('TestObject'); - query.fullText('subject', 'CAFÉ', { diacriticSensitive: true }); - const resp = await query.find(); - expect(resp.length).toBe(1); - }); + it_id('7d3da216-9582-40ee-a2fe-8316feaf5c0c')(it)( + 'fullTextSearch: $diacriticSensitive', + async () => { + await fullTextHelper(); + const query = new Parse.Query('TestObject'); + query.fullText('subject', 'CAFÉ', { diacriticSensitive: true }); + const resp = await query.find(); + expect(resp.length).toBe(1); + } + ); - it_id('dade10c8-2b9c-4f43-bb3f-a13bbd82ac22')(it)('fullTextSearch: $search, invalid input', async () => { - await fullTextHelper(); - const invalidQuery = async () => { - const where = { - subject: { - $text: { - $search: true, + it_id('dade10c8-2b9c-4f43-bb3f-a13bbd82ac22')(it)( + 'fullTextSearch: $search, invalid input', + async () => { + await fullTextHelper(); + const invalidQuery = async () => { + const where = { + subject: { + $text: { + $search: true, + }, }, - }, + }; + try { + await request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + body: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + 'Content-Type': 'application/json', + }, + }); + } catch (e) { + throw new Parse.Error(e.data.code, e.data.error); + } }; - try { - await request({ - method: 'POST', - url: 'http://localhost:8378/1/classes/TestObject', - body: { where, _method: 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test', - 'Content-Type': 'application/json', - }, - }); - } catch (e) { - throw new Parse.Error(e.data.code, e.data.error); - } - }; - await expectAsync(invalidQuery()).toBeRejectedWith( - new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $search, should be object') - ); - }); + await expectAsync(invalidQuery()).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $search, should be object') + ); + } + ); - it_id('ff7c6b1c-4712-4847-bb76-f4e1f641f7b5')(it)('fullTextSearch: $language, invalid input', async () => { - await fullTextHelper(); - const query = new Parse.Query('TestObject'); - query.fullText('subject', 'leche', { language: true }); - await expectAsync(query.find()).toBeRejectedWith( - new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $language, should be string') - ); - }); + it_id('ff7c6b1c-4712-4847-bb76-f4e1f641f7b5')(it)( + 'fullTextSearch: $language, invalid input', + async () => { + await fullTextHelper(); + const query = new Parse.Query('TestObject'); + query.fullText('subject', 'leche', { language: true }); + await expectAsync(query.find()).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $language, should be string') + ); + } + ); - it_id('de262dbc-ec75-4ec6-9217-fbb90146c272')(it)('fullTextSearch: $caseSensitive, invalid input', async () => { - await fullTextHelper(); - const query = new Parse.Query('TestObject'); - query.fullText('subject', 'leche', { caseSensitive: 'string' }); - await expectAsync(query.find()).toBeRejectedWith( - new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $caseSensitive, should be boolean') - ); - }); + it_id('de262dbc-ec75-4ec6-9217-fbb90146c272')(it)( + 'fullTextSearch: $caseSensitive, invalid input', + async () => { + await fullTextHelper(); + const query = new Parse.Query('TestObject'); + query.fullText('subject', 'leche', { caseSensitive: 'string' }); + await expectAsync(query.find()).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $caseSensitive, should be boolean') + ); + } + ); - it_id('b7b7b3a9-8d6c-4f98-a0ff-0113593d06d4')(it)('fullTextSearch: $diacriticSensitive, invalid input', async () => { - await fullTextHelper(); - const query = new Parse.Query('TestObject'); - query.fullText('subject', 'leche', { diacriticSensitive: 'string' }); - await expectAsync(query.find()).toBeRejectedWith( - new Parse.Error(Parse.Error.INVALID_JSON, 'bad $text: $diacriticSensitive, should be boolean') - ); - }); + it_id('b7b7b3a9-8d6c-4f98-a0ff-0113593d06d4')(it)( + 'fullTextSearch: $diacriticSensitive, invalid input', + async () => { + await fullTextHelper(); + const query = new Parse.Query('TestObject'); + query.fullText('subject', 'leche', { diacriticSensitive: 'string' }); + await expectAsync(query.find()).toBeRejectedWith( + new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $text: $diacriticSensitive, should be boolean' + ) + ); + } + ); }); describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () => { diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index e6f3b1e08a..2faeee1387 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -6,7 +6,8 @@ const Parse = require('parse/node'); const request = require('../lib/request'); -const ParseServerRESTController = require('../lib/ParseServerRESTController').ParseServerRESTController; +const ParseServerRESTController = + require('../lib/ParseServerRESTController').ParseServerRESTController; const ParseServer = require('../lib/ParseServer').default; const masterKeyHeaders = { @@ -592,40 +593,43 @@ describe('Parse.Query testing', () => { }); }); - it_id('25bb35a6-e953-4d6d-a31c-66324d5ae076')(it)('containsAll object array queries', function (done) { - const MessageSet = Parse.Object.extend({ className: 'MessageSet' }); + it_id('25bb35a6-e953-4d6d-a31c-66324d5ae076')(it)( + 'containsAll object array queries', + function (done) { + const MessageSet = Parse.Object.extend({ className: 'MessageSet' }); - const messageList = []; - for (let i = 0; i < 4; ++i) { - messageList.push(new TestObject({ i: i })); - } + const messageList = []; + for (let i = 0; i < 4; ++i) { + messageList.push(new TestObject({ i: i })); + } - Parse.Object.saveAll(messageList).then(function () { - equal(messageList.length, 4); + Parse.Object.saveAll(messageList).then(function () { + equal(messageList.length, 4); - const messageSetList = []; - messageSetList.push(new MessageSet({ messages: messageList })); + const messageSetList = []; + messageSetList.push(new MessageSet({ messages: messageList })); - const someList = []; - someList.push(messageList[0]); - someList.push(messageList[1]); - someList.push(messageList[3]); - messageSetList.push(new MessageSet({ messages: someList })); + const someList = []; + someList.push(messageList[0]); + someList.push(messageList[1]); + someList.push(messageList[3]); + messageSetList.push(new MessageSet({ messages: someList })); - Parse.Object.saveAll(messageSetList).then(function () { - const inList = []; - inList.push(messageList[0]); - inList.push(messageList[2]); + Parse.Object.saveAll(messageSetList).then(function () { + const inList = []; + inList.push(messageList[0]); + inList.push(messageList[2]); - const query = new Parse.Query(MessageSet); - query.containsAll('messages', inList); - query.find().then(function (results) { - equal(results.length, 1); - done(); + const query = new Parse.Query(MessageSet); + query.containsAll('messages', inList); + query.find().then(function (results) { + equal(results.length, 1); + done(); + }); }); }); - }); - }); + } + ); it('containsAllStartingWith should match all strings that starts with string', done => { const object = new Parse.Object('Object'); @@ -707,40 +711,43 @@ describe('Parse.Query testing', () => { }); }); - it_id('3ea6ae04-bcc2-453d-8817-4c64d059c2f6')(it)('containsAllStartingWith values must be all of type starting with regex', done => { - const object = new Parse.Object('Object'); - object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + it_id('3ea6ae04-bcc2-453d-8817-4c64d059c2f6')(it)( + 'containsAllStartingWith values must be all of type starting with regex', + done => { + const object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - object - .save() - .then(() => { - equal(object.isNew(), false); + object + .save() + .then(() => { + equal(object.isNew(), false); - return request({ - url: Parse.serverURL + '/classes/Object', - qs: { - where: JSON.stringify({ - strings: { - $all: [ - { $regex: '^\\Qthe\\E' }, - { $regex: '^\\Qlazy\\E' }, - { $regex: '^\\Qfox\\E' }, - { $unknown: /unknown/ }, - ], - }, - }), - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey, - 'Content-Type': 'application/json', - }, + return request({ + url: Parse.serverURL + '/classes/Object', + qs: { + where: JSON.stringify({ + strings: { + $all: [ + { $regex: '^\\Qthe\\E' }, + { $regex: '^\\Qlazy\\E' }, + { $regex: '^\\Qfox\\E' }, + { $unknown: /unknown/ }, + ], + }, + }), + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + 'Content-Type': 'application/json', + }, + }); + }) + .then(done.fail, function () { + done(); }); - }) - .then(done.fail, function () { - done(); - }); - }); + } + ); it('containsAllStartingWith empty array values should return empty results', done => { const object = new Parse.Object('Object'); @@ -1670,47 +1677,53 @@ describe('Parse.Query testing', () => { .catch(done.fail); }); - it_id('65c8238d-cf02-49d0-a919-8a17f5a58280')(it)('can order on an object number field', function (done) { - const testSet = [ - { sortField: { value: 10 } }, - { sortField: { value: 1 } }, - { sortField: { value: 5 } }, - ]; - - const objects = testSet.map(e => new Parse.Object('Test', e)); - Parse.Object.saveAll(objects) - .then(() => new Parse.Query('Test').addDescending('sortField.value').first()) - .then(result => { - expect(result.get('sortField').value).toBe(10); - return new Parse.Query('Test').addAscending('sortField.value').first(); - }) - .then(result => { - expect(result.get('sortField').value).toBe(1); - done(); - }) - .catch(done.fail); - }); - - it_id('d8f0bead-b931-4d66-8b0c-28c5705e463c')(it)('can order on an object number field (level 2)', function (done) { - const testSet = [ - { sortField: { value: { field: 10 } } }, - { sortField: { value: { field: 1 } } }, - { sortField: { value: { field: 5 } } }, - ]; - - const objects = testSet.map(e => new Parse.Object('Test', e)); - Parse.Object.saveAll(objects) - .then(() => new Parse.Query('Test').addDescending('sortField.value.field').first()) - .then(result => { - expect(result.get('sortField').value.field).toBe(10); - return new Parse.Query('Test').addAscending('sortField.value.field').first(); - }) - .then(result => { - expect(result.get('sortField').value.field).toBe(1); - done(); - }) - .catch(done.fail); - }); + it_id('65c8238d-cf02-49d0-a919-8a17f5a58280')(it)( + 'can order on an object number field', + function (done) { + const testSet = [ + { sortField: { value: 10 } }, + { sortField: { value: 1 } }, + { sortField: { value: 5 } }, + ]; + + const objects = testSet.map(e => new Parse.Object('Test', e)); + Parse.Object.saveAll(objects) + .then(() => new Parse.Query('Test').addDescending('sortField.value').first()) + .then(result => { + expect(result.get('sortField').value).toBe(10); + return new Parse.Query('Test').addAscending('sortField.value').first(); + }) + .then(result => { + expect(result.get('sortField').value).toBe(1); + done(); + }) + .catch(done.fail); + } + ); + + it_id('d8f0bead-b931-4d66-8b0c-28c5705e463c')(it)( + 'can order on an object number field (level 2)', + function (done) { + const testSet = [ + { sortField: { value: { field: 10 } } }, + { sortField: { value: { field: 1 } } }, + { sortField: { value: { field: 5 } } }, + ]; + + const objects = testSet.map(e => new Parse.Object('Test', e)); + Parse.Object.saveAll(objects) + .then(() => new Parse.Query('Test').addDescending('sortField.value.field').first()) + .then(result => { + expect(result.get('sortField').value.field).toBe(10); + return new Parse.Query('Test').addAscending('sortField.value.field').first(); + }) + .then(result => { + expect(result.get('sortField').value.field).toBe(1); + done(); + }) + .catch(done.fail); + } + ); it('order by ascending number then descending string', function (done) { const strings = ['a', 'b', 'c', 'd']; @@ -2113,30 +2126,33 @@ describe('Parse.Query testing', () => { .then(done); }); - it_id('823852f6-1de5-45ba-a2b9-ed952fcc6012')(it)('Use a regex that requires all modifiers', function (done) { - const thing = new TestObject(); - thing.set('myString', 'PArSe\nCom'); - Parse.Object.saveAll([thing]).then(function () { - const query = new Parse.Query(TestObject); - query.matches( - 'myString', - "parse # First fragment. We'll write this in one case but match insensitively\n" + - '.com # Second fragment. This can be separated by any character, including newline;' + - 'however, this comment must end with a newline to recognize it as a comment\n', - 'mixs' - ); - query.find().then( - function (results) { - equal(results.length, 1); - done(); - }, - function (err) { - jfail(err); - done(); - } - ); - }); - }); + it_id('823852f6-1de5-45ba-a2b9-ed952fcc6012')(it)( + 'Use a regex that requires all modifiers', + function (done) { + const thing = new TestObject(); + thing.set('myString', 'PArSe\nCom'); + Parse.Object.saveAll([thing]).then(function () { + const query = new Parse.Query(TestObject); + query.matches( + 'myString', + "parse # First fragment. We'll write this in one case but match insensitively\n" + + '.com # Second fragment. This can be separated by any character, including newline;' + + 'however, this comment must end with a newline to recognize it as a comment\n', + 'mixs' + ); + query.find().then( + function (results) { + equal(results.length, 1); + done(); + }, + function (err) { + jfail(err); + done(); + } + ); + }); + } + ); it('Regular expression constructor includes modifiers inline', function (done) { const thing = new TestObject(); @@ -4005,51 +4021,54 @@ describe('Parse.Query testing', () => { ); }); - it_id('7079f0ef-47b3-4a1e-aac0-32654dadaa27')(it)('should properly interpret a query v2', done => { - const user = new Parse.User(); - user.set('username', 'foo'); - user.set('password', 'bar'); - return user - .save() - .then(user => { - const objIdQuery = new Parse.Query('_User').equalTo('objectId', user.id); - const blockedUserQuery = user.relation('blockedUsers').query(); - - const aResponseQuery = new Parse.Query('MatchRelationshipActivityResponse'); - aResponseQuery.equalTo('userA', user); - aResponseQuery.equalTo('userAResponse', 1); - - const bResponseQuery = new Parse.Query('MatchRelationshipActivityResponse'); - bResponseQuery.equalTo('userB', user); - bResponseQuery.equalTo('userBResponse', 1); - - const matchOr = Parse.Query.or(aResponseQuery, bResponseQuery); - const matchRelationshipA = new Parse.Query('_User'); - matchRelationshipA.matchesKeyInQuery('objectId', 'userAObjectId', matchOr); - const matchRelationshipB = new Parse.Query('_User'); - matchRelationshipB.matchesKeyInQuery('objectId', 'userBObjectId', matchOr); - - const orQuery = Parse.Query.or( - objIdQuery, - blockedUserQuery, - matchRelationshipA, - matchRelationshipB + it_id('7079f0ef-47b3-4a1e-aac0-32654dadaa27')(it)( + 'should properly interpret a query v2', + done => { + const user = new Parse.User(); + user.set('username', 'foo'); + user.set('password', 'bar'); + return user + .save() + .then(user => { + const objIdQuery = new Parse.Query('_User').equalTo('objectId', user.id); + const blockedUserQuery = user.relation('blockedUsers').query(); + + const aResponseQuery = new Parse.Query('MatchRelationshipActivityResponse'); + aResponseQuery.equalTo('userA', user); + aResponseQuery.equalTo('userAResponse', 1); + + const bResponseQuery = new Parse.Query('MatchRelationshipActivityResponse'); + bResponseQuery.equalTo('userB', user); + bResponseQuery.equalTo('userBResponse', 1); + + const matchOr = Parse.Query.or(aResponseQuery, bResponseQuery); + const matchRelationshipA = new Parse.Query('_User'); + matchRelationshipA.matchesKeyInQuery('objectId', 'userAObjectId', matchOr); + const matchRelationshipB = new Parse.Query('_User'); + matchRelationshipB.matchesKeyInQuery('objectId', 'userBObjectId', matchOr); + + const orQuery = Parse.Query.or( + objIdQuery, + blockedUserQuery, + matchRelationshipA, + matchRelationshipB + ); + const query = new Parse.Query('_User'); + query.doesNotMatchQuery('objectId', orQuery); + return query.find(); + }) + .then( + () => { + done(); + }, + err => { + jfail(err); + fail('should not fail'); + done(); + } ); - const query = new Parse.Query('_User'); - query.doesNotMatchQuery('objectId', orQuery); - return query.find(); - }) - .then( - () => { - done(); - }, - err => { - jfail(err); - fail('should not fail'); - done(); - } - ); - }); + } + ); it('should match a key in an array (#3195)', function (done) { const AuthorObject = Parse.Object.extend('Author'); @@ -4084,48 +4103,51 @@ describe('Parse.Query testing', () => { }); }); - it_id('d95818c0-9e3c-41e6-be20-e7bafb59eefb')(it)('should find objects with array of pointers', done => { - const objects = []; - while (objects.length != 5) { - const object = new Parse.Object('ContainedObject'); - object.set('index', objects.length); - objects.push(object); - } + it_id('d95818c0-9e3c-41e6-be20-e7bafb59eefb')(it)( + 'should find objects with array of pointers', + done => { + const objects = []; + while (objects.length != 5) { + const object = new Parse.Object('ContainedObject'); + object.set('index', objects.length); + objects.push(object); + } - Parse.Object.saveAll(objects) - .then(objects => { - const container = new Parse.Object('Container'); - const pointers = objects.map(obj => { - return { - __type: 'Pointer', - className: 'ContainedObject', - objectId: obj.id, - }; + Parse.Object.saveAll(objects) + .then(objects => { + const container = new Parse.Object('Container'); + const pointers = objects.map(obj => { + return { + __type: 'Pointer', + className: 'ContainedObject', + objectId: obj.id, + }; + }); + container.set('objects', pointers); + const container2 = new Parse.Object('Container'); + container2.set('objects', pointers.slice(2, 3)); + return Parse.Object.saveAll([container, container2]); + }) + .then(() => { + const inQuery = new Parse.Query('ContainedObject'); + inQuery.greaterThanOrEqualTo('index', 1); + const query = new Parse.Query('Container'); + query.matchesQuery('objects', inQuery); + return query.find(); + }) + .then(results => { + if (results) { + expect(results.length).toBe(2); + } + done(); + }) + .catch(err => { + jfail(err); + fail('should not fail'); + done(); }); - container.set('objects', pointers); - const container2 = new Parse.Object('Container'); - container2.set('objects', pointers.slice(2, 3)); - return Parse.Object.saveAll([container, container2]); - }) - .then(() => { - const inQuery = new Parse.Query('ContainedObject'); - inQuery.greaterThanOrEqualTo('index', 1); - const query = new Parse.Query('Container'); - query.matchesQuery('objects', inQuery); - return query.find(); - }) - .then(results => { - if (results) { - expect(results.length).toBe(2); - } - done(); - }) - .catch(err => { - jfail(err); - fail('should not fail'); - done(); - }); - }); + } + ); it('query with two OR subqueries (regression test #1259)', done => { const relatedObject = new Parse.Object('Class2'); @@ -5015,48 +5037,51 @@ describe('Parse.Query testing', () => { equal(results[0].get('name'), group2.get('name')); }); - it_id('8886b994-fbb8-487d-a863-43bbd2b24b73')(it)('withJSON supports geoWithin.centerSphere', done => { - const inbound = new Parse.GeoPoint(1.5, 1.5); - const onbound = new Parse.GeoPoint(10, 10); - const outbound = new Parse.GeoPoint(20, 20); - const obj1 = new Parse.Object('TestObject', { location: inbound }); - const obj2 = new Parse.Object('TestObject', { location: onbound }); - const obj3 = new Parse.Object('TestObject', { location: outbound }); - const center = new Parse.GeoPoint(0, 0); - const distanceInKilometers = 1569 + 1; // 1569km is the approximate distance between {0, 0} and {10, 10}. - Parse.Object.saveAll([obj1, obj2, obj3]) - .then(() => { - const q = new Parse.Query(TestObject); - const jsonQ = q.toJSON(); - jsonQ.where.location = { - $geoWithin: { - $centerSphere: [center, distanceInKilometers / 6371.0], - }, - }; - q.withJSON(jsonQ); - return q.find(); - }) - .then(results => { - equal(results.length, 2); - const q = new Parse.Query(TestObject); - const jsonQ = q.toJSON(); - jsonQ.where.location = { - $geoWithin: { - $centerSphere: [[0, 0], distanceInKilometers / 6371.0], - }, - }; - q.withJSON(jsonQ); - return q.find(); - }) - .then(results => { - equal(results.length, 2); - done(); - }) - .catch(error => { - fail(error); - done(); - }); - }); + it_id('8886b994-fbb8-487d-a863-43bbd2b24b73')(it)( + 'withJSON supports geoWithin.centerSphere', + done => { + const inbound = new Parse.GeoPoint(1.5, 1.5); + const onbound = new Parse.GeoPoint(10, 10); + const outbound = new Parse.GeoPoint(20, 20); + const obj1 = new Parse.Object('TestObject', { location: inbound }); + const obj2 = new Parse.Object('TestObject', { location: onbound }); + const obj3 = new Parse.Object('TestObject', { location: outbound }); + const center = new Parse.GeoPoint(0, 0); + const distanceInKilometers = 1569 + 1; // 1569km is the approximate distance between {0, 0} and {10, 10}. + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + $geoWithin: { + $centerSphere: [center, distanceInKilometers / 6371.0], + }, + }; + q.withJSON(jsonQ); + return q.find(); + }) + .then(results => { + equal(results.length, 2); + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + $geoWithin: { + $centerSphere: [[0, 0], distanceInKilometers / 6371.0], + }, + }; + q.withJSON(jsonQ); + return q.find(); + }) + .then(results => { + equal(results.length, 2); + done(); + }) + .catch(error => { + fail(error); + done(); + }); + } + ); it('withJSON with geoWithin.centerSphere fails without parameters', done => { const q = new Parse.Query(TestObject); @@ -5116,37 +5141,40 @@ describe('Parse.Query testing', () => { .catch(() => done()); }); - it_id('02d4e7e6-859a-4ab6-878d-135ccc77040e')(it)('can add new config to existing config', async () => { - await request({ - method: 'PUT', - url: 'http://localhost:8378/1/config', - json: true, - body: { - params: { - files: [{ __type: 'File', name: 'name', url: 'http://url' }], + it_id('02d4e7e6-859a-4ab6-878d-135ccc77040e')(it)( + 'can add new config to existing config', + async () => { + await request({ + method: 'PUT', + url: 'http://localhost:8378/1/config', + json: true, + body: { + params: { + files: [{ __type: 'File', name: 'name', url: 'http://url' }], + }, }, - }, - headers: masterKeyHeaders, - }); + headers: masterKeyHeaders, + }); - await request({ - method: 'PUT', - url: 'http://localhost:8378/1/config', - json: true, - body: { - params: { newConfig: 'good' }, - }, - headers: masterKeyHeaders, - }); + await request({ + method: 'PUT', + url: 'http://localhost:8378/1/config', + json: true, + body: { + params: { newConfig: 'good' }, + }, + headers: masterKeyHeaders, + }); - const result = await Parse.Config.get(); - equal(result.get('files')[0].toJSON(), { - __type: 'File', - name: 'name', - url: 'http://url', - }); - equal(result.get('newConfig'), 'good'); - }); + const result = await Parse.Config.get(); + equal(result.get('files')[0].toJSON(), { + __type: 'File', + name: 'name', + url: 'http://url', + }); + equal(result.get('newConfig'), 'good'); + } + ); it('can set object type key', async () => { const data = { bar: true, baz: 100 }; @@ -5284,7 +5312,10 @@ describe('Parse.Query testing', () => { }); Parse.CoreManager.setRESTController( - ParseServerRESTController(Parse.applicationId, ParseServer.promiseRouter({ appId: Parse.applicationId })) + ParseServerRESTController( + Parse.applicationId, + ParseServer.promiseRouter({ appId: Parse.applicationId }) + ) ); const user = new Parse.User(); @@ -5297,13 +5328,14 @@ describe('Parse.Query testing', () => { score.set('score', 1); await score.save(); - await new Parse.Query('_User') - .equalTo('objectId', user.id) - .eachBatch(async ([user]) => { + await new Parse.Query('_User').equalTo('objectId', user.id).eachBatch( + async ([user]) => { const score = await new Parse.Query('Score') .equalTo('player', user) .distinct('score', { useMasterKey: true }); expect(score).toEqual([1]); - }, { useMasterKey: true }); + }, + { useMasterKey: true } + ); }); }); diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 35a91c6c15..0a9eb6df92 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -126,76 +126,79 @@ describe('Parse Role testing', () => { ); }); - it_id('b03abe32-e8e4-4666-9b81-9c804aa53400')(it)('should not recursively load the same role multiple times', done => { - const rootRole = 'RootRole'; - const roleNames = ['FooRole', 'BarRole', 'BazRole']; - const allRoles = [rootRole].concat(roleNames); - - const roleObjs = {}; - const createAllRoles = function (user) { - const promises = allRoles.map(function (roleName) { - return createRole(roleName, null, user).then(function (roleObj) { - roleObjs[roleName] = roleObj; - return roleObj; + it_id('b03abe32-e8e4-4666-9b81-9c804aa53400')(it)( + 'should not recursively load the same role multiple times', + done => { + const rootRole = 'RootRole'; + const roleNames = ['FooRole', 'BarRole', 'BazRole']; + const allRoles = [rootRole].concat(roleNames); + + const roleObjs = {}; + const createAllRoles = function (user) { + const promises = allRoles.map(function (roleName) { + return createRole(roleName, null, user).then(function (roleObj) { + roleObjs[roleName] = roleObj; + return roleObj; + }); }); - }); - return Promise.all(promises); - }; + return Promise.all(promises); + }; - const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough(); + const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough(); - let user, auth, getAllRolesSpy; - createTestUser() - .then(newUser => { - user = newUser; - return createAllRoles(user); - }) - .then(roles => { - const rootRoleObj = roleObjs[rootRole]; - roles.forEach(function (role, i) { - // Add all roles to the RootRole - if (role.id !== rootRoleObj.id) { - role.relation('roles').add(rootRoleObj); - } - // Add all "roleNames" roles to the previous role - if (i > 0) { - role.relation('roles').add(roles[i - 1]); - } - }); + let user, auth, getAllRolesSpy; + createTestUser() + .then(newUser => { + user = newUser; + return createAllRoles(user); + }) + .then(roles => { + const rootRoleObj = roleObjs[rootRole]; + roles.forEach(function (role, i) { + // Add all roles to the RootRole + if (role.id !== rootRoleObj.id) { + role.relation('roles').add(rootRoleObj); + } + // Add all "roleNames" roles to the previous role + if (i > 0) { + role.relation('roles').add(roles[i - 1]); + } + }); - return Parse.Object.saveAll(roles, { useMasterKey: true }); - }) - .then(() => { - auth = new Auth({ - config: Config.get('test'), - isMaster: true, - user: user, - }); - getAllRolesSpy = spyOn(auth, '_getAllRolesNamesForRoleIds').and.callThrough(); + return Parse.Object.saveAll(roles, { useMasterKey: true }); + }) + .then(() => { + auth = new Auth({ + config: Config.get('test'), + isMaster: true, + user: user, + }); + getAllRolesSpy = spyOn(auth, '_getAllRolesNamesForRoleIds').and.callThrough(); - return auth._loadRoles(); - }) - .then(roles => { - expect(roles.length).toEqual(4); + return auth._loadRoles(); + }) + .then(roles => { + expect(roles.length).toEqual(4); - allRoles.forEach(function (name) { - expect(roles.indexOf('role:' + name)).not.toBe(-1); - }); + allRoles.forEach(function (name) { + expect(roles.indexOf('role:' + name)).not.toBe(-1); + }); - // 1 Query for the initial setup - // 1 query for the parent roles - expect(restExecute.calls.count()).toEqual(2); + // 1 Query for the initial setup + // 1 query for the parent roles + expect(restExecute.calls.count()).toEqual(2); - // 1 call for the 1st layer of roles - // 1 call for the 2nd layer - expect(getAllRolesSpy.calls.count()).toEqual(2); - done(); - }) - .catch(() => { - fail('should succeed'); - done(); - }); - }); + // 1 call for the 1st layer of roles + // 1 call for the 2nd layer + expect(getAllRolesSpy.calls.count()).toEqual(2); + done(); + }) + .catch(() => { + fail('should succeed'); + done(); + }); + } + ); it('should recursively load roles', done => { testLoadRoles(Config.get('test'), done); diff --git a/spec/ParseServer.spec.js b/spec/ParseServer.spec.js index b55c6fc548..7d67aaa320 100644 --- a/spec/ParseServer.spec.js +++ b/spec/ParseServer.spec.js @@ -2,8 +2,8 @@ /* Tests for ParseServer.js */ const express = require('express'); const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') - .default; +const PostgresStorageAdapter = + require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; const ParseServer = require('../lib/ParseServer').default; const path = require('path'); const { spawn } = require('child_process'); diff --git a/spec/ParseServerRESTController.spec.js b/spec/ParseServerRESTController.spec.js index fbc244ab87..e881086faa 100644 --- a/spec/ParseServerRESTController.spec.js +++ b/spec/ParseServerRESTController.spec.js @@ -1,5 +1,5 @@ -const ParseServerRESTController = require('../lib/ParseServerRESTController') - .ParseServerRESTController; +const ParseServerRESTController = + require('../lib/ParseServerRESTController').ParseServerRESTController; const ParseServer = require('../lib/ParseServer').default; const Parse = require('parse/node').Parse; @@ -163,9 +163,7 @@ describe('ParseServerRESTController', () => { const results = await query.find(); expect(createSpy.calls.count()).toBe(2); for (let i = 0; i + 1 < createSpy.calls.length; i = i + 2) { - expect(createSpy.calls.argsFor(i)[3]).toBe( - createSpy.calls.argsFor(i + 1)[3] - ); + expect(createSpy.calls.argsFor(i)[3]).toBe(createSpy.calls.argsFor(i + 1)[3]); } expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); }); @@ -660,16 +658,23 @@ describe('ParseServerRESTController', () => { data: { alert: 'We return status!' }, where: { deviceType: 'ios' }, }; - const res = await RESTController.request('POST', 'batch', { - requests: [{ - method: 'POST', - path: '/push', - body: payload, - }], - }, { - useMasterKey: true, - returnStatus: true, - }); + const res = await RESTController.request( + 'POST', + 'batch', + { + requests: [ + { + method: 'POST', + path: '/push', + body: payload, + }, + ], + }, + { + useMasterKey: true, + returnStatus: true, + } + ); const pushStatusId = res[0]._headers['X-Parse-Push-Status-Id']; expect(pushStatusId).toBeDefined(); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index ba34fbf6e9..e43e018e05 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2257,68 +2257,80 @@ describe('Parse.User testing', () => { }); describe('case insensitive signup not allowed', () => { - it_id('464eddc2-7a46-413d-888e-b43b040f1511')(it)('signup should fail with duplicate case insensitive username with basic setter', async () => { - const user = new Parse.User(); - user.set('username', 'test1'); - user.set('password', 'test'); - await user.signUp(); - - const user2 = new Parse.User(); - user2.set('username', 'Test1'); - user2.set('password', 'test'); - await expectAsync(user2.signUp()).toBeRejectedWith( - new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.') - ); - }); + it_id('464eddc2-7a46-413d-888e-b43b040f1511')(it)( + 'signup should fail with duplicate case insensitive username with basic setter', + async () => { + const user = new Parse.User(); + user.set('username', 'test1'); + user.set('password', 'test'); + await user.signUp(); - it_id('1cef005b-d5f0-4699-af0c-bb0af27d2437')(it)('signup should fail with duplicate case insensitive username with field specific setter', async () => { - const user = new Parse.User(); - user.setUsername('test1'); - user.setPassword('test'); - await user.signUp(); + const user2 = new Parse.User(); + user2.set('username', 'Test1'); + user2.set('password', 'test'); + await expectAsync(user2.signUp()).toBeRejectedWith( + new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.') + ); + } + ); - const user2 = new Parse.User(); - user2.setUsername('Test1'); - user2.setPassword('test'); - await expectAsync(user2.signUp()).toBeRejectedWith( - new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.') - ); - }); + it_id('1cef005b-d5f0-4699-af0c-bb0af27d2437')(it)( + 'signup should fail with duplicate case insensitive username with field specific setter', + async () => { + const user = new Parse.User(); + user.setUsername('test1'); + user.setPassword('test'); + await user.signUp(); - it_id('12735529-98d1-42c0-b437-3b47fe78ddde')(it)('signup should fail with duplicate case insensitive email', async () => { - const user = new Parse.User(); - user.setUsername('test1'); - user.setPassword('test'); - user.setEmail('test@example.com'); - await user.signUp(); + const user2 = new Parse.User(); + user2.setUsername('Test1'); + user2.setPassword('test'); + await expectAsync(user2.signUp()).toBeRejectedWith( + new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.') + ); + } + ); - const user2 = new Parse.User(); - user2.setUsername('test2'); - user2.setPassword('test'); - user2.setEmail('Test@Example.Com'); - await expectAsync(user2.signUp()).toBeRejectedWith( - new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.') - ); - }); + it_id('12735529-98d1-42c0-b437-3b47fe78ddde')(it)( + 'signup should fail with duplicate case insensitive email', + async () => { + const user = new Parse.User(); + user.setUsername('test1'); + user.setPassword('test'); + user.setEmail('test@example.com'); + await user.signUp(); - it_id('66e51d52-2420-4b62-8a0d-c7e1b384763e')(it)('edit should fail with duplicate case insensitive email', async () => { - const user = new Parse.User(); - user.setUsername('test1'); - user.setPassword('test'); - user.setEmail('test@example.com'); - await user.signUp(); + const user2 = new Parse.User(); + user2.setUsername('test2'); + user2.setPassword('test'); + user2.setEmail('Test@Example.Com'); + await expectAsync(user2.signUp()).toBeRejectedWith( + new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.') + ); + } + ); - const user2 = new Parse.User(); - user2.setUsername('test2'); - user2.setPassword('test'); - user2.setEmail('Foo@Example.Com'); - await user2.signUp(); + it_id('66e51d52-2420-4b62-8a0d-c7e1b384763e')(it)( + 'edit should fail with duplicate case insensitive email', + async () => { + const user = new Parse.User(); + user.setUsername('test1'); + user.setPassword('test'); + user.setEmail('test@example.com'); + await user.signUp(); - user2.setEmail('Test@Example.Com'); - await expectAsync(user2.save()).toBeRejectedWith( - new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.') - ); - }); + const user2 = new Parse.User(); + user2.setUsername('test2'); + user2.setPassword('test'); + user2.setEmail('Foo@Example.Com'); + await user2.signUp(); + + user2.setEmail('Test@Example.Com'); + await expectAsync(user2.save()).toBeRejectedWith( + new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.') + ); + } + ); describe('anonymous users', () => { it('should not fail on case insensitive matches', async () => { @@ -2952,119 +2964,125 @@ describe('Parse.User testing', () => { }); }); - it_id('1be98368-19ac-4c77-8531-762a114f43fb')(it)('should send email when upgrading from anon', async done => { - await reconfigureServer(); - let emailCalled = false; - let emailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - emailOptions = options; - emailCalled = true; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve(), - }; - await reconfigureServer({ - appName: 'unused', - verifyUserEmails: true, - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }); - // Simulate anonymous user save - return request({ - method: 'POST', - url: 'http://localhost:8378/1/classes/_User', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - 'Content-Type': 'application/json', - }, - body: { - authData: { - anonymous: { id: '00000000-0000-0000-0000-000000000001' }, + it_id('1be98368-19ac-4c77-8531-762a114f43fb')(it)( + 'should send email when upgrading from anon', + async done => { + await reconfigureServer(); + let emailCalled = false; + let emailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + emailOptions = options; + emailCalled = true; }, - }, - }) - .then(response => { - const user = response.data; - return request({ - method: 'PUT', - url: 'http://localhost:8378/1/classes/_User/' + user.objectId, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Session-Token': user.sessionToken, - 'X-Parse-REST-API-Key': 'rest', - 'Content-Type': 'application/json', - }, - body: { - authData: { anonymous: null }, - username: 'user', - email: 'user@email.com', - password: 'password', + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => Promise.resolve(), + }; + await reconfigureServer({ + appName: 'unused', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + // Simulate anonymous user save + return request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/_User', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + authData: { + anonymous: { id: '00000000-0000-0000-0000-000000000001' }, }, - }); - }) - .then(() => jasmine.timeout()) - .then(() => { - expect(emailCalled).toBe(true); - expect(emailOptions).not.toBeUndefined(); - expect(emailOptions.user.get('email')).toEqual('user@email.com'); - done(); + }, }) - .catch(err => { - jfail(err); - fail('no request should fail: ' + JSON.stringify(err)); - done(); - }); - }); + .then(response => { + const user = response.data; + return request({ + method: 'PUT', + url: 'http://localhost:8378/1/classes/_User/' + user.objectId, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Session-Token': user.sessionToken, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + authData: { anonymous: null }, + username: 'user', + email: 'user@email.com', + password: 'password', + }, + }); + }) + .then(() => jasmine.timeout()) + .then(() => { + expect(emailCalled).toBe(true); + expect(emailOptions).not.toBeUndefined(); + expect(emailOptions.user.get('email')).toEqual('user@email.com'); + done(); + }) + .catch(err => { + jfail(err); + fail('no request should fail: ' + JSON.stringify(err)); + done(); + }); + } + ); - it_id('bf668670-39fa-44d3-a9a9-cad52f36d272')(it)('should not send email when email is not a string', async done => { - let emailCalled = false; - let emailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - emailOptions = options; - emailCalled = true; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve(), - }; - await reconfigureServer({ - appName: 'unused', - verifyUserEmails: true, - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }); - const user = new Parse.User(); - user.set('username', 'asdf@jkl.com'); - user.set('password', 'zxcv'); - user.set('email', 'asdf@jkl.com'); - await user.signUp(); - request({ - method: 'POST', - url: 'http://localhost:8378/1/requestPasswordReset', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Session-Token': user.sessionToken, - 'X-Parse-REST-API-Key': 'rest', - 'Content-Type': 'application/json', - }, - body: { - email: { $regex: '^asd' }, - }, - }) - .then(res => { - fail('no request should succeed: ' + JSON.stringify(res)); - done(); - }) - .catch(err => { - expect(emailCalled).toBeTruthy(); - expect(emailOptions).toBeDefined(); - expect(err.status).toBe(400); - expect(err.text).toMatch('{"code":125,"error":"you must provide a valid email string"}'); - done(); + it_id('bf668670-39fa-44d3-a9a9-cad52f36d272')(it)( + 'should not send email when email is not a string', + async done => { + let emailCalled = false; + let emailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + emailOptions = options; + emailCalled = true; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => Promise.resolve(), + }; + await reconfigureServer({ + appName: 'unused', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', }); - }); + const user = new Parse.User(); + user.set('username', 'asdf@jkl.com'); + user.set('password', 'zxcv'); + user.set('email', 'asdf@jkl.com'); + await user.signUp(); + request({ + method: 'POST', + url: 'http://localhost:8378/1/requestPasswordReset', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Session-Token': user.sessionToken, + 'X-Parse-REST-API-Key': 'rest', + 'Content-Type': 'application/json', + }, + body: { + email: { $regex: '^asd' }, + }, + }) + .then(res => { + fail('no request should succeed: ' + JSON.stringify(res)); + done(); + }) + .catch(err => { + expect(emailCalled).toBeTruthy(); + expect(emailOptions).toBeDefined(); + expect(err.status).toBe(400); + expect(err.text).toMatch('{"code":125,"error":"you must provide a valid email string"}'); + done(); + }); + } + ); it('should aftersave with full object', done => { let hit = 0; diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index 1fd2e6aa50..b8ad67c391 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -3,65 +3,68 @@ const request = require('../lib/request'); describe('Password Policy: ', () => { - it_id('b400a867-9f05-496f-af79-933aa588dde5')(it)('should show the invalid link page if the user clicks on the password reset link after the token expires', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: options => { - sendEmailOptions = options; - }, - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'passwordPolicy', - emailAdapter: emailAdapter, - passwordPolicy: { - resetTokenValidityDuration: 0.5, // 0.5 second - }, - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setUsername('testResetTokenValidity'); - user.setPassword('original'); - user.set('email', 'user@parse.com'); - return user.signUp(); + it_id('b400a867-9f05-496f-af79-933aa588dde5')(it)( + 'should show the invalid link page if the user clicks on the password reset link after the token expires', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + sendEmailOptions = options; + }, + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'passwordPolicy', + emailAdapter: emailAdapter, + passwordPolicy: { + resetTokenValidityDuration: 0.5, // 0.5 second + }, + publicServerURL: 'http://localhost:8378/1', }) - .then(() => { - Parse.User.requestPasswordReset('user@parse.com').catch(err => { + .then(() => { + user.setUsername('testResetTokenValidity'); + user.setPassword('original'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + Parse.User.requestPasswordReset('user@parse.com').catch(err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + }); + }) + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + request({ + url: sendEmailOptions.link, + followRedirects: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' + ); + done(); + }) + .catch(error => { + fail(error); + }); + }, 1000); + }) + .catch(err => { jfail(err); - fail('Reset password request should not fail'); done(); }); - }) - .then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - expect(sendEmailOptions).not.toBeUndefined(); - - request({ - url: sendEmailOptions.link, - followRedirects: false, - simple: false, - resolveWithFullResponse: true, - }) - .then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' - ); - done(); - }) - .catch(error => { - fail(error); - }); - }, 1000); - }) - .catch(err => { - jfail(err); - done(); - }); - }); + } + ); it('should show the reset password page if the user clicks on the password reset link before the token expires', done => { const user = new Parse.User(); @@ -107,7 +110,8 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/; expect(response.text.match(re)).not.toBe(null); done(); }) @@ -150,34 +154,37 @@ describe('Password Policy: ', () => { done(); }); - it_id('7d98e1f2-ae89-4038-9ea7-5254854ea42e')(it)('should keep reset token with resetTokenReuseIfValid', async done => { - const sendEmailOptions = []; - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: options => { - sendEmailOptions.push(options); - }, - sendMail: () => {}, - }; - await reconfigureServer({ - appName: 'passwordPolicy', - emailAdapter: emailAdapter, - passwordPolicy: { - resetTokenValidityDuration: 5 * 60, // 5 minutes - resetTokenReuseIfValid: true, - }, - publicServerURL: 'http://localhost:8378/1', - }); - const user = new Parse.User(); - user.setUsername('testResetTokenValidity'); - user.setPassword('original'); - user.set('email', 'user@example.com'); - await user.signUp(); - await Parse.User.requestPasswordReset('user@example.com'); - await Parse.User.requestPasswordReset('user@example.com'); - expect(sendEmailOptions[0].link).toBe(sendEmailOptions[1].link); - done(); - }); + it_id('7d98e1f2-ae89-4038-9ea7-5254854ea42e')(it)( + 'should keep reset token with resetTokenReuseIfValid', + async done => { + const sendEmailOptions = []; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + sendEmailOptions.push(options); + }, + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'passwordPolicy', + emailAdapter: emailAdapter, + passwordPolicy: { + resetTokenValidityDuration: 5 * 60, // 5 minutes + resetTokenReuseIfValid: true, + }, + publicServerURL: 'http://localhost:8378/1', + }); + const user = new Parse.User(); + user.setUsername('testResetTokenValidity'); + user.setPassword('original'); + user.set('email', 'user@example.com'); + await user.signUp(); + await Parse.User.requestPasswordReset('user@example.com'); + await Parse.User.requestPasswordReset('user@example.com'); + expect(sendEmailOptions[0].link).toBe(sendEmailOptions[1].link); + done(); + } + ); it('should throw with invalid resetTokenReuseIfValid', async done => { const sendEmailOptions = []; @@ -622,7 +629,8 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -714,7 +722,8 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -900,7 +909,8 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -991,7 +1001,8 @@ describe('Password Policy: ', () => { resolveWithFullResponse: true, }); expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1051,7 +1062,8 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1164,248 +1176,261 @@ describe('Password Policy: ', () => { }); }); - it_id('d7d0a93e-efe6-48c0-b622-0f7fb570ccc1')(it)('should succeed if logged in before password expires', done => { - const user = new Parse.User(); - reconfigureServer({ - appName: 'passwordPolicy', - passwordPolicy: { - maxPasswordAge: 1, // 1 day - }, - publicServerURL: 'http://localhost:8378/1', - }).then(() => { - user.setUsername('user1'); - user.setPassword('user1'); - user.set('email', 'user1@parse.com'); - user - .signUp() - .then(() => { - Parse.User.logIn('user1', 'user1') - .then(() => { - done(); - }) - .catch(error => { - jfail(error); - fail('Login should have succeeded before password expiry.'); - done(); - }); - }) - .catch(error => { - jfail(error); - fail('Signup failed.'); - done(); - }); - }); - }); - - it_id('22428408-8763-445d-9833-2b2d92008f62')(it)('should fail if logged in after password expires', done => { - const user = new Parse.User(); - reconfigureServer({ - appName: 'passwordPolicy', - passwordPolicy: { - maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec - }, - publicServerURL: 'http://localhost:8378/1', - }).then(() => { - user.setUsername('user1'); - user.setPassword('user1'); - user.set('email', 'user1@parse.com'); - user - .signUp() - .then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { + it_id('d7d0a93e-efe6-48c0-b622-0f7fb570ccc1')(it)( + 'should succeed if logged in before password expires', + done => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + maxPasswordAge: 1, // 1 day + }, + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + user.setUsername('user1'); + user.setPassword('user1'); + user.set('email', 'user1@parse.com'); + user + .signUp() + .then(() => { Parse.User.logIn('user1', 'user1') .then(() => { - fail('logIn should have failed'); done(); }) .catch(error => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - expect(error.message).toEqual( - 'Your password has expired. Please reset your password.' - ); + jfail(error); + fail('Login should have succeeded before password expiry.'); done(); }); - }, 1000); - }) - .catch(error => { - jfail(error); - fail('Signup failed.'); - done(); - }); - }); - }); - - it_id('cc97a109-e35f-4f94-b942-3a6134921cdd')(it)('should apply password expiry policy to existing user upon first login after policy is enabled', done => { - const user = new Parse.User(); - reconfigureServer({ - appName: 'passwordPolicy', - publicServerURL: 'http://localhost:8378/1', - }).then(() => { - user.setUsername('user1'); - user.setPassword('user1'); - user.set('email', 'user1@parse.com'); - user - .signUp() - .then(() => { - Parse.User.logOut() - .then(() => { - reconfigureServer({ - appName: 'passwordPolicy', - passwordPolicy: { - maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec - }, - publicServerURL: 'http://localhost:8378/1', - }).then(() => { - Parse.User.logIn('user1', 'user1') - .then(() => { - Parse.User.logOut() - .then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - Parse.User.logIn('user1', 'user1') - .then(() => { - fail('logIn should have failed'); - done(); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - expect(error.message).toEqual( - 'Your password has expired. Please reset your password.' - ); - done(); - }); - }, 2000); - }) - .catch(error => { - jfail(error); - fail('logout should have succeeded'); - done(); - }); - }) - .catch(error => { - jfail(error); - fail('Login failed.'); - done(); - }); - }); - }) - .catch(error => { - jfail(error); - fail('logout should have succeeded'); - done(); - }); - }) - .catch(error => { - jfail(error); - fail('Signup failed.'); - done(); - }); - }); - }); - - it_id('d1e6ab9d-c091-4fea-b952-08b7484bfc89')(it)('should reset password timestamp when password is reset', done => { - const user = new Parse.User(); - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: options => { - request({ - url: options.link, - followRedirects: false, - simple: false, - resolveWithFullResponse: true, - }) - .then(response => { - expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; - const match = response.text.match(re); - if (!match) { - fail('should have a token'); - done(); - return; - } - const token = match[1]; + }) + .catch(error => { + jfail(error); + fail('Signup failed.'); + done(); + }); + }); + } + ); - request({ - method: 'POST', - url: 'http://localhost:8378/1/apps/test/request_password_reset', - body: `new_password=uuser11&token=${token}`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - followRedirects: false, - simple: false, - resolveWithFullResponse: true, - }) - .then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' - ); + it_id('22428408-8763-445d-9833-2b2d92008f62')(it)( + 'should fail if logged in after password expires', + done => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec + }, + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + user.setUsername('user1'); + user.setPassword('user1'); + user.set('email', 'user1@parse.com'); + user + .signUp() + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + fail('logIn should have failed'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + expect(error.message).toEqual( + 'Your password has expired. Please reset your password.' + ); + done(); + }); + }, 1000); + }) + .catch(error => { + jfail(error); + fail('Signup failed.'); + done(); + }); + }); + } + ); - Parse.User.logIn('user1', 'uuser11') - .then(function () { - done(); - }) - .catch(err => { - jfail(err); - fail('should login with new password'); - done(); - }); + it_id('cc97a109-e35f-4f94-b942-3a6134921cdd')(it)( + 'should apply password expiry policy to existing user upon first login after policy is enabled', + done => { + const user = new Parse.User(); + reconfigureServer({ + appName: 'passwordPolicy', + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + user.setUsername('user1'); + user.setPassword('user1'); + user.set('email', 'user1@parse.com'); + user + .signUp() + .then(() => { + Parse.User.logOut() + .then(() => { + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec + }, + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + Parse.User.logOut() + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + fail('logIn should have failed'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + expect(error.message).toEqual( + 'Your password has expired. Please reset your password.' + ); + done(); + }); + }, 2000); + }) + .catch(error => { + jfail(error); + fail('logout should have succeeded'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Login failed.'); + done(); + }); + }); }) .catch(error => { jfail(error); - fail('Failed to POST request password reset'); + fail('logout should have succeeded'); + done(); }); }) .catch(error => { jfail(error); - fail('Failed to get the reset link'); + fail('Signup failed.'); + done(); }); - }, - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'passwordPolicy', - emailAdapter: emailAdapter, - passwordPolicy: { - maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec - }, - publicServerURL: 'http://localhost:8378/1', - }).then(() => { - user.setUsername('user1'); - user.setPassword('user1'); - user.set('email', 'user1@parse.com'); - user - .signUp() - .then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - Parse.User.logIn('user1', 'user1') - .then(() => { - fail('logIn should have failed'); + }); + } + ); + + it_id('d1e6ab9d-c091-4fea-b952-08b7484bfc89')(it)( + 'should reset password timestamp when password is reset', + done => { + const user = new Parse.User(); + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: options => { + request({ + url: options.link, + followRedirects: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.status).toEqual(302); + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const match = response.text.match(re); + if (!match) { + fail('should have a token'); done(); + return; + } + const token = match[1]; + + request({ + method: 'POST', + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=uuser11&token=${token}`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirects: false, + simple: false, + resolveWithFullResponse: true, }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - expect(error.message).toEqual( - 'Your password has expired. Please reset your password.' - ); - Parse.User.requestPasswordReset('user1@parse.com').catch(err => { - jfail(err); - fail('Reset password request should not fail'); + .then(response => { + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html' + ); + + Parse.User.logIn('user1', 'uuser11') + .then(function () { + done(); + }) + .catch(err => { + jfail(err); + fail('should login with new password'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Failed to POST request password reset'); + }); + }) + .catch(error => { + jfail(error); + fail('Failed to get the reset link'); + }); + }, + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'passwordPolicy', + emailAdapter: emailAdapter, + passwordPolicy: { + maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec + }, + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + user.setUsername('user1'); + user.setPassword('user1'); + user.set('email', 'user1@parse.com'); + user + .signUp() + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + fail('logIn should have failed'); done(); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + expect(error.message).toEqual( + 'Your password has expired. Please reset your password.' + ); + Parse.User.requestPasswordReset('user1@parse.com').catch(err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + }); }); - }); - }, 1000); - }) - .catch(error => { - jfail(error); - fail('Signup failed.'); - done(); - }); - }); - }); + }, 1000); + }) + .catch(error => { + jfail(error); + fail('Signup failed.'); + done(); + }); + }); + } + ); it('should fail if passwordPolicy.maxPasswordHistory is not a number', done => { reconfigureServer({ @@ -1472,7 +1497,8 @@ describe('Password Policy: ', () => { }) .then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); diff --git a/spec/PointerPermissions.spec.js b/spec/PointerPermissions.spec.js index a4cf43899d..94257681c5 100644 --- a/spec/PointerPermissions.spec.js +++ b/spec/PointerPermissions.spec.js @@ -226,59 +226,62 @@ describe('Pointer Permissions', () => { }); }); - it_id('f38c35e7-d804-4d32-986d-2579e25d2461')(it)('should query on pointer permission enabled column', done => { - const config = Config.get(Parse.applicationId); - const user = new Parse.User(); - const user2 = new Parse.User(); - user.set({ - username: 'user1', - password: 'password', - }); - user2.set({ - username: 'user2', - password: 'password', - }); - const obj = new Parse.Object('AnObject'); - const obj2 = new Parse.Object('AnObject'); - user - .signUp() - .then(() => { - return user2.signUp(); - }) - .then(() => { - Parse.User.logOut(); - }) - .then(() => { - obj.set('owner', user); - return Parse.Object.saveAll([obj, obj2]); - }) - .then(() => { - return config.database.loadSchema().then(schema => { - return schema.updateClass( - 'AnObject', - {}, - { find: {}, get: {}, readUserFields: ['owner'] } - ); + it_id('f38c35e7-d804-4d32-986d-2579e25d2461')(it)( + 'should query on pointer permission enabled column', + done => { + const config = Config.get(Parse.applicationId); + const user = new Parse.User(); + const user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password', + }); + user2.set({ + username: 'user2', + password: 'password', + }); + const obj = new Parse.Object('AnObject'); + const obj2 = new Parse.Object('AnObject'); + user + .signUp() + .then(() => { + return user2.signUp(); + }) + .then(() => { + Parse.User.logOut(); + }) + .then(() => { + obj.set('owner', user); + return Parse.Object.saveAll([obj, obj2]); + }) + .then(() => { + return config.database.loadSchema().then(schema => { + return schema.updateClass( + 'AnObject', + {}, + { find: {}, get: {}, readUserFields: ['owner'] } + ); + }); + }) + .then(() => { + return Parse.User.logIn('user1', 'password'); + }) + .then(() => { + const q = new Parse.Query('AnObject'); + q.equalTo('owner', user2); + return q.find(); + }) + .then(res => { + expect(res.length).toBe(0); + done(); + }) + .catch(err => { + jfail(err); + fail('should not fail'); + done(); }); - }) - .then(() => { - return Parse.User.logIn('user1', 'password'); - }) - .then(() => { - const q = new Parse.Query('AnObject'); - q.equalTo('owner', user2); - return q.find(); - }) - .then(res => { - expect(res.length).toBe(0); - done(); - }) - .catch(err => { - jfail(err); - fail('should not fail'); - done(); - }); - }); + } + ); it('should not allow creating objects', done => { const config = Config.get(Parse.applicationId); @@ -1203,50 +1206,53 @@ describe('Pointer Permissions', () => { done(); }); - it_id('8a7d188c-b75c-4eac-90b6-9b0b11f873ae')(it)('should query on pointer permission enabled column', async done => { - const config = Config.get(Parse.applicationId); - const user = new Parse.User(); - const user2 = new Parse.User(); - const user3 = new Parse.User(); - user.set({ - username: 'user1', - password: 'password', - }); - user2.set({ - username: 'user2', - password: 'password', - }); - user3.set({ - username: 'user3', - password: 'password', - }); - const obj = new Parse.Object('AnObject'); - const obj2 = new Parse.Object('AnObject'); + it_id('8a7d188c-b75c-4eac-90b6-9b0b11f873ae')(it)( + 'should query on pointer permission enabled column', + async done => { + const config = Config.get(Parse.applicationId); + const user = new Parse.User(); + const user2 = new Parse.User(); + const user3 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password', + }); + user2.set({ + username: 'user2', + password: 'password', + }); + user3.set({ + username: 'user3', + password: 'password', + }); + const obj = new Parse.Object('AnObject'); + const obj2 = new Parse.Object('AnObject'); - await user.signUp(); - await user2.signUp(); - await user3.signUp(); - await Parse.User.logOut(); + await user.signUp(); + await user2.signUp(); + await user3.signUp(); + await Parse.User.logOut(); - obj.set('owners', [user, user2]); - await Parse.Object.saveAll([obj, obj2]); + obj.set('owners', [user, user2]); + await Parse.Object.saveAll([obj, obj2]); - const schema = await config.database.loadSchema(); - await schema.updateClass('AnObject', {}, { find: {}, get: {}, readUserFields: ['owners'] }); + const schema = await config.database.loadSchema(); + await schema.updateClass('AnObject', {}, { find: {}, get: {}, readUserFields: ['owners'] }); - for (const owner of ['user1', 'user2']) { - await Parse.User.logIn(owner, 'password'); - try { - const q = new Parse.Query('AnObject'); - q.equalTo('owners', user3); - const result = await q.find(); - expect(result.length).toBe(0); - } catch (err) { - done.fail('should not fail'); + for (const owner of ['user1', 'user2']) { + await Parse.User.logIn(owner, 'password'); + try { + const q = new Parse.Query('AnObject'); + q.equalTo('owners', user3); + const result = await q.find(); + expect(result.length).toBe(0); + } catch (err) { + done.fail('should not fail'); + } } + done(); } - done(); - }); + ); it('should not query using arrays on pointer permission enabled column', async done => { const config = Config.get(Parse.applicationId); @@ -2058,18 +2064,21 @@ describe('Pointer Permissions', () => { done(); }); - it_id('9ba681d5-59f5-4996-b36d-6647d23e6a44')(it)('should fail for user not listed', async done => { - await updateCLP({ - get: { - pointerFields: ['owner'], - }, - }); + it_id('9ba681d5-59f5-4996-b36d-6647d23e6a44')(it)( + 'should fail for user not listed', + async done => { + await updateCLP({ + get: { + pointerFields: ['owner'], + }, + }); - await logIn(user2); + await logIn(user2); - await expectAsync(actionGet(obj1.id)).toBeRejectedWith(OBJECT_NOT_FOUND); - done(); - }); + await expectAsync(actionGet(obj1.id)).toBeRejectedWith(OBJECT_NOT_FOUND); + done(); + } + ); it('should not allow other actions', async done => { await updateCLP({ @@ -2218,18 +2227,21 @@ describe('Pointer Permissions', () => { done(); }); - it_id('bcdb158d-c0b6-45e3-84ab-a3636f7cb470')(it)('should fail for user not listed', async done => { - await updateCLP({ - update: { - pointerFields: ['owner'], - }, - }); + it_id('bcdb158d-c0b6-45e3-84ab-a3636f7cb470')(it)( + 'should fail for user not listed', + async done => { + await updateCLP({ + update: { + pointerFields: ['owner'], + }, + }); - await logIn(user2); + await logIn(user2); - await expectAsync(actionUpdate(obj1)).toBeRejectedWith(OBJECT_NOT_FOUND); - done(); - }); + await expectAsync(actionUpdate(obj1)).toBeRejectedWith(OBJECT_NOT_FOUND); + done(); + } + ); it('should not allow other actions', async done => { await updateCLP({ @@ -2270,18 +2282,21 @@ describe('Pointer Permissions', () => { done(); }); - it_id('70aa3853-6e26-4c38-a927-2ddb24ced7d4')(it)('should fail for user not listed', async done => { - await updateCLP({ - delete: { - pointerFields: ['owner'], - }, - }); + it_id('70aa3853-6e26-4c38-a927-2ddb24ced7d4')(it)( + 'should fail for user not listed', + async done => { + await updateCLP({ + delete: { + pointerFields: ['owner'], + }, + }); - await logIn(user2); + await logIn(user2); - await expectAsync(actionDelete(obj1)).toBeRejectedWith(OBJECT_NOT_FOUND); - done(); - }); + await expectAsync(actionDelete(obj1)).toBeRejectedWith(OBJECT_NOT_FOUND); + done(); + } + ); it('should not allow other actions', async done => { await updateCLP({ @@ -2517,18 +2532,21 @@ describe('Pointer Permissions', () => { done(); }); - it_id('84a42339-c7b5-4735-a431-57b46535b073')(it)('should fail for user not listed', async done => { - await updateCLP({ - get: { - pointerFields: ['moderators'], - }, - }); + it_id('84a42339-c7b5-4735-a431-57b46535b073')(it)( + 'should fail for user not listed', + async done => { + await updateCLP({ + get: { + pointerFields: ['moderators'], + }, + }); - await logIn(user1); + await logIn(user1); - await expectAsync(actionGet(obj3.id)).toBeRejectedWith(OBJECT_NOT_FOUND); - done(); - }); + await expectAsync(actionGet(obj3.id)).toBeRejectedWith(OBJECT_NOT_FOUND); + done(); + } + ); it('should not allow other actions', async done => { await updateCLP({ @@ -2685,31 +2703,37 @@ describe('Pointer Permissions', () => { done(); }); - it_id('2b19234a-a471-48b4-bd1a-27bd286d066f')(it)('should be allowed (multiple users in array)', async done => { - await updateCLP({ - update: { - pointerFields: ['moderators'], - }, - }); + it_id('2b19234a-a471-48b4-bd1a-27bd286d066f')(it)( + 'should be allowed (multiple users in array)', + async done => { + await updateCLP({ + update: { + pointerFields: ['moderators'], + }, + }); - await logIn(user2); + await logIn(user2); - await expectAsync(actionUpdate(obj1)).toBeResolved(); - done(); - }); + await expectAsync(actionUpdate(obj1)).toBeResolved(); + done(); + } + ); - it_id('1abb9f4a-fb24-48c7-8025-3001d6cf8737')(it)('should fail for user not listed', async done => { - await updateCLP({ - update: { - pointerFields: ['moderators'], - }, - }); + it_id('1abb9f4a-fb24-48c7-8025-3001d6cf8737')(it)( + 'should fail for user not listed', + async done => { + await updateCLP({ + update: { + pointerFields: ['moderators'], + }, + }); - await logIn(user2); + await logIn(user2); - await expectAsync(actionUpdate(obj3)).toBeRejectedWith(OBJECT_NOT_FOUND); - done(); - }); + await expectAsync(actionUpdate(obj3)).toBeRejectedWith(OBJECT_NOT_FOUND); + done(); + } + ); it('should not allow other actions', async done => { await updateCLP({ @@ -2764,18 +2788,21 @@ describe('Pointer Permissions', () => { done(); }); - it_id('3175a0e3-e51e-4b84-a2e6-50bbcc582123')(it)('should fail for user not listed', async done => { - await updateCLP({ - delete: { - pointerFields: ['owners'], - }, - }); + it_id('3175a0e3-e51e-4b84-a2e6-50bbcc582123')(it)( + 'should fail for user not listed', + async done => { + await updateCLP({ + delete: { + pointerFields: ['owners'], + }, + }); - await logIn(user1); + await logIn(user1); - await expectAsync(actionDelete(obj3)).toBeRejectedWith(OBJECT_NOT_FOUND); - done(); - }); + await expectAsync(actionDelete(obj3)).toBeRejectedWith(OBJECT_NOT_FOUND); + done(); + } + ); it('should not allow other actions', async done => { await updateCLP({ @@ -2874,22 +2901,25 @@ describe('Pointer Permissions', () => { done(); }); - it_id('51e896e9-73b3-404f-b5ff-bdb99005a9f7')(it)('should be restricted when updating object without addField permission', async done => { - await updateCLP({ - update: { - '*': true, - }, - addField: { - pointerFields: ['moderators'], - }, - }); + it_id('51e896e9-73b3-404f-b5ff-bdb99005a9f7')(it)( + 'should be restricted when updating object without addField permission', + async done => { + await updateCLP({ + update: { + '*': true, + }, + addField: { + pointerFields: ['moderators'], + }, + }); - await logIn(user1); + await logIn(user1); - await expectAsync(actionAddFieldOnUpdate(obj2)).toBeRejectedWith(OBJECT_NOT_FOUND); + await expectAsync(actionAddFieldOnUpdate(obj2)).toBeRejectedWith(OBJECT_NOT_FOUND); - done(); - }); + done(); + } + ); }); }); @@ -2946,44 +2976,50 @@ describe('Pointer Permissions', () => { await initialize(); }); - it_id('b43db366-8cce-4a11-9cf2-eeee9603d40b')(it)('should not limit the scope of grouped read permissions', async done => { - await updateCLP({ - get: { - pointerFields: ['owner'], - }, - readUserFields: ['moderators'], - }); + it_id('b43db366-8cce-4a11-9cf2-eeee9603d40b')(it)( + 'should not limit the scope of grouped read permissions', + async done => { + await updateCLP({ + get: { + pointerFields: ['owner'], + }, + readUserFields: ['moderators'], + }); - await logIn(user2); + await logIn(user2); - await expectAsync(actionGet(obj1.id)).toBeResolved(); + await expectAsync(actionGet(obj1.id)).toBeResolved(); - const found = await actionFind(); - expect(found.length).toBe(2); + const found = await actionFind(); + expect(found.length).toBe(2); - const counted = await actionCount(); - expect(counted).toBe(2); + const counted = await actionCount(); + expect(counted).toBe(2); - done(); - }); + done(); + } + ); - it_id('bbb1686d-0e2a-4365-8b64-b5faa3e7b9cf')(it)('should not limit the scope of grouped write permissions', async done => { - await updateCLP({ - update: { - pointerFields: ['owner'], - }, - writeUserFields: ['moderators'], - }); + it_id('bbb1686d-0e2a-4365-8b64-b5faa3e7b9cf')(it)( + 'should not limit the scope of grouped write permissions', + async done => { + await updateCLP({ + update: { + pointerFields: ['owner'], + }, + writeUserFields: ['moderators'], + }); - await logIn(user2); + await logIn(user2); - await expectAsync(actionUpdate(obj1)).toBeResolved(); - await expectAsync(actionAddFieldOnUpdate(obj1)).toBeResolved(); - await expectAsync(actionDelete(obj1)).toBeResolved(); - // [create] and [addField on create] can't be enabled with pointer by design + await expectAsync(actionUpdate(obj1)).toBeResolved(); + await expectAsync(actionAddFieldOnUpdate(obj1)).toBeResolved(); + await expectAsync(actionDelete(obj1)).toBeResolved(); + // [create] and [addField on create] can't be enabled with pointer by design - done(); - }); + done(); + } + ); it('should not inherit scope of grouped read permissions from another field', async done => { await updateCLP({ diff --git a/spec/PostgresInitOptions.spec.js b/spec/PostgresInitOptions.spec.js index 1e3282ad77..2af94e9fc3 100644 --- a/spec/PostgresInitOptions.spec.js +++ b/spec/PostgresInitOptions.spec.js @@ -1,6 +1,6 @@ const Parse = require('parse/node').Parse; -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') - .default; +const PostgresStorageAdapter = + require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; const postgresURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; diff --git a/spec/PostgresStorageAdapter.spec.js b/spec/PostgresStorageAdapter.spec.js index aa5e692fe4..88a07c87d8 100644 --- a/spec/PostgresStorageAdapter.spec.js +++ b/spec/PostgresStorageAdapter.spec.js @@ -1,5 +1,5 @@ -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') - .default; +const PostgresStorageAdapter = + require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; const databaseURI = process.env.PARSE_SERVER_TEST_DATABASE_URI || 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index af29156a16..d0f853e270 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -233,67 +233,70 @@ describe('PushController', () => { } }); - it_id('14afcedf-e65d-41cd-981e-07f32df84c14')(it)('properly increment badges by more than 1', async () => { - const pushAdapter = { - send: function (body, installations) { - const badge = body.data.badge; - installations.forEach(installation => { - expect(installation.badge).toEqual(badge); - expect(installation.originalBadge + 3).toEqual(installation.badge); - }); - return successfulTransmissions(body, installations); - }, - getValidPushTypes: function () { - return ['ios', 'android']; - }, - }; - const payload = { - data: { - alert: 'Hello World!', - badge: { __op: 'Increment', amount: 3 }, - }, - }; - const installations = []; - while (installations.length != 10) { - const installation = new Parse.Object('_Installation'); - installation.set('installationId', 'installation_' + installations.length); - installation.set('deviceToken', 'device_token_' + installations.length); - installation.set('badge', installations.length); - installation.set('originalBadge', installations.length); - installation.set('deviceType', 'ios'); - installations.push(installation); - } + it_id('14afcedf-e65d-41cd-981e-07f32df84c14')(it)( + 'properly increment badges by more than 1', + async () => { + const pushAdapter = { + send: function (body, installations) { + const badge = body.data.badge; + installations.forEach(installation => { + expect(installation.badge).toEqual(badge); + expect(installation.originalBadge + 3).toEqual(installation.badge); + }); + return successfulTransmissions(body, installations); + }, + getValidPushTypes: function () { + return ['ios', 'android']; + }, + }; + const payload = { + data: { + alert: 'Hello World!', + badge: { __op: 'Increment', amount: 3 }, + }, + }; + const installations = []; + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set('installationId', 'installation_' + installations.length); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); + installations.push(installation); + } - while (installations.length != 15) { - const installation = new Parse.Object('_Installation'); - installation.set('installationId', 'installation_' + installations.length); - installation.set('deviceToken', 'device_token_' + installations.length); - installation.set('badge', installations.length); - installation.set('originalBadge', installations.length); - installation.set('deviceType', 'android'); - installations.push(installation); - } - const config = Config.get(Parse.applicationId); - const auth = { - isMaster: true, - }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); - await Parse.Object.saveAll(installations); - const pushStatusId = await sendPush(payload, {}, config, auth); - await pushCompleted(pushStatusId); - const pushStatus = await Parse.Push.getPushStatus(pushStatusId); - expect(pushStatus.get('numSent')).toBe(15); - // Check that the installations were actually updated. - const query = new Parse.Query('_Installation'); - const results = await query.find({ useMasterKey: true }); - expect(results.length).toBe(15); - for (let i = 0; i < 15; i++) { - const installation = results[i]; - expect(installation.get('badge')).toBe(parseInt(installation.get('originalBadge')) + 3); + while (installations.length != 15) { + const installation = new Parse.Object('_Installation'); + installation.set('installationId', 'installation_' + installations.length); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'android'); + installations.push(installation); + } + const config = Config.get(Parse.applicationId); + const auth = { + isMaster: true, + }; + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); + await Parse.Object.saveAll(installations); + const pushStatusId = await sendPush(payload, {}, config, auth); + await pushCompleted(pushStatusId); + const pushStatus = await Parse.Push.getPushStatus(pushStatusId); + expect(pushStatus.get('numSent')).toBe(15); + // Check that the installations were actually updated. + const query = new Parse.Query('_Installation'); + const results = await query.find({ useMasterKey: true }); + expect(results.length).toBe(15); + for (let i = 0; i < 15; i++) { + const installation = results[i]; + expect(installation.get('badge')).toBe(parseInt(installation.get('originalBadge')) + 3); + } } - }); + ); it_id('758dd579-aa91-4010-9033-8d48d3463644')(it)('properly set badges to 1', async () => { const pushAdapter = { @@ -350,61 +353,64 @@ describe('PushController', () => { } }); - it_id('75c39ae3-06ac-4354-b321-931e81c5a927')(it)('properly set badges to 1 with complex query #2903 #3022', async () => { - const payload = { - data: { - alert: 'Hello World!', - badge: 1, - }, - }; - const installations = []; - while (installations.length != 10) { - const installation = new Parse.Object('_Installation'); - installation.set('installationId', 'installation_' + installations.length); - installation.set('deviceToken', 'device_token_' + installations.length); - installation.set('badge', installations.length); - installation.set('originalBadge', installations.length); - installation.set('deviceType', 'ios'); - installations.push(installation); - } - let matchedInstallationsCount = 0; - const pushAdapter = { - send: function (body, installations) { - matchedInstallationsCount += installations.length; - const badge = body.data.badge; - installations.forEach(installation => { - expect(installation.badge).toEqual(badge); - expect(1).toEqual(installation.badge); - }); - return successfulTransmissions(body, installations); - }, - getValidPushTypes: function () { - return ['ios']; - }, - }; + it_id('75c39ae3-06ac-4354-b321-931e81c5a927')(it)( + 'properly set badges to 1 with complex query #2903 #3022', + async () => { + const payload = { + data: { + alert: 'Hello World!', + badge: 1, + }, + }; + const installations = []; + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set('installationId', 'installation_' + installations.length); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); + installations.push(installation); + } + let matchedInstallationsCount = 0; + const pushAdapter = { + send: function (body, installations) { + matchedInstallationsCount += installations.length; + const badge = body.data.badge; + installations.forEach(installation => { + expect(installation.badge).toEqual(badge); + expect(1).toEqual(installation.badge); + }); + return successfulTransmissions(body, installations); + }, + getValidPushTypes: function () { + return ['ios']; + }, + }; - const config = Config.get(Parse.applicationId); - const auth = { - isMaster: true, - }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); - await Parse.Object.saveAll(installations); - const objectIds = installations.map(installation => { - return installation.id; - }); - const where = { - objectId: { $in: objectIds.slice(0, 5) }, - }; - const pushStatusId = await sendPush(payload, where, config, auth); - await pushCompleted(pushStatusId); - expect(matchedInstallationsCount).toBe(5); - const query = new Parse.Query(Parse.Installation); - query.equalTo('badge', 1); - const results = await query.find({ useMasterKey: true }); - expect(results.length).toBe(5); - }); + const config = Config.get(Parse.applicationId); + const auth = { + isMaster: true, + }; + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); + await Parse.Object.saveAll(installations); + const objectIds = installations.map(installation => { + return installation.id; + }); + const where = { + objectId: { $in: objectIds.slice(0, 5) }, + }; + const pushStatusId = await sendPush(payload, where, config, auth); + await pushCompleted(pushStatusId); + expect(matchedInstallationsCount).toBe(5); + const query = new Parse.Query(Parse.Installation); + query.equalTo('badge', 1); + const results = await query.find({ useMasterKey: true }); + expect(results.length).toBe(5); + } + ); it_id('667f31c0-b458-4f61-ab57-668c04e3cc0b')(it)('properly creates _PushStatus', async () => { const pushStatusAfterSave = { @@ -521,51 +527,54 @@ describe('PushController', () => { expect(succeedCount).toBe(1); }); - it_id('30e0591a-56de-4720-8c60-7d72291b532a')(it)('properly creates _PushStatus without serverURL', async () => { - const pushStatusAfterSave = { - handler: function () {}, - }; - Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler); - const installation = new Parse.Object('_Installation'); - installation.set('installationId', 'installation'); - installation.set('deviceToken', 'device_token'); - installation.set('badge', 0); - installation.set('originalBadge', 0); - installation.set('deviceType', 'ios'); + it_id('30e0591a-56de-4720-8c60-7d72291b532a')(it)( + 'properly creates _PushStatus without serverURL', + async () => { + const pushStatusAfterSave = { + handler: function () {}, + }; + Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler); + const installation = new Parse.Object('_Installation'); + installation.set('installationId', 'installation'); + installation.set('deviceToken', 'device_token'); + installation.set('badge', 0); + installation.set('originalBadge', 0); + installation.set('deviceType', 'ios'); - const payload = { - data: { - alert: 'Hello World!', - badge: 1, - }, - }; + const payload = { + data: { + alert: 'Hello World!', + badge: 1, + }, + }; - const pushAdapter = { - send: function (body, installations) { - return successfulIOS(body, installations); - }, - getValidPushTypes: function () { - return ['ios']; - }, - }; + const pushAdapter = { + send: function (body, installations) { + return successfulIOS(body, installations); + }, + getValidPushTypes: function () { + return ['ios']; + }, + }; - const config = Config.get(Parse.applicationId); - const auth = { - isMaster: true, - }; - await installation.save(); - await reconfigureServer({ - serverURL: 'http://localhost:8378/', // server with borked URL - push: { adapter: pushAdapter }, - }); - const pushStatusId = await sendPush(payload, {}, config, auth); - // it is enqueued so it can take time - await jasmine.timeout(1000); - Parse.serverURL = 'http://localhost:8378/1'; // GOOD url - const result = await Parse.Push.getPushStatus(pushStatusId); - expect(result).toBeDefined(); - await pushCompleted(pushStatusId); - }); + const config = Config.get(Parse.applicationId); + const auth = { + isMaster: true, + }; + await installation.save(); + await reconfigureServer({ + serverURL: 'http://localhost:8378/', // server with borked URL + push: { adapter: pushAdapter }, + }); + const pushStatusId = await sendPush(payload, {}, config, auth); + // it is enqueued so it can take time + await jasmine.timeout(1000); + Parse.serverURL = 'http://localhost:8378/1'; // GOOD url + const result = await Parse.Push.getPushStatus(pushStatusId); + expect(result).toBeDefined(); + await pushCompleted(pushStatusId); + } + ); it('should properly report failures in _PushStatus', async () => { const pushAdapter = { @@ -615,51 +624,54 @@ describe('PushController', () => { } }); - it_id('53551fc3-b975-4774-92e6-7e5f3c05e105')(it)('should support full RESTQuery for increment', async () => { - const payload = { - data: { - alert: 'Hello World!', - badge: 'Increment', - }, - }; + it_id('53551fc3-b975-4774-92e6-7e5f3c05e105')(it)( + 'should support full RESTQuery for increment', + async () => { + const payload = { + data: { + alert: 'Hello World!', + badge: 'Increment', + }, + }; - const pushAdapter = { - send: function (body, installations) { - return successfulTransmissions(body, installations); - }, - getValidPushTypes: function () { - return ['ios']; - }, - }; - const config = Config.get(Parse.applicationId); - const auth = { - isMaster: true, - }; + const pushAdapter = { + send: function (body, installations) { + return successfulTransmissions(body, installations); + }, + getValidPushTypes: function () { + return ['ios']; + }, + }; + const config = Config.get(Parse.applicationId); + const auth = { + isMaster: true, + }; - const where = { - deviceToken: { - $in: ['device_token_0', 'device_token_1', 'device_token_2'], - }, - }; - await reconfigureServer({ - push: { adapter: pushAdapter }, - }); - const installations = []; - while (installations.length != 5) { - const installation = new Parse.Object('_Installation'); - installation.set('installationId', 'installation_' + installations.length); - installation.set('deviceToken', 'device_token_' + installations.length); - installation.set('badge', installations.length); - installation.set('originalBadge', installations.length); - installation.set('deviceType', 'ios'); - installations.push(installation); + const where = { + deviceToken: { + $in: ['device_token_0', 'device_token_1', 'device_token_2'], + }, + }; + await reconfigureServer({ + push: { adapter: pushAdapter }, + }); + const installations = []; + while (installations.length != 5) { + const installation = new Parse.Object('_Installation'); + installation.set('installationId', 'installation_' + installations.length); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); + installations.push(installation); + } + await Parse.Object.saveAll(installations); + const pushStatusId = await sendPush(payload, where, config, auth); + await pushCompleted(pushStatusId); + const pushStatus = await Parse.Push.getPushStatus(pushStatusId); + expect(pushStatus.get('numSent')).toBe(3); } - await Parse.Object.saveAll(installations); - const pushStatusId = await sendPush(payload, where, config, auth); - await pushCompleted(pushStatusId); - const pushStatus = await Parse.Push.getPushStatus(pushStatusId); - expect(pushStatus.get('numSent')).toBe(3); - }); + ); it('should support object type for alert', async () => { const payload = { diff --git a/spec/PushWorker.spec.js b/spec/PushWorker.spec.js index 6299962d52..23e8206fe3 100644 --- a/spec/PushWorker.spec.js +++ b/spec/PushWorker.spec.js @@ -271,99 +271,102 @@ describe('PushWorker', () => { toAwait.then(done).catch(done); }); - it_id('764d28ab-241b-4b96-8ce9-e03541850e3f')(it)('tracks push status per UTC offsets', done => { - const config = Config.get('test'); - const handler = pushStatusHandler(config); - const spy = spyOn(rest, 'update').and.callThrough(); - const UTCOffset = 1; - handler - .setInitial() - .then(() => { - return handler.trackSent( - [ - { - transmitted: false, - device: { - deviceToken: 1, - deviceType: 'ios', + it_id('764d28ab-241b-4b96-8ce9-e03541850e3f')(it)( + 'tracks push status per UTC offsets', + done => { + const config = Config.get('test'); + const handler = pushStatusHandler(config); + const spy = spyOn(rest, 'update').and.callThrough(); + const UTCOffset = 1; + handler + .setInitial() + .then(() => { + return handler.trackSent( + [ + { + transmitted: false, + device: { + deviceToken: 1, + deviceType: 'ios', + }, }, - }, - { - transmitted: true, - device: { - deviceToken: 1, - deviceType: 'ios', + { + transmitted: true, + device: { + deviceToken: 1, + deviceType: 'ios', + }, }, + ], + UTCOffset + ); + }) + .then(() => { + expect(spy).toHaveBeenCalled(); + const lastCall = spy.calls.mostRecent(); + expect(lastCall.args[2]).toBe(`_PushStatus`); + expect(lastCall.args[4]).toEqual({ + numSent: { __op: 'Increment', amount: 1 }, + numFailed: { __op: 'Increment', amount: 1 }, + 'sentPerType.ios': { __op: 'Increment', amount: 1 }, + 'failedPerType.ios': { __op: 'Increment', amount: 1 }, + [`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 }, + [`failedPerUTCOffset.${UTCOffset}`]: { + __op: 'Increment', + amount: 1, }, - ], - UTCOffset - ); - }) - .then(() => { - expect(spy).toHaveBeenCalled(); - const lastCall = spy.calls.mostRecent(); - expect(lastCall.args[2]).toBe(`_PushStatus`); - expect(lastCall.args[4]).toEqual({ - numSent: { __op: 'Increment', amount: 1 }, - numFailed: { __op: 'Increment', amount: 1 }, - 'sentPerType.ios': { __op: 'Increment', amount: 1 }, - 'failedPerType.ios': { __op: 'Increment', amount: 1 }, - [`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 }, - [`failedPerUTCOffset.${UTCOffset}`]: { - __op: 'Increment', - amount: 1, - }, - count: { __op: 'Increment', amount: -1 }, - status: 'running', - }); - const query = new Parse.Query('_PushStatus'); - return query.get(handler.objectId, { useMasterKey: true }); - }) - .then(pushStatus => { - const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset'); - expect(sentPerUTCOffset['1']).toBe(1); - const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset'); - expect(failedPerUTCOffset['1']).toBe(1); - return handler.trackSent( - [ - { - transmitted: false, - device: { - deviceToken: 1, - deviceType: 'ios', + count: { __op: 'Increment', amount: -1 }, + status: 'running', + }); + const query = new Parse.Query('_PushStatus'); + return query.get(handler.objectId, { useMasterKey: true }); + }) + .then(pushStatus => { + const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset'); + expect(sentPerUTCOffset['1']).toBe(1); + const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset'); + expect(failedPerUTCOffset['1']).toBe(1); + return handler.trackSent( + [ + { + transmitted: false, + device: { + deviceToken: 1, + deviceType: 'ios', + }, }, - }, - { - transmitted: true, - device: { - deviceToken: 1, - deviceType: 'ios', + { + transmitted: true, + device: { + deviceToken: 1, + deviceType: 'ios', + }, }, - }, - { - transmitted: true, - device: { - deviceToken: 1, - deviceType: 'ios', + { + transmitted: true, + device: { + deviceToken: 1, + deviceType: 'ios', + }, }, - }, - ], - UTCOffset - ); - }) - .then(() => { - const query = new Parse.Query('_PushStatus'); - return query.get(handler.objectId, { useMasterKey: true }); - }) - .then(pushStatus => { - const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset'); - expect(sentPerUTCOffset['1']).toBe(3); - const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset'); - expect(failedPerUTCOffset['1']).toBe(2); - }) - .then(done) - .catch(done.fail); - }); + ], + UTCOffset + ); + }) + .then(() => { + const query = new Parse.Query('_PushStatus'); + return query.get(handler.objectId, { useMasterKey: true }); + }) + .then(pushStatus => { + const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset'); + expect(sentPerUTCOffset['1']).toBe(3); + const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset'); + expect(failedPerUTCOffset['1']).toBe(2); + }) + .then(done) + .catch(done.fail); + } + ); it('tracks push status per UTC offsets with negative offsets', done => { const config = Config.get('test'); diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 6fe3c0fa18..131433e177 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -419,69 +419,72 @@ describe('RestQuery.each', () => { expect(results.length).toBe(7); }); - it_id('0fe22501-4b18-461e-b87d-82ceac4a496e')(it)('should work with query on relations', async () => { - const objectA = new Parse.Object('Letter', { value: 'A' }); - const objectB = new Parse.Object('Letter', { value: 'B' }); - - const object1 = new Parse.Object('Number', { value: '1' }); - const object2 = new Parse.Object('Number', { value: '2' }); - const object3 = new Parse.Object('Number', { value: '3' }); - const object4 = new Parse.Object('Number', { value: '4' }); - await Parse.Object.saveAll([object1, object2, object3, object4]); - - objectA.relation('numbers').add(object1); - objectB.relation('numbers').add(object2); - await Parse.Object.saveAll([objectA, objectB]); - - const config = Config.get('test'); - - /** - * Two queries needed since objectId are sorted and we can't know which one - * going to be the first and then skip by the $gt added by each - */ - const queryOne = await RestQuery({ - method: RestQuery.Method.get, - config, - auth: auth.master(config), - className: 'Letter', - restWhere: { - numbers: { - __type: 'Pointer', - className: 'Number', - objectId: object1.id, + it_id('0fe22501-4b18-461e-b87d-82ceac4a496e')(it)( + 'should work with query on relations', + async () => { + const objectA = new Parse.Object('Letter', { value: 'A' }); + const objectB = new Parse.Object('Letter', { value: 'B' }); + + const object1 = new Parse.Object('Number', { value: '1' }); + const object2 = new Parse.Object('Number', { value: '2' }); + const object3 = new Parse.Object('Number', { value: '3' }); + const object4 = new Parse.Object('Number', { value: '4' }); + await Parse.Object.saveAll([object1, object2, object3, object4]); + + objectA.relation('numbers').add(object1); + objectB.relation('numbers').add(object2); + await Parse.Object.saveAll([objectA, objectB]); + + const config = Config.get('test'); + + /** + * Two queries needed since objectId are sorted and we can't know which one + * going to be the first and then skip by the $gt added by each + */ + const queryOne = await RestQuery({ + method: RestQuery.Method.get, + config, + auth: auth.master(config), + className: 'Letter', + restWhere: { + numbers: { + __type: 'Pointer', + className: 'Number', + objectId: object1.id, + }, }, - }, - restOptions: { limit: 1 }, - }); + restOptions: { limit: 1 }, + }); - const queryTwo = await RestQuery({ - method: RestQuery.Method.get, - config, - auth: auth.master(config), - className: 'Letter', - restWhere: { - numbers: { - __type: 'Pointer', - className: 'Number', - objectId: object2.id, + const queryTwo = await RestQuery({ + method: RestQuery.Method.get, + config, + auth: auth.master(config), + className: 'Letter', + restWhere: { + numbers: { + __type: 'Pointer', + className: 'Number', + objectId: object2.id, + }, }, - }, - restOptions: { limit: 1 }, - }); + restOptions: { limit: 1 }, + }); - const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough(); - const resultsOne = []; - const resultsTwo = []; - await queryOne.each(result => { - resultsOne.push(result); - }); - await queryTwo.each(result => { - resultsTwo.push(result); - }); - expect(classSpy.calls.count()).toBe(4); - expect(resultsOne.length).toBe(1); - expect(resultsTwo.length).toBe(1); - }); + const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough(); + const resultsOne = []; + const resultsTwo = []; + await queryOne.each(result => { + resultsOne.push(result); + }); + await queryTwo.each(result => { + resultsTwo.push(result); + }); + expect(classSpy.calls.count()).toBe(4); + expect(resultsOne.length).toBe(1); + expect(resultsTwo.length).toBe(1); + } + ); it('test afterSave response object is return', done => { Parse.Cloud.beforeSave('TestObject2', function (req) { diff --git a/spec/SchemaPerformance.spec.js b/spec/SchemaPerformance.spec.js index 729401d068..a6b771b4b6 100644 --- a/spec/SchemaPerformance.spec.js +++ b/spec/SchemaPerformance.spec.js @@ -247,21 +247,24 @@ describe('Schema Performance', function () { expect(spy.reloadCalls).toBe(1); }); - it_id('b0ae21f2-c947-48ed-a0db-e8900d45a4c8')(it)('cannot set invalid databaseOptions', async () => { - const expectError = async (key, value, expected) => - expectAsync( - reconfigureServer({ databaseAdapter: undefined, databaseOptions: { [key]: value } }) - ).toBeRejectedWith(`databaseOptions.${key} must be a ${expected}`); - for (const databaseOptions of [[], 0, 'string']) { - await expectAsync( - reconfigureServer({ databaseAdapter: undefined, databaseOptions }) - ).toBeRejectedWith(`databaseOptions must be an object`); + it_id('b0ae21f2-c947-48ed-a0db-e8900d45a4c8')(it)( + 'cannot set invalid databaseOptions', + async () => { + const expectError = async (key, value, expected) => + expectAsync( + reconfigureServer({ databaseAdapter: undefined, databaseOptions: { [key]: value } }) + ).toBeRejectedWith(`databaseOptions.${key} must be a ${expected}`); + for (const databaseOptions of [[], 0, 'string']) { + await expectAsync( + reconfigureServer({ databaseAdapter: undefined, databaseOptions }) + ).toBeRejectedWith(`databaseOptions must be an object`); + } + for (const value of [null, 0, 'string', {}, []]) { + await expectError('enableSchemaHooks', value, 'boolean'); + } + for (const value of [null, false, 'string', {}, []]) { + await expectError('schemaCacheTtl', value, 'number'); + } } - for (const value of [null, 0, 'string', {}, []]) { - await expectError('enableSchemaHooks', value, 'boolean'); - } - for (const value of [null, false, 'string', {}, []]) { - await expectError('schemaCacheTtl', value, 'number'); - } - }); + ); }); diff --git a/spec/Uniqueness.spec.js b/spec/Uniqueness.spec.js index 92ee6ea92c..7f53ac54bb 100644 --- a/spec/Uniqueness.spec.js +++ b/spec/Uniqueness.spec.js @@ -68,25 +68,28 @@ describe('Uniqueness', function () { }); }); - it_id('802650a9-a6db-447e-88d0-8aae99100088')(it)('fails when attempting to ensure uniqueness of fields that are not currently unique', done => { - const o1 = new Parse.Object('UniqueFail'); - o1.set('key', 'val'); - const o2 = new Parse.Object('UniqueFail'); - o2.set('key', 'val'); - Parse.Object.saveAll([o1, o2]) - .then(() => { - const config = Config.get('test'); - return config.database.adapter.ensureUniqueness( - 'UniqueFail', - { fields: { key: { __type: 'String' } } }, - ['key'] - ); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); - done(); - }); - }); + it_id('802650a9-a6db-447e-88d0-8aae99100088')(it)( + 'fails when attempting to ensure uniqueness of fields that are not currently unique', + done => { + const o1 = new Parse.Object('UniqueFail'); + o1.set('key', 'val'); + const o2 = new Parse.Object('UniqueFail'); + o2.set('key', 'val'); + Parse.Object.saveAll([o1, o2]) + .then(() => { + const config = Config.get('test'); + return config.database.adapter.ensureUniqueness( + 'UniqueFail', + { fields: { key: { __type: 'String' } } }, + ['key'] + ); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + }); + } + ); it_exclude_dbs(['postgres'])('can do compound uniqueness', done => { const config = Config.get('test'); diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 31d5051960..a4791b5101 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -29,49 +29,66 @@ describe('UserController', () => { await user.signUp(); const config = Config.get('test'); - const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config)); + const rawUser = await config.database.find( + '_User', + { username }, + {}, + Auth.maintenance(config) + ); const rawUsername = rawUser[0].username; const rawToken = rawUser[0]._email_verify_token; expect(rawToken).toBeDefined(); expect(rawUsername).toBe(username); - expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}`); + expect(emailOptions.link).toEqual( + `http://www.example.com/apps/test/verify_email?token=${rawToken}` + ); }); }); describe('parseFrameURL provided', () => { - it_id('673c2bb1-049e-4dda-b6be-88c866260036')(it)('uses parseFrameURL and includes the destination in the link parameter', async () => { - await reconfigureServer({ - publicServerURL: 'http://www.example.com', - customPages: { - parseFrameURL: 'http://someother.example.com/handle-parse-iframe', - }, - verifyUserEmails: true, - emailAdapter, - appName: 'test', - }); + it_id('673c2bb1-049e-4dda-b6be-88c866260036')(it)( + 'uses parseFrameURL and includes the destination in the link parameter', + async () => { + await reconfigureServer({ + publicServerURL: 'http://www.example.com', + customPages: { + parseFrameURL: 'http://someother.example.com/handle-parse-iframe', + }, + verifyUserEmails: true, + emailAdapter, + appName: 'test', + }); - let emailOptions; - emailAdapter.sendVerificationEmail = options => { - emailOptions = options; - }; + let emailOptions; + emailAdapter.sendVerificationEmail = options => { + emailOptions = options; + }; - const username = 'verificationUser'; - const user = new Parse.User(); - user.setUsername(username); - user.setPassword('pass'); - user.setEmail('verification@example.com'); - await user.signUp(); + const username = 'verificationUser'; + const user = new Parse.User(); + user.setUsername(username); + user.setPassword('pass'); + user.setEmail('verification@example.com'); + await user.signUp(); - const config = Config.get('test'); - const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config)); - const rawUsername = rawUser[0].username; - const rawToken = rawUser[0]._email_verify_token; - expect(rawToken).toBeDefined(); - expect(rawUsername).toBe(username); + const config = Config.get('test'); + const rawUser = await config.database.find( + '_User', + { username }, + {}, + Auth.maintenance(config) + ); + const rawUsername = rawUser[0].username; + const rawToken = rawUser[0]._email_verify_token; + expect(rawToken).toBeDefined(); + expect(rawUsername).toBe(username); - expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}`); - }); + expect(emailOptions.link).toEqual( + `http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}` + ); + } + ); }); }); }); diff --git a/spec/Utils.spec.js b/spec/Utils.spec.js index fe86854e33..c4f04096fa 100644 --- a/spec/Utils.spec.js +++ b/spec/Utils.spec.js @@ -5,10 +5,10 @@ describe('Utils', () => { it('should properly escape email with all special ASCII characters for use in URLs', async () => { const values = [ { input: `!\"'),.:;<>?]^}`, output: '%21%22%27%29%2C%2E%3A%3B%3C%3E%3F%5D%5E%7D' }, - ] + ]; for (const value of values) { expect(Utils.encodeForUrl(value.input)).toBe(value.output); - } + } }); }); @@ -18,28 +18,28 @@ describe('Utils', () => { a: 1, b: { c: 2, - d: 3 + d: 3, }, - e: 4 + e: 4, }; Utils.addNestedKeysToRoot(obj, 'b'); expect(obj).toEqual({ a: 1, c: 2, d: 3, - e: 4 + e: 4, }); }); it('should not modify the object if the key does not exist', async () => { const obj = { a: 1, - e: 4 + e: 4, }; Utils.addNestedKeysToRoot(obj, 'b'); expect(obj).toEqual({ a: 1, - e: 4 + e: 4, }); }); @@ -47,13 +47,13 @@ describe('Utils', () => { const obj = { a: 1, b: 2, - e: 4 + e: 4, }; Utils.addNestedKeysToRoot(obj, 'b'); expect(obj).toEqual({ a: 1, b: 2, - e: 4 + e: 4, }); }); }); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 3f6d4048c5..5816217a07 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -34,32 +34,35 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - it_id('5e558687-40f3-496c-9e4f-af6100bd1b2f')(it)('sends verification email if email verification is enabled', done => { - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve(), - }; - reconfigureServer({ - appName: 'unused', - verifyUserEmails: true, - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }).then(async () => { - spyOn(emailAdapter, 'sendVerificationEmail'); - const user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('zxcv'); - user.setEmail('testIfEnabled@parse.com'); - await user.signUp(); - await jasmine.timeout(); - expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); - user.fetch().then(() => { - expect(user.get('emailVerified')).toEqual(false); - done(); + it_id('5e558687-40f3-496c-9e4f-af6100bd1b2f')(it)( + 'sends verification email if email verification is enabled', + done => { + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => Promise.resolve(), + }; + reconfigureServer({ + appName: 'unused', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }).then(async () => { + spyOn(emailAdapter, 'sendVerificationEmail'); + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.setEmail('testIfEnabled@parse.com'); + await user.signUp(); + await jasmine.timeout(); + expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); + user.fetch().then(() => { + expect(user.get('emailVerified')).toEqual(false); + done(); + }); }); - }); - }); + } + ); it('does not send verification email when verification is enabled and email is not set', done => { const emailAdapter = { @@ -279,7 +282,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { await user.signUp(); const verifyUserEmails = { - method: async (params) => { + method: async params => { expect(params.object).toBeInstanceOf(Parse.User); expect(params.ip).toBeDefined(); expect(params.master).toBeDefined(); @@ -306,43 +309,46 @@ describe('Custom Pages, Email Verification, Password Reset', () => { expect(verifyUserEmailsSpy).toHaveBeenCalledTimes(2); }); - it_id('2a5d24be-2ca5-4385-b580-1423bd392e43')(it)('allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', async () => { - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - await reconfigureServer({ - appName: 'emailing app', - verifyUserEmails: true, - preventLoginWithUnverifiedEmail: true, - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }); - let user = new Parse.User(); - user.setPassword('other-password'); - user.setUsername('user'); - user.set('email', 'user@example.com'); - await user.signUp(); - await jasmine.timeout(); - expect(sendEmailOptions).not.toBeUndefined(); - const response = await request({ - url: sendEmailOptions.link, - followRedirects: false, - }); - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' - ); - user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); - expect(user.get('emailVerified')).toEqual(true); - user = await Parse.User.logIn('user', 'other-password'); - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(true); - }); + it_id('2a5d24be-2ca5-4385-b580-1423bd392e43')(it)( + 'allows user to login only after user clicks on the link to confirm email address if preventLoginWithUnverifiedEmail is set to true', + async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'emailing app', + verifyUserEmails: true, + preventLoginWithUnverifiedEmail: true, + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + let user = new Parse.User(); + user.setPassword('other-password'); + user.setUsername('user'); + user.set('email', 'user@example.com'); + await user.signUp(); + await jasmine.timeout(); + expect(sendEmailOptions).not.toBeUndefined(); + const response = await request({ + url: sendEmailOptions.link, + followRedirects: false, + }); + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' + ); + user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); + expect(user.get('emailVerified')).toEqual(true); + user = await Parse.User.logIn('user', 'other-password'); + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); + } + ); it('allows user to login if email is not verified but preventLoginWithUnverifiedEmail is set to false', done => { reconfigureServer({ @@ -382,34 +388,37 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - it_id('a18a07af-0319-4f15-8237-28070c5948fa')(it)('does not allow signup with preventSignupWithUnverified', async () => { - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - await reconfigureServer({ - appName: 'test', - publicServerURL: 'http://localhost:1337/1', - verifyUserEmails: true, - preventLoginWithUnverifiedEmail: true, - preventSignupWithUnverifiedEmail: true, - emailAdapter, - }); - const newUser = new Parse.User(); - newUser.setPassword('asdf'); - newUser.setUsername('zxcv'); - newUser.set('email', 'test@example.com'); - await expectAsync(newUser.signUp()).toBeRejectedWith( - new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') - ); - const user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); - expect(user).toBeDefined(); - expect(sendEmailOptions).toBeDefined(); - }); + it_id('a18a07af-0319-4f15-8237-28070c5948fa')(it)( + 'does not allow signup with preventSignupWithUnverified', + async () => { + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + await reconfigureServer({ + appName: 'test', + publicServerURL: 'http://localhost:1337/1', + verifyUserEmails: true, + preventLoginWithUnverifiedEmail: true, + preventSignupWithUnverifiedEmail: true, + emailAdapter, + }); + const newUser = new Parse.User(); + newUser.setPassword('asdf'); + newUser.setUsername('zxcv'); + newUser.set('email', 'test@example.com'); + await expectAsync(newUser.signUp()).toBeRejectedWith( + new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.') + ); + const user = await new Parse.Query(Parse.User).first({ useMasterKey: true }); + expect(user).toBeDefined(); + expect(sendEmailOptions).toBeDefined(); + } + ); it('fails if you include an emailAdapter, set a publicServerURL, but have no appName and send a password reset email', done => { reconfigureServer({ @@ -617,87 +626,93 @@ describe('Custom Pages, Email Verification, Password Reset', () => { }); }); - it_id('45f550a2-a2b2-4b2b-b533-ccbf96139cc9')(it)('receives the app name and user in the adapter', done => { - let emailSent = false; - const emailAdapter = { - sendVerificationEmail: options => { - expect(options.appName).toEqual('emailing app'); - expect(options.user.get('email')).toEqual('user@parse.com'); - emailSent = true; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailing app', - verifyUserEmails: true, - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }).then(async () => { - const user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('zxcv'); - user.set('email', 'user@parse.com'); - await user.signUp(); - await jasmine.timeout(); - expect(emailSent).toBe(true); - done(); - }); - }); - - it_id('ea37ef62-aad8-4a17-8dfe-35e5b2986f0f')(it)('when you click the link in the email it sets emailVerified to true and redirects you', done => { - const user = new Parse.User(); - let sendEmailOptions; - const emailAdapter = { - sendVerificationEmail: options => { - sendEmailOptions = options; - }, - sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {}, - }; - reconfigureServer({ - appName: 'emailing app', - verifyUserEmails: true, - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - user.setPassword('other-password'); - user.setUsername('user'); + it_id('45f550a2-a2b2-4b2b-b533-ccbf96139cc9')(it)( + 'receives the app name and user in the adapter', + done => { + let emailSent = false; + const emailAdapter = { + sendVerificationEmail: options => { + expect(options.appName).toEqual('emailing app'); + expect(options.user.get('email')).toEqual('user@parse.com'); + emailSent = true; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailing app', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }).then(async () => { + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); user.set('email', 'user@parse.com'); - return user.signUp(); + await user.signUp(); + await jasmine.timeout(); + expect(emailSent).toBe(true); + done(); + }); + } + ); + + it_id('ea37ef62-aad8-4a17-8dfe-35e5b2986f0f')(it)( + 'when you click the link in the email it sets emailVerified to true and redirects you', + done => { + const user = new Parse.User(); + let sendEmailOptions; + const emailAdapter = { + sendVerificationEmail: options => { + sendEmailOptions = options; + }, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + reconfigureServer({ + appName: 'emailing app', + verifyUserEmails: true, + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', }) - .then(() => jasmine.timeout()) - .then(() => { - expect(sendEmailOptions).not.toBeUndefined(); - request({ - url: sendEmailOptions.link, - followRedirects: false, - }).then(response => { - expect(response.status).toEqual(302); - expect(response.text).toEqual( - 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' - ); - user - .fetch() - .then( - () => { - expect(user.get('emailVerified')).toEqual(true); - done(); - }, - err => { + .then(() => { + user.setPassword('other-password'); + user.setUsername('user'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => jasmine.timeout()) + .then(() => { + expect(sendEmailOptions).not.toBeUndefined(); + request({ + url: sendEmailOptions.link, + followRedirects: false, + }).then(response => { + expect(response.status).toEqual(302); + expect(response.text).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html' + ); + user + .fetch() + .then( + () => { + expect(user.get('emailVerified')).toEqual(true); + done(); + }, + err => { + jfail(err); + fail('this should not fail'); + done(); + } + ) + .catch(err => { jfail(err); - fail('this should not fail'); done(); - } - ) - .catch(err => { - jfail(err); - done(); - }); + }); + }); }); - }); - }); + } + ); it('redirects you to invalid link if you try to verify email incorrectly', done => { reconfigureServer({ @@ -825,7 +840,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/; expect(response.text.match(re)).not.toBe(null); done(); }); @@ -887,7 +903,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -964,7 +981,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }).then(response => { expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); @@ -1023,7 +1041,8 @@ describe('Custom Pages, Email Verification, Password Reset', () => { followRedirects: false, }); expect(response.status).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; + const re = + /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/; const match = response.text.match(re); if (!match) { fail('should have a token'); diff --git a/spec/WinstonLoggerAdapter.spec.js b/spec/WinstonLoggerAdapter.spec.js index 81bdc213de..da9efd2610 100644 --- a/spec/WinstonLoggerAdapter.spec.js +++ b/spec/WinstonLoggerAdapter.spec.js @@ -1,7 +1,7 @@ 'use strict'; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') - .WinstonLoggerAdapter; +const WinstonLoggerAdapter = + require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; const request = require('../lib/request'); describe_only(() => { @@ -174,50 +174,53 @@ describe_only(() => { describe_only(() => { return process.env.PARSE_SERVER_LOG_LEVEL !== 'debug'; })('verbose logs', () => { - it_id('9ca72994-d255-4c11-a5a2-693c99ee2cdb')(it)('mask sensitive information in _User class', done => { - reconfigureServer({ verbose: true }) - .then(() => createTestUser()) - .then(() => { - const winstonLoggerAdapter = new WinstonLoggerAdapter(); - return winstonLoggerAdapter.query({ - from: new Date(Date.now() - 500), - size: 100, - level: 'verbose', - }); - }) - .then(results => { - const logString = JSON.stringify(results); - expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0); - expect(logString.match(/moon-y/g)).toBe(null); - - const headers = { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }; - request({ - headers: headers, - url: 'http://localhost:8378/1/login?username=test&password=moon-y', - }).then(() => { + it_id('9ca72994-d255-4c11-a5a2-693c99ee2cdb')(it)( + 'mask sensitive information in _User class', + done => { + reconfigureServer({ verbose: true }) + .then(() => createTestUser()) + .then(() => { const winstonLoggerAdapter = new WinstonLoggerAdapter(); - return winstonLoggerAdapter - .query({ - from: new Date(Date.now() - 500), - size: 100, - level: 'verbose', - }) - .then(results => { - const logString = JSON.stringify(results); - expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0); - expect(logString.match(/moon-y/g)).toBe(null); - done(); - }); + return winstonLoggerAdapter.query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose', + }); + }) + .then(results => { + const logString = JSON.stringify(results); + expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0); + expect(logString.match(/moon-y/g)).toBe(null); + + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + request({ + headers: headers, + url: 'http://localhost:8378/1/login?username=test&password=moon-y', + }).then(() => { + const winstonLoggerAdapter = new WinstonLoggerAdapter(); + return winstonLoggerAdapter + .query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose', + }) + .then(results => { + const logString = JSON.stringify(results); + expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0); + expect(logString.match(/moon-y/g)).toBe(null); + done(); + }); + }); + }) + .catch(err => { + fail(JSON.stringify(err)); + done(); }); - }) - .catch(err => { - fail(JSON.stringify(err)); - done(); - }); - }); + } + ); it('verbose logs should interpolate string', async () => { await reconfigureServer({ verbose: true }); diff --git a/spec/batch.spec.js b/spec/batch.spec.js index 8c9ef27a6b..66a743b1d9 100644 --- a/spec/batch.spec.js +++ b/spec/batch.spec.js @@ -242,9 +242,7 @@ describe('batch', () => { const results = await query.find(); expect(createSpy.calls.count()).toBe(2); for (let i = 0; i + 1 < createSpy.calls.length; i = i + 2) { - expect(createSpy.calls.argsFor(i)[3]).toBe( - createSpy.calls.argsFor(i + 1)[3] - ); + expect(createSpy.calls.argsFor(i)[3]).toBe(createSpy.calls.argsFor(i + 1)[3]); } expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']); }); diff --git a/spec/eslint.config.js b/spec/eslint.config.js index 0f6c0b11c5..e061f8b54b 100644 --- a/spec/eslint.config.js +++ b/spec/eslint.config.js @@ -1,56 +1,56 @@ -const js = require("@eslint/js"); -const globals = require("globals"); +const js = require('@eslint/js'); +const globals = require('globals'); module.exports = [ js.configs.recommended, { languageOptions: { - ecmaVersion: "latest", - sourceType: "module", + ecmaVersion: 'latest', + sourceType: 'module', globals: { ...globals.node, ...globals.jasmine, - mockFetch: "readonly", - Parse: "readonly", - reconfigureServer: "readonly", - createTestUser: "readonly", - jfail: "readonly", - ok: "readonly", - strictEqual: "readonly", - TestObject: "readonly", - Item: "readonly", - Container: "readonly", - equal: "readonly", - expectAsync: "readonly", - notEqual: "readonly", - it_id: "readonly", - fit_id: "readonly", - it_only_db: "readonly", - it_only_mongodb_version: "readonly", - it_only_postgres_version: "readonly", - it_only_node_version: "readonly", - fit_only_mongodb_version: "readonly", - fit_only_postgres_version: "readonly", - fit_only_node_version: "readonly", - it_exclude_dbs: "readonly", - fit_exclude_dbs: "readonly", - describe_only_db: "readonly", - fdescribe_only_db: "readonly", - describe_only: "readonly", - on_db: "readonly", - defaultConfiguration: "readonly", - range: "readonly", - jequal: "readonly", - create: "readonly", - arrayContains: "readonly", - databaseAdapter: "readonly", - databaseURI: "readonly" + mockFetch: 'readonly', + Parse: 'readonly', + reconfigureServer: 'readonly', + createTestUser: 'readonly', + jfail: 'readonly', + ok: 'readonly', + strictEqual: 'readonly', + TestObject: 'readonly', + Item: 'readonly', + Container: 'readonly', + equal: 'readonly', + expectAsync: 'readonly', + notEqual: 'readonly', + it_id: 'readonly', + fit_id: 'readonly', + it_only_db: 'readonly', + it_only_mongodb_version: 'readonly', + it_only_postgres_version: 'readonly', + it_only_node_version: 'readonly', + fit_only_mongodb_version: 'readonly', + fit_only_postgres_version: 'readonly', + fit_only_node_version: 'readonly', + it_exclude_dbs: 'readonly', + fit_exclude_dbs: 'readonly', + describe_only_db: 'readonly', + fdescribe_only_db: 'readonly', + describe_only: 'readonly', + on_db: 'readonly', + defaultConfiguration: 'readonly', + range: 'readonly', + jequal: 'readonly', + create: 'readonly', + arrayContains: 'readonly', + databaseAdapter: 'readonly', + databaseURI: 'readonly', }, }, rules: { - "no-console": "off", - "no-var": "error", - "no-unused-vars": "off", - "no-useless-escape": "off", - } + 'no-console': 'off', + 'no-var': 'error', + 'no-unused-vars': 'off', + 'no-useless-escape': 'off', + }, }, ]; diff --git a/spec/helper.js b/spec/helper.js index 7deb5c495e..c7a35c5c54 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -39,11 +39,11 @@ const ParseServer = require('../lib/index').ParseServer; const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter; const path = require('path'); const TestUtils = require('../lib/TestUtils'); -const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') - .GridFSBucketAdapter; +const GridFSBucketAdapter = + require('../lib/Adapters/Files/GridFSBucketAdapter').GridFSBucketAdapter; const FSAdapter = require('@parse/fs-files-adapter'); -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') - .default; +const PostgresStorageAdapter = + require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default; const RESTController = require('parse/lib/node/RESTController'); @@ -274,7 +274,7 @@ global.afterEachFn = async () => { } else { await databaseAdapter.performInitialization({ VolatileClassesSchemas }); } -} +}; afterEach(global.afterEachFn); afterAll(() => { @@ -415,10 +415,10 @@ function mockShortLivedAuth() { } function mockFetch(mockResponses) { - global.fetch = jasmine.createSpy('fetch').and.callFake((url, options = { }) => { + global.fetch = jasmine.createSpy('fetch').and.callFake((url, options = {}) => { options.method ||= 'GET'; const mockResponse = mockResponses.find( - (mock) => mock.url === url && mock.method === options.method + mock => mock.url === url && mock.method === options.method ); if (mockResponse) { @@ -432,7 +432,6 @@ function mockFetch(mockResponses) { }); } - // This is polluting, but, it makes it way easier to directly port old tests. global.Parse = Parse; global.TestObject = TestObject; diff --git a/spec/index.spec.js b/spec/index.spec.js index 1a2ea889a9..6ac1ee3058 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -638,7 +638,8 @@ describe('server', () => { }); it('should reload masterKey if ttl is set and expired', async () => { - const masterKeySpy = jasmine.createSpy() + const masterKeySpy = jasmine + .createSpy() .and.returnValues(Promise.resolve('firstMasterKey'), Promise.resolve('secondMasterKey')); await reconfigureServer({ @@ -657,7 +658,6 @@ describe('server', () => { expect(config.masterKeyCache.masterKey).toEqual('secondMasterKey'); }); - it('should not fail when Google signin is introduced without the optional clientId', done => { const jwt = require('jsonwebtoken'); const authUtils = require('../lib/Adapters/Auth/utils'); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index fed64c988b..92e25c3941 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -787,21 +787,24 @@ describe('rest create', () => { ); }); - it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)('cannot get object in _GlobalConfig if not masterKey through pointer', async () => { - await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); - const obj2 = new Parse.Object('TestObject'); - obj2.set('globalConfigPointer', { - __type: 'Pointer', - className: '_GlobalConfig', - objectId: 1, - }); - await obj2.save(); - const query = new Parse.Query('TestObject'); - query.include('globalConfigPointer'); - await expectAsync(query.get(obj2.id)).toBeRejectedWithError( - "Clients aren't allowed to perform the get operation on the _GlobalConfig collection." - ); - }); + it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)( + 'cannot get object in _GlobalConfig if not masterKey through pointer', + async () => { + await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); + const obj2 = new Parse.Object('TestObject'); + obj2.set('globalConfigPointer', { + __type: 'Pointer', + className: '_GlobalConfig', + objectId: 1, + }); + await obj2.save(); + const query = new Parse.Query('TestObject'); + query.include('globalConfigPointer'); + await expectAsync(query.get(obj2.id)).toBeRejectedWithError( + "Clients aren't allowed to perform the get operation on the _GlobalConfig collection." + ); + } + ); it('locks down session', done => { let currentUser; diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 140dd405ec..9002603f16 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -2893,7 +2893,9 @@ describe('schemas', () => { object.save({ '!12field': 'field', }) - ).toBeRejectedWith(new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: "!12field"')); + ).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: "!12field"') + ); done(); }); @@ -3726,93 +3728,102 @@ describe('schemas', () => { }); }); - it_id('5d0926b2-2d31-459d-a2b1-23ecc32e72a3')(it_exclude_dbs(['postgres']))('get indexes on startup', done => { - const obj = new Parse.Object('TestObject'); - obj - .save() - .then(() => { - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - }); - }) - .then(() => { - request({ - url: 'http://localhost:8378/1/schemas/TestObject', - headers: masterKeyHeaders, - json: true, - }).then(response => { - expect(response.data.indexes._id_).toBeDefined(); - done(); + it_id('5d0926b2-2d31-459d-a2b1-23ecc32e72a3')(it_exclude_dbs(['postgres']))( + 'get indexes on startup', + done => { + const obj = new Parse.Object('TestObject'); + obj + .save() + .then(() => { + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + }); + }) + .then(() => { + request({ + url: 'http://localhost:8378/1/schemas/TestObject', + headers: masterKeyHeaders, + json: true, + }).then(response => { + expect(response.data.indexes._id_).toBeDefined(); + done(); + }); }); - }); - }); + } + ); - it_id('9f2ba51a-6a9c-4b25-9da0-51c82ac65f90')(it_exclude_dbs(['postgres']))('get compound indexes on startup', done => { - const obj = new Parse.Object('TestObject'); - obj.set('subject', 'subject'); - obj.set('comment', 'comment'); - obj - .save() - .then(() => { - return config.database.adapter.createIndex('TestObject', { - subject: 'text', - comment: 'text', - }); - }) - .then(() => { - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', + it_id('9f2ba51a-6a9c-4b25-9da0-51c82ac65f90')(it_exclude_dbs(['postgres']))( + 'get compound indexes on startup', + done => { + const obj = new Parse.Object('TestObject'); + obj.set('subject', 'subject'); + obj.set('comment', 'comment'); + obj + .save() + .then(() => { + return config.database.adapter.createIndex('TestObject', { + subject: 'text', + comment: 'text', + }); + }) + .then(() => { + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + }); + }) + .then(() => { + request({ + url: 'http://localhost:8378/1/schemas/TestObject', + headers: masterKeyHeaders, + json: true, + }).then(response => { + expect(response.data.indexes._id_).toBeDefined(); + expect(response.data.indexes._id_._id).toEqual(1); + expect(response.data.indexes.subject_text_comment_text).toBeDefined(); + expect(response.data.indexes.subject_text_comment_text.subject).toEqual('text'); + expect(response.data.indexes.subject_text_comment_text.comment).toEqual('text'); + done(); + }); }); - }) - .then(() => { - request({ - url: 'http://localhost:8378/1/schemas/TestObject', - headers: masterKeyHeaders, - json: true, - }).then(response => { - expect(response.data.indexes._id_).toBeDefined(); - expect(response.data.indexes._id_._id).toEqual(1); - expect(response.data.indexes.subject_text_comment_text).toBeDefined(); - expect(response.data.indexes.subject_text_comment_text.subject).toEqual('text'); - expect(response.data.indexes.subject_text_comment_text.comment).toEqual('text'); + } + ); + + it_id('cbd5d897-b938-43a4-8f5a-5d02dd2be9be')(it_exclude_dbs(['postgres']))( + 'cannot update to duplicate value on unique index', + done => { + const index = { + code: 1, + }; + const obj1 = new Parse.Object('UniqueIndexClass'); + obj1.set('code', 1); + const obj2 = new Parse.Object('UniqueIndexClass'); + obj2.set('code', 2); + const adapter = config.database.adapter; + adapter + ._adaptiveCollection('UniqueIndexClass') + .then(collection => { + return collection._ensureSparseUniqueIndexInBackground(index); + }) + .then(() => { + return obj1.save(); + }) + .then(() => { + return obj2.save(); + }) + .then(() => { + obj1.set('code', 2); + return obj1.save(); + }) + .then(done.fail) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); done(); }); - }); - }); - - it_id('cbd5d897-b938-43a4-8f5a-5d02dd2be9be')(it_exclude_dbs(['postgres']))('cannot update to duplicate value on unique index', done => { - const index = { - code: 1, - }; - const obj1 = new Parse.Object('UniqueIndexClass'); - obj1.set('code', 1); - const obj2 = new Parse.Object('UniqueIndexClass'); - obj2.set('code', 2); - const adapter = config.database.adapter; - adapter - ._adaptiveCollection('UniqueIndexClass') - .then(collection => { - return collection._ensureSparseUniqueIndexInBackground(index); - }) - .then(() => { - return obj1.save(); - }) - .then(() => { - return obj2.save(); - }) - .then(() => { - obj1.set('code', 2); - return obj1.save(); - }) - .then(done.fail) - .catch(error => { - expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); - done(); - }); - }); + } + ); }); }); diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js index 8e0e0dafd1..bd45864a6b 100755 --- a/spec/support/CurrentSpecReporter.js +++ b/spec/support/CurrentSpecReporter.js @@ -11,15 +11,15 @@ global.currentSpec = null; */ const flakyTests = [ // Timeout - "ParseLiveQuery handle invalid websocket payload length", + 'ParseLiveQuery handle invalid websocket payload length', // Unhandled promise rejection: TypeError: message.split is not a function - "rest query query internal field", + 'rest query query internal field', // TypeError: Cannot read properties of undefined (reading 'link') - "UserController sendVerificationEmail parseFrameURL not provided uses publicServerURL", + 'UserController sendVerificationEmail parseFrameURL not provided uses publicServerURL', // TypeError: Cannot read properties of undefined (reading 'link') - "UserController sendVerificationEmail parseFrameURL provided uses parseFrameURL and includes the destination in the link parameter", + 'UserController sendVerificationEmail parseFrameURL provided uses parseFrameURL and includes the destination in the link parameter', // Expected undefined to be defined - "Email Verification Token Expiration: sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp", + 'Email Verification Token Expiration: sets the _email_verify_token_expires_at and _email_verify_token fields after user SignUp', ]; /** The minimum execution time in seconds for a test to be considered slow. */ @@ -50,29 +50,34 @@ class CurrentSpecReporter { } } -global.displayTestStats = function() { - const times = Object.values(timerMap).sort((a,b) => b - a).filter(time => time >= slowTestLimit); +global.displayTestStats = function () { + const times = Object.values(timerMap) + .sort((a, b) => b - a) + .filter(time => time >= slowTestLimit); if (times.length > 0) { console.log(`Slow tests with execution time >=${slowTestLimit}s:`); } - times.forEach((time) => { - console.warn(`${time.toFixed(1)}s:`, Object.keys(timerMap).find(key => timerMap[key] === time)); + times.forEach(time => { + console.warn( + `${time.toFixed(1)}s:`, + Object.keys(timerMap).find(key => timerMap[key] === time) + ); }); console.log('\n'); - duplicates.forEach((spec) => { + duplicates.forEach(spec => { console.warn('Duplicate spec: ' + spec); }); console.log('\n'); - Object.keys(retryMap).forEach((spec) => { + Object.keys(retryMap).forEach(spec => { console.warn(`Flaky test: ${spec} failed ${retryMap[spec]} times`); }); console.log('\n'); }; -global.retryFlakyTests = function() { +global.retryFlakyTests = function () { const originalSpecConstructor = jasmine.Spec; - jasmine.Spec = function(attrs) { + jasmine.Spec = function (attrs) { const spec = new originalSpecConstructor(attrs); const originalTestFn = spec.queueableFn.fn; const runOriginalTest = () => { @@ -81,12 +86,12 @@ global.retryFlakyTests = function() { return originalTestFn(); } else { // handle done() callback - return new Promise((resolve) => { + return new Promise(resolve => { originalTestFn(resolve); }); } }; - spec.queueableFn.fn = async function() { + spec.queueableFn.fn = async function () { const isFlaky = flakyTests.includes(spec.result.fullName); const runs = isFlaky ? retries : 1; let exceptionCaught; @@ -101,8 +106,8 @@ global.retryFlakyTests = function() { } catch (exception) { exceptionCaught = exception; } - const failed = !spec.markedPending && - (exceptionCaught || spec.result.failedExpectations.length != 0); + const failed = + !spec.markedPending && (exceptionCaught || spec.result.failedExpectations.length != 0); if (!failed) { break; } @@ -118,6 +123,6 @@ global.retryFlakyTests = function() { }; return spec; }; -} +}; -module.exports = CurrentSpecReporter; \ No newline at end of file +module.exports = CurrentSpecReporter; diff --git a/spec/support/MockLdapServer.js b/spec/support/MockLdapServer.js index 935f0703d6..270a9603cc 100644 --- a/spec/support/MockLdapServer.js +++ b/spec/support/MockLdapServer.js @@ -10,8 +10,9 @@ function newServer(port, dn, provokeSearchError = false, ssl = false) { const server = ssl ? ldapjs.createServer(tlsOptions) : ldapjs.createServer(); server.bind('o=example', function (req, res, next) { - if (req.dn.toString() !== dn || req.credentials !== 'secret') - { return next(new ldapjs.InvalidCredentialsError()); } + if (req.dn.toString() !== dn || req.credentials !== 'secret') { + return next(new ldapjs.InvalidCredentialsError()); + } res.end(); return next(); }); diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index f7a94cd221..858245ecc4 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -40,7 +40,9 @@ describe('Vulnerabilities', () => { it('refuses session token of user with poisoned object ID', async () => { await expectAsync( new Parse.Query(Parse.User).find({ sessionToken: poisonedUser.getSessionToken() }) - ).toBeRejectedWith(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid object ID.')); + ).toBeRejectedWith( + new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid object ID.') + ); await new Parse.Query(Parse.User).find({ sessionToken: innocentUser.getSessionToken() }); }); }); @@ -248,36 +250,39 @@ describe('Vulnerabilities', () => { ); }); - it_id('e8b5f1e1-8326-4c70-b5f4-1e8678dfff8d')(it)('denies creating a hook with polluted data', async () => { - const express = require('express'); - const port = 34567; - const hookServerURL = 'http://localhost:' + port; - const app = express(); - app.use(express.json({ type: '*/*' })); - const server = await new Promise(resolve => { - const res = app.listen(port, undefined, () => resolve(res)); - }); - app.post('/BeforeSave', function (req, res) { - const object = Parse.Object.fromJSON(req.body.object); - object.set('hello', 'world'); - object.set('obj', { - constructor: { - prototype: { - dummy: 0, + it_id('e8b5f1e1-8326-4c70-b5f4-1e8678dfff8d')(it)( + 'denies creating a hook with polluted data', + async () => { + const express = require('express'); + const port = 34567; + const hookServerURL = 'http://localhost:' + port; + const app = express(); + app.use(express.json({ type: '*/*' })); + const server = await new Promise(resolve => { + const res = app.listen(port, undefined, () => resolve(res)); + }); + app.post('/BeforeSave', function (req, res) { + const object = Parse.Object.fromJSON(req.body.object); + object.set('hello', 'world'); + object.set('obj', { + constructor: { + prototype: { + dummy: 0, + }, }, - }, + }); + res.json({ success: object }); }); - 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"}.' - ) - ); - await new Promise(resolve => server.close(resolve)); - }); + 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"}.' + ) + ); + await new Promise(resolve => server.close(resolve)); + } + ); it('denies write request with custom denylist of key/value', async () => { await reconfigureServer({ @@ -488,8 +493,7 @@ describe('Postgres regex sanitizater', () => { const response = await request({ method: 'GET', - url: - "http://localhost:8378/1/classes/_User?where[username][$regex]=A'B'%3BSELECT+PG_SLEEP(3)%3B--", + url: "http://localhost:8378/1/classes/_User?where[username][$regex]=A'B'%3BSELECT+PG_SLEEP(3)%3B--", headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', diff --git a/src/Auth.js b/src/Auth.js index d8bf7e651f..a93f49051f 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -449,11 +449,15 @@ const findUsersWithAuthData = async (config, authData, beforeFind) => { }; const hasMutatedAuthData = (authData, userAuthData) => { - if (!userAuthData) { return { hasMutatedAuthData: true, mutatedAuthData: authData }; } + if (!userAuthData) { + return { hasMutatedAuthData: true, mutatedAuthData: authData }; + } const mutatedAuthData = {}; Object.keys(authData).forEach(provider => { // Anonymous provider is not handled this way - if (provider === 'anonymous') { return; } + if (provider === 'anonymous') { + return; + } const providerData = authData[provider]; const userProviderAuthData = userAuthData[provider]; if (!isDeepStrictEqual(providerData, userProviderAuthData)) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 0050216e2c..9c1fdc7033 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -142,7 +142,9 @@ const filterSensitiveData = ( object: any ) => { let userId = null; - if (auth && auth.user) { userId = auth.user.id; } + if (auth && auth.user) { + userId = auth.user.id; + } // replace protectedFields when using pointer-permissions const perms = @@ -508,9 +510,10 @@ class DatabaseController { var aclGroup = acl || []; return this.loadSchemaIfNeeded(validSchemaController).then(schemaController => { - return (isMaster - ? Promise.resolve() - : schemaController.validatePermission(className, aclGroup, 'update') + return ( + isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, 'update') ) .then(() => { relationUpdates = this.collectRelationUpdates(className, originalQuery.objectId, update); @@ -772,9 +775,10 @@ class DatabaseController { const aclGroup = acl || []; return this.loadSchemaIfNeeded(validSchemaController).then(schemaController => { - return (isMaster - ? Promise.resolve() - : schemaController.validatePermission(className, aclGroup, 'delete') + return ( + isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, 'delete') ).then(() => { if (!isMaster) { query = this.addPointerPermissions( @@ -852,9 +856,10 @@ class DatabaseController { return this.validateClassName(className) .then(() => this.loadSchemaIfNeeded(validSchemaController)) .then(schemaController => { - return (isMaster - ? Promise.resolve() - : schemaController.validatePermission(className, aclGroup, 'create') + return ( + isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, 'create') ) .then(() => schemaController.enforceClassExists(className)) .then(() => schemaController.getOneSchema(className, true)) @@ -1255,9 +1260,10 @@ class DatabaseController { delete sort[fieldName]; } }); - return (isMaster - ? Promise.resolve() - : schemaController.validatePermission(className, aclGroup, op) + return ( + isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, op) ) .then(() => this.reduceRelationKeys(className, query, queryOptions)) .then(() => this.reduceInRelation(className, query, schemaController)) @@ -1592,12 +1598,18 @@ class DatabaseController { schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : schema; - if (!perms) { return null; } + if (!perms) { + return null; + } const protectedFields = perms.protectedFields; - if (!protectedFields) { return null; } + if (!protectedFields) { + return null; + } - if (aclGroup.indexOf(query.objectId) > -1) { return null; } + if (aclGroup.indexOf(query.objectId) > -1) { + return null; + } // for queries where "keys" are set and do not include all 'userField':{field}, // we have to transparently include it, and then remove before returning to client diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index a88c527b00..21ab5efe4c 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -18,7 +18,7 @@ export class FilesController extends AdaptableController { const extname = path.extname(filename); const hasExtension = extname.length > 0; - const mime = (await import('mime')).default + const mime = (await import('mime')).default; if (!hasExtension && contentType && mime.getExtension(contentType)) { filename = filename + '.' + mime.getExtension(contentType); } else if (hasExtension && !contentType) { @@ -34,7 +34,7 @@ export class FilesController extends AdaptableController { return { url: location, name: filename, - } + }; } deleteFile(config, filename) { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index fccadd23ce..ef3b96e594 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -688,10 +688,18 @@ const VolatileClassesSchemas = [ ]; const dbTypeMatchesObjectType = (dbType: SchemaField | string, objectType: SchemaField) => { - if (dbType.type !== objectType.type) { return false; } - if (dbType.targetClass !== objectType.targetClass) { return false; } - if (dbType === objectType.type) { return true; } - if (dbType.type === objectType.type) { return true; } + if (dbType.type !== objectType.type) { + return false; + } + if (dbType.targetClass !== objectType.targetClass) { + return false; + } + if (dbType === objectType.type) { + return true; + } + if (dbType.type === objectType.type) { + return true; + } return false; }; @@ -1042,7 +1050,9 @@ export default class SchemaController { } const fieldType = fields[fieldName]; const error = fieldTypeIsInvalid(fieldType); - if (error) { return { code: error.code, error: error.message }; } + if (error) { + return { code: error.code, error: error.message }; + } if (fieldType.defaultValue !== undefined) { let defaultValueType = getType(fieldType.defaultValue); if (typeof defaultValueType === 'string') { diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 296b7f6868..2a6796052f 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -209,7 +209,7 @@ export class UserController extends AdaptableController { master, installationId, ip, - resendRequest: true + resendRequest: true, }); if (!shouldSend) { return; @@ -222,7 +222,12 @@ export class UserController extends AdaptableController { if (!aUser || aUser.emailVerified) { throw undefined; } - const generate = await this.regenerateEmailVerifyToken(aUser, req.auth?.isMaster, req.auth?.installationId, req.ip); + const generate = await this.regenerateEmailVerifyToken( + aUser, + req.auth?.isMaster, + req.auth?.installationId, + req.ip + ); if (generate) { this.sendVerificationEmail(aUser, req); } diff --git a/src/GraphQL/parseGraphQLUtils.js b/src/GraphQL/parseGraphQLUtils.js index f1194784cb..d7164525a7 100644 --- a/src/GraphQL/parseGraphQLUtils.js +++ b/src/GraphQL/parseGraphQLUtils.js @@ -23,7 +23,9 @@ export const extractKeysAndInclude = selectedFields => { selectedFields = selectedFields.filter(field => !field.includes('__typename')); // Handles "id" field for both current and included objects selectedFields = selectedFields.map(field => { - if (field === 'id') { return 'objectId'; } + if (field === 'id') { + return 'objectId'; + } return field.endsWith('.id') ? `${field.substring(0, field.lastIndexOf('.id'))}.objectId` : field; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index ca2f7cc2ee..faffb754ac 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -15,30 +15,26 @@ module.exports.SchemaOptions = { }, definitions: { env: 'PARSE_SERVER_SCHEMA_DEFINITIONS', - help: - 'Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema', + help: 'Rest representation on Parse.Schema https://docs.parseplatform.org/rest/guide/#adding-a-schema', required: true, action: parsers.objectParser, default: [], }, deleteExtraFields: { env: 'PARSE_SERVER_SCHEMA_DELETE_EXTRA_FIELDS', - help: - 'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.', + help: 'Is true if Parse Server should delete any fields not defined in a schema definition. This should only be used during development.', action: parsers.booleanParser, default: false, }, lockSchemas: { env: 'PARSE_SERVER_SCHEMA_LOCK_SCHEMAS', - help: - 'Is true if Parse Server will reject any attempts to modify the schema while the server is running.', + help: 'Is true if Parse Server will reject any attempts to modify the schema while the server is running.', action: parsers.booleanParser, default: false, }, recreateModifiedFields: { env: 'PARSE_SERVER_SCHEMA_RECREATE_MODIFIED_FIELDS', - help: - 'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.', + help: 'Is true if Parse Server should recreate any fields that are different between the current database schema and theschema definition. This should only be used during development.', action: parsers.booleanParser, default: false, }, @@ -70,8 +66,7 @@ module.exports.ParseServerOptions = { }, allowExpiredAuthDataToken: { env: 'PARSE_SERVER_ALLOW_EXPIRED_AUTH_DATA_TOKEN', - help: - 'Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`.', + help: 'Allow a user to log in even if the 3rd party authentication token that was used to sign in to their account has expired. If this is set to `false`, then the token will be validated every time the user signs in to their account. This refers to the token that is stored in the `_User.authData` field. Defaults to `false`.', action: parsers.booleanParser, default: false, }, @@ -82,8 +77,7 @@ module.exports.ParseServerOptions = { }, allowOrigin: { env: 'PARSE_SERVER_ALLOW_ORIGIN', - help: - 'Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins.', + help: 'Sets origins for Access-Control-Allow-Origin. This can be a string for a single origin or an array of strings for multiple origins.', action: parsers.arrayParser, }, analyticsAdapter: { @@ -102,8 +96,7 @@ module.exports.ParseServerOptions = { }, auth: { env: 'PARSE_SERVER_AUTH_PROVIDERS', - help: - 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', + help: 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', }, cacheAdapter: { env: 'PARSE_SERVER_CACHE_ADAPTER', @@ -142,15 +135,13 @@ module.exports.ParseServerOptions = { }, convertEmailToLowercase: { env: 'PARSE_SERVER_CONVERT_EMAIL_TO_LOWERCASE', - help: - 'Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`.', + help: 'Optional. If set to `true`, the `email` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `email` property is stored as set, without any case modifications. Default is `false`.', action: parsers.booleanParser, default: false, }, convertUsernameToLowercase: { env: 'PARSE_SERVER_CONVERT_USERNAME_TO_LOWERCASE', - help: - 'Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`.', + help: 'Optional. If set to `true`, the `username` property of a user is automatically converted to lowercase before being stored in the database. Consequently, queries must match the case as stored in the database, which would be lowercase in this scenario. If `false`, the `username` property is stored as set, without any case modifications. Default is `false`.', action: parsers.booleanParser, default: false, }, @@ -163,8 +154,7 @@ module.exports.ParseServerOptions = { }, databaseAdapter: { env: 'PARSE_SERVER_DATABASE_ADAPTER', - help: - 'Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.', + help: 'Adapter module for the database; any options that are not explicitly described here are passed directly to the database client.', action: parsers.moduleOrObjectParser, }, databaseOptions: { @@ -187,8 +177,7 @@ module.exports.ParseServerOptions = { }, directAccess: { env: 'PARSE_SERVER_DIRECT_ACCESS', - help: - 'Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

\u26A0\uFE0F In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`.', + help: 'Set to `true` if Parse requests within the same Node.js environment as Parse Server should be routed to Parse Server directly instead of via the HTTP interface. Default is `false`.

If set to `false` then Parse requests within the same Node.js environment as Parse Server are executed as HTTP requests sent to Parse Server via the `serverURL`. For example, a `Parse.Query` in Cloud Code is calling Parse Server via a HTTP request. The server is essentially making a HTTP request to itself, unnecessarily using network resources such as network ports.

\u26A0\uFE0F In environments where multiple Parse Server instances run behind a load balancer and Parse requests within the current Node.js environment should be routed via the load balancer and distributed as HTTP requests among all instances via the `serverURL`, this should be set to `false`.', action: parsers.booleanParser, default: true, }, @@ -203,15 +192,13 @@ module.exports.ParseServerOptions = { }, emailVerifyTokenReuseIfValid: { env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID', - help: - 'Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`.', + help: 'Set to `true` if a email verification token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.
Requires option `verifyUserEmails: true`.', action: parsers.booleanParser, default: false, }, emailVerifyTokenValidityDuration: { env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION', - help: - 'Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`.', + help: 'Set the validity duration of the email verification token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.
Requires option `verifyUserEmails: true`.', action: parsers.numberParser('emailVerifyTokenValidityDuration'), }, enableAnonymousUsers: { @@ -222,8 +209,7 @@ module.exports.ParseServerOptions = { }, enableCollationCaseComparison: { env: 'PARSE_SERVER_ENABLE_COLLATION_CASE_COMPARISON', - help: - 'Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.', + help: 'Optional. If set to `true`, the collation rule of case comparison for queries and indexes is enabled. Enable this option to run Parse Server with MongoDB Atlas Serverless or AWS Amazon DocumentDB. If `false`, the collation rule of case comparison is disabled. Default is `false`.', action: parsers.booleanParser, default: false, }, @@ -235,15 +221,13 @@ module.exports.ParseServerOptions = { }, enableInsecureAuthAdapters: { env: 'PARSE_SERVER_ENABLE_INSECURE_AUTH_ADAPTERS', - help: - 'Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.', + help: 'Enable (or disable) insecure auth adapters, defaults to true. Insecure auth adapters are deprecated and it is recommended to disable them.', action: parsers.booleanParser, default: true, }, encodeParseObjectInCloudFunction: { env: 'PARSE_SERVER_ENCODE_PARSE_OBJECT_IN_CLOUD_FUNCTION', - help: - 'If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.

\u2139\uFE0F The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.', + help: 'If set to `true`, a `Parse.Object` that is in the payload when calling a Cloud Function will be converted to an instance of `Parse.Object`. If `false`, the object will not be converted and instead be a plain JavaScript object, which contains the raw data of a `Parse.Object` but is not an actual instance of `Parse.Object`. Default is `false`.

\u2139\uFE0F The expected behavior would be that the object is converted to an instance of `Parse.Object`, so you would normally set this option to `true`. The default is `false` because this is a temporary option that has been introduced to avoid a breaking change when fixing a bug where JavaScript objects are not converted to actual instances of `Parse.Object`.', action: parsers.booleanParser, default: true, }, @@ -259,15 +243,13 @@ module.exports.ParseServerOptions = { }, expireInactiveSessions: { env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', - help: - 'Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.', + help: 'Sets whether we should expire the inactive sessions, defaults to true. If false, all new sessions are created with no expiration date.', action: parsers.booleanParser, default: true, }, extendSessionOnUse: { env: 'PARSE_SERVER_EXTEND_SESSION_ON_USE', - help: - "Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.", + help: "Whether Parse Server should automatically extend a valid session by the sessionLength. In order to reduce the number of session updates in the database, a session will only be extended when a request is received after at least half of the current session's lifetime has passed.", action: parsers.booleanParser, default: false, }, @@ -303,8 +285,7 @@ module.exports.ParseServerOptions = { }, idempotencyOptions: { env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS', - help: - 'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.', + help: 'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.', action: parsers.objectParser, type: 'IdempotencyOptions', default: {}, @@ -353,14 +334,12 @@ module.exports.ParseServerOptions = { }, maintenanceKey: { env: 'PARSE_SERVER_MAINTENANCE_KEY', - help: - '(Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.', + help: '(Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.

\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.', required: true, }, maintenanceKeyIps: { env: 'PARSE_SERVER_MAINTENANCE_KEY_IPS', - help: - "(Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key.", + help: "(Optional) Restricts the use of maintenance key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the maintenance key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the maintenance key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the maintenance key.", action: parsers.arrayParser, default: ['127.0.0.1', '::1'], }, @@ -371,15 +350,13 @@ module.exports.ParseServerOptions = { }, masterKeyIps: { env: 'PARSE_SERVER_MASTER_KEY_IPS', - help: - "(Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key.", + help: "(Optional) Restricts the use of master key permissions to a list of IP addresses or ranges.

This option accepts a list of single IP addresses, for example `['10.0.0.1', '10.0.0.2']`. You can also use CIDR notation to specify an IP address range, for example `['10.0.1.0/24']`.

Special scenarios:
- Setting an empty array `[]` means that the master key cannot be used even in Parse Server Cloud Code. This value cannot be set via an environment variable as there is no way to pass an empty array to Parse Server via an environment variable.
- Setting `['0.0.0.0/0', '::0']` means to allow any IPv4 and IPv6 address to use the master key and effectively disables the IP filter.

Considerations:
- IPv4 and IPv6 addresses are not compared against each other. Each IP version (IPv4 and IPv6) needs to be considered separately. For example, `['0.0.0.0/0']` allows any IPv4 address and blocks every IPv6 address. Conversely, `['::0']` allows any IPv6 address and blocks every IPv4 address.
- Keep in mind that the IP version in use depends on the network stack of the environment in which Parse Server runs. A local environment may use a different IP version than a remote environment. For example, it's possible that locally the value `['0.0.0.0/0']` allows the request IP because the environment is using IPv4, but when Parse Server is deployed remotely the request IP is blocked because the remote environment is using IPv6.
- When setting the option via an environment variable the notation is a comma-separated string, for example `\"0.0.0.0/0,::0\"`.
- IPv6 zone indices (`%` suffix) are not supported, for example `fe80::1%eth0`, `fe80::1%1` or `::1%lo`.

Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server instance on which Parse Server runs, is allowed to use the master key.", action: parsers.arrayParser, default: ['127.0.0.1', '::1'], }, masterKeyTtl: { env: 'PARSE_SERVER_MASTER_KEY_TTL', - help: - '(Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server.', + help: '(Optional) The duration in seconds for which the current `masterKey` is being used before it is requested again if `masterKey` is set to a function. If `masterKey` is not set to a function, this option has no effect. Default is `0`, which means the master key is requested by invoking the `masterKey` function every time the master key is used internally by Parse Server.', action: parsers.numberParser('masterKeyTtl'), }, maxLimit: { @@ -389,8 +366,7 @@ module.exports.ParseServerOptions = { }, maxLogFiles: { env: 'PARSE_SERVER_MAX_LOG_FILES', - help: - "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", + help: "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", action: parsers.numberOrStringParser('maxLogFiles'), }, maxUploadSize: { @@ -457,15 +433,13 @@ module.exports.ParseServerOptions = { }, preventLoginWithUnverifiedEmail: { env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', - help: - 'Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`.', + help: 'Set to `true` to prevent a user from logging in if the email has not yet been verified and email verification is required.

Default is `false`.
Requires option `verifyUserEmails: true`.', action: parsers.booleanParser, default: false, }, preventSignupWithUnverifiedEmail: { env: 'PARSE_SERVER_PREVENT_SIGNUP_WITH_UNVERIFIED_EMAIL', - help: - "If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`.", + help: "If set to `true` it prevents a user from signing up if the email has not yet been verified and email verification is required. In that case the server responds to the sign-up with HTTP status 400 and a Parse Error 205 `EMAIL_NOT_FOUND`. If set to `false` the server responds with HTTP status 200, and client SDKs return an unauthenticated Parse User without session token. In that case subsequent requests fail until the user's email address is verified.

Default is `false`.
Requires option `verifyUserEmails: true`.", action: parsers.booleanParser, default: false, }, @@ -485,14 +459,12 @@ module.exports.ParseServerOptions = { }, push: { env: 'PARSE_SERVER_PUSH', - help: - 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', + help: 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', action: parsers.objectParser, }, rateLimit: { env: 'PARSE_SERVER_RATE_LIMIT', - help: - "Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

\u2139\uFE0F Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.", + help: "Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

\u2139\uFE0F Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.", action: parsers.arrayParser, type: 'RateLimitOptions[]', default: [], @@ -503,8 +475,7 @@ module.exports.ParseServerOptions = { }, requestKeywordDenylist: { env: 'PARSE_SERVER_REQUEST_KEYWORD_DENYLIST', - help: - 'An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.', + help: 'An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns.', action: parsers.arrayParser, default: [ { @@ -525,8 +496,7 @@ module.exports.ParseServerOptions = { }, revokeSessionOnPasswordReset: { env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', - help: - "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", + help: "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", action: parsers.booleanParser, default: true, }, @@ -551,8 +521,7 @@ module.exports.ParseServerOptions = { }, sendUserEmailVerification: { env: 'PARSE_SERVER_SEND_USER_EMAIL_VERIFICATION', - help: - 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
', + help: 'Set to `false` to prevent sending of verification email. Supports a function with a return value of `true` or `false` for conditional email sending.

Default is `true`.
', default: true, }, serverCloseComplete: { @@ -582,15 +551,13 @@ module.exports.ParseServerOptions = { }, trustProxy: { env: 'PARSE_SERVER_TRUST_PROXY', - help: - 'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`.', + help: 'The trust proxy settings. It is important to understand the exact setup of the reverse proxy, since this setting will trust values provided in the Parse Server API request. See the express trust proxy settings documentation. Defaults to `false`.', action: parsers.objectParser, default: [], }, userSensitiveFields: { env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS', - help: - 'Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields', + help: 'Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields', action: parsers.arrayParser, }, verbose: { @@ -600,8 +567,7 @@ module.exports.ParseServerOptions = { }, verifyUserEmails: { env: 'PARSE_SERVER_VERIFY_USER_EMAILS', - help: - 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.

Default is `false`.', + help: 'Set to `true` to require users to verify their email address to complete the sign-up process. Supports a function with a return value of `true` or `false` for conditional verification.

Default is `false`.', default: false, }, webhookKey: { @@ -612,64 +578,54 @@ module.exports.ParseServerOptions = { module.exports.RateLimitOptions = { errorResponseMessage: { env: 'PARSE_SERVER_RATE_LIMIT_ERROR_RESPONSE_MESSAGE', - help: - 'The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.', + help: 'The error message that should be returned in the body of the HTTP 429 response when the rate limit is hit. Default is `Too many requests.`.', default: 'Too many requests.', }, includeInternalRequests: { env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_INTERNAL_REQUESTS', - help: - 'Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.', + help: 'Optional, if `true` the rate limit will also apply to requests that are made in by Cloud Code, default is `false`. Note that a public Cloud Code function that triggers internal requests may circumvent rate limiting and be vulnerable to attacks.', action: parsers.booleanParser, default: false, }, includeMasterKey: { env: 'PARSE_SERVER_RATE_LIMIT_INCLUDE_MASTER_KEY', - help: - 'Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.', + help: 'Optional, if `true` the rate limit will also apply to requests using the `masterKey`, default is `false`. Note that a public Cloud Code function that triggers internal requests using the `masterKey` may circumvent rate limiting and be vulnerable to attacks.', action: parsers.booleanParser, default: false, }, redisUrl: { env: 'PARSE_SERVER_RATE_LIMIT_REDIS_URL', - help: - 'Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests.', + help: 'Optional, the URL of the Redis server to store rate limit data. This allows to rate limit requests for multiple servers by calculating the sum of all requests across all servers. This is useful if multiple servers are processing requests behind a load balancer. For example, the limit of 10 requests is reached if each of 2 servers processed 5 requests.', }, requestCount: { env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_COUNT', - help: - 'The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied.', + help: 'The number of requests that can be made per IP address within the time window set in `requestTimeWindow` before the rate limit is applied.', action: parsers.numberParser('requestCount'), }, requestMethods: { env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_METHODS', - help: - 'Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.', + help: 'Optional, the HTTP request methods to which the rate limit should be applied, default is all methods.', action: parsers.arrayParser, }, requestPath: { env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_PATH', - help: - 'The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html', + help: 'The path of the API route to be rate limited. Route paths, in combination with a request method, define the endpoints at which requests can be made. Route paths can be strings, string patterns, or regular expression. See: https://expressjs.com/en/guide/routing.html', required: true, }, requestTimeWindow: { env: 'PARSE_SERVER_RATE_LIMIT_REQUEST_TIME_WINDOW', - help: - 'The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.', + help: 'The window of time in milliseconds within which the number of requests set in `requestCount` can be made before the rate limit is applied.', action: parsers.numberParser('requestTimeWindow'), }, zone: { env: 'PARSE_SERVER_RATE_LIMIT_ZONE', - help: - "The type of rate limit to apply. The following types are supported:

- `global`: rate limit based on the number of requests made by all users
- `ip`: rate limit based on the IP address of the request
- `user`: rate limit based on the user ID of the request
- `session`: rate limit based on the session token of the request


:default: 'ip'", + help: "The type of rate limit to apply. The following types are supported:

- `global`: rate limit based on the number of requests made by all users
- `ip`: rate limit based on the IP address of the request
- `user`: rate limit based on the user ID of the request
- `session`: rate limit based on the session token of the request


:default: 'ip'", }, }; module.exports.SecurityOptions = { checkGroups: { env: 'PARSE_SERVER_SECURITY_CHECK_GROUPS', - help: - 'The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`.', + help: 'The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`.', action: parsers.arrayParser, }, enableCheck: { @@ -680,8 +636,7 @@ module.exports.SecurityOptions = { }, enableCheckLog: { env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK_LOG', - help: - 'Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.', + help: 'Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.', action: parsers.booleanParser, default: false, }, @@ -709,28 +664,24 @@ module.exports.PagesOptions = { }, enableRouter: { env: 'PARSE_SERVER_PAGES_ENABLE_ROUTER', - help: - 'Is true if the pages router should be enabled; this is required for any of the pages options to take effect.', + help: 'Is true if the pages router should be enabled; this is required for any of the pages options to take effect.', action: parsers.booleanParser, default: false, }, forceRedirect: { env: 'PARSE_SERVER_PAGES_FORCE_REDIRECT', - help: - 'Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).', + help: 'Is true if responses should always be redirects and never content, false if the response type should depend on the request type (GET request -> content response; POST request -> redirect response).', action: parsers.booleanParser, default: false, }, localizationFallbackLocale: { env: 'PARSE_SERVER_PAGES_LOCALIZATION_FALLBACK_LOCALE', - help: - 'The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file.', + help: 'The fallback locale for localization if no matching translation is provided for the given locale. This is only relevant when providing translation resources via JSON file.', default: 'en', }, localizationJsonPath: { env: 'PARSE_SERVER_PAGES_LOCALIZATION_JSON_PATH', - help: - 'The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale.', + help: 'The path to the JSON file for localization; the translations will be used to fill template placeholders according to the locale.', }, pagesEndpoint: { env: 'PARSE_SERVER_PAGES_PAGES_ENDPOINT', @@ -739,14 +690,12 @@ module.exports.PagesOptions = { }, pagesPath: { env: 'PARSE_SERVER_PAGES_PAGES_PATH', - help: - "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory.", + help: "The path to the pages directory; this also defines where the static endpoint '/apps' points to. Default is the './public/' directory.", default: './public', }, placeholders: { env: 'PARSE_SERVER_PAGES_PLACEHOLDERS', - help: - 'The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function.', + help: 'The placeholder keys and values which will be filled in pages; this can be a simple object or a callback function.', action: parsers.objectParser, default: {}, }, @@ -873,30 +822,25 @@ module.exports.LiveQueryOptions = { module.exports.LiveQueryServerOptions = { appId: { env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', - help: - 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', + help: 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', }, cacheTimeout: { env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', - help: - "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", + help: "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", action: parsers.numberParser('cacheTimeout'), }, keyPairs: { env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', - help: - 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', + help: 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', action: parsers.objectParser, }, logLevel: { env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', - help: - 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', + help: 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', }, masterKey: { env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', - help: - 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', + help: 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', }, port: { env: 'PARSE_LIVE_QUERY_SERVER_PORT', @@ -920,13 +864,11 @@ module.exports.LiveQueryServerOptions = { }, serverURL: { env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', - help: - 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', + help: 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', }, websocketTimeout: { env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', - help: - 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', + help: 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', action: parsers.numberParser('websocketTimeout'), }, wssAdapter: { @@ -938,15 +880,13 @@ module.exports.LiveQueryServerOptions = { module.exports.IdempotencyOptions = { paths: { env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS', - help: - 'An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.', + help: 'An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.', action: parsers.arrayParser, default: [], }, ttl: { env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL', - help: - 'The duration in seconds after which a request record is discarded from the database, defaults to 300s.', + help: 'The duration in seconds after which a request record is discarded from the database, defaults to 300s.', action: parsers.numberParser('ttl'), default: 300, }, @@ -954,20 +894,17 @@ module.exports.IdempotencyOptions = { module.exports.AccountLockoutOptions = { duration: { env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', - help: - 'Set the duration in minutes that a locked-out account remains locked out before automatically becoming unlocked.

Valid values are greater than `0` and less than `100000`.', + help: 'Set the duration in minutes that a locked-out account remains locked out before automatically becoming unlocked.

Valid values are greater than `0` and less than `100000`.', action: parsers.numberParser('duration'), }, threshold: { env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD', - help: - 'Set the number of failed sign-in attempts that will cause a user account to be locked. If the account is locked. The account will unlock after the duration set in the `duration` option has passed and no further login attempts have been made.

Valid values are greater than `0` and less than `1000`.', + help: 'Set the number of failed sign-in attempts that will cause a user account to be locked. If the account is locked. The account will unlock after the duration set in the `duration` option has passed and no further login attempts have been made.

Valid values are greater than `0` and less than `1000`.', action: parsers.numberParser('threshold'), }, unlockOnPasswordReset: { env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_UNLOCK_ON_PASSWORD_RESET', - help: - 'Set to `true` if the account should be unlocked after a successful password reset.

Default is `false`.
Requires options `duration` and `threshold` to be set.', + help: 'Set to `true` if the account should be unlocked after a successful password reset.

Default is `false`.
Requires options `duration` and `threshold` to be set.', action: parsers.booleanParser, default: false, }, @@ -975,57 +912,48 @@ module.exports.AccountLockoutOptions = { module.exports.PasswordPolicyOptions = { doNotAllowUsername: { env: 'PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME', - help: - 'Set to `true` to disallow the username as part of the password.

Default is `false`.', + help: 'Set to `true` to disallow the username as part of the password.

Default is `false`.', action: parsers.booleanParser, default: false, }, maxPasswordAge: { env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE', - help: - 'Set the number of days after which a password expires. Login attempts fail if the user does not reset the password before expiration.', + help: 'Set the number of days after which a password expires. Login attempts fail if the user does not reset the password before expiration.', action: parsers.numberParser('maxPasswordAge'), }, maxPasswordHistory: { env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY', - help: - 'Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to `0`, no previous passwords will be considered.

Valid values are >= `0` and <= `20`.
Default is `0`.', + help: 'Set the number of previous password that will not be allowed to be set as new password. If the option is not set or set to `0`, no previous passwords will be considered.

Valid values are >= `0` and <= `20`.
Default is `0`.', action: parsers.numberParser('maxPasswordHistory'), }, resetPasswordSuccessOnInvalidEmail: { env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_PASSWORD_SUCCESS_ON_INVALID_EMAIL', - help: - 'Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.', + help: 'Set to `true` if a request to reset the password should return a success response even if the provided email address is invalid, or `false` if the request should return an error response if the email address is invalid.

Default is `true`.', action: parsers.booleanParser, default: true, }, resetTokenReuseIfValid: { env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID', - help: - 'Set to `true` if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.', + help: 'Set to `true` if a password reset token should be reused in case another token is requested but there is a token that is still valid, i.e. has not expired. This avoids the often observed issue that a user requests multiple emails and does not know which link contains a valid token because each newly generated token would invalidate the previous token.

Default is `false`.', action: parsers.booleanParser, default: false, }, resetTokenValidityDuration: { env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION', - help: - 'Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.', + help: 'Set the validity duration of the password reset token in seconds after which the token expires. The token is used in the link that is set in the email. After the token expires, the link becomes invalid and a new link has to be sent. If the option is not set or set to `undefined`, then the token never expires.

For example, to expire the token after 2 hours, set a value of 7200 seconds (= 60 seconds * 60 minutes * 2 hours).

Default is `undefined`.', action: parsers.numberParser('resetTokenValidityDuration'), }, validationError: { env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATION_ERROR', - help: - 'Set the error message to be sent.

Default is `Password does not meet the Password Policy requirements.`', + help: 'Set the error message to be sent.

Default is `Password does not meet the Password Policy requirements.`', }, validatorCallback: { env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK', - help: - 'Set a callback function to validate a password to be accepted.

If used in combination with `validatorPattern`, the password must pass both to be accepted.', + help: 'Set a callback function to validate a password to be accepted.

If used in combination with `validatorPattern`, the password must pass both to be accepted.', }, validatorPattern: { env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN', - help: - 'Set the regular expression validation pattern a password must match to be accepted.

If used in combination with `validatorCallback`, the password must pass both to be accepted.', + help: 'Set the regular expression validation pattern a password must match to be accepted.

If used in combination with `validatorCallback`, the password must pass both to be accepted.', }, }; module.exports.FileUploadOptions = { @@ -1049,8 +977,7 @@ module.exports.FileUploadOptions = { }, fileExtensions: { env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS', - help: - "Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.

It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.

Defaults to `^(?!(h|H)(t|T)(m|M)(l|L)?$)` which allows any file extension except HTML files.", + help: "Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.

It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.

Defaults to `^(?!(h|H)(t|T)(m|M)(l|L)?$)` which allows any file extension except HTML files.", action: parsers.arrayParser, default: ['^(?!(h|H)(t|T)(m|M)(l|L)?$)'], }, @@ -1058,51 +985,43 @@ module.exports.FileUploadOptions = { module.exports.DatabaseOptions = { autoSelectFamily: { env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY', - help: - 'The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address.', + help: 'The MongoDB driver option to set whether the socket attempts to connect to IPv6 and IPv4 addresses until a connection is established. If available, the driver will select the first IPv6 address.', action: parsers.booleanParser, }, autoSelectFamilyAttemptTimeout: { env: 'PARSE_SERVER_DATABASE_AUTO_SELECT_FAMILY_ATTEMPT_TIMEOUT', - help: - 'The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.', + help: 'The MongoDB driver option to specify the amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the autoSelectFamily option. If set to a positive integer less than 10, the value 10 is used instead.', action: parsers.numberParser('autoSelectFamilyAttemptTimeout'), }, connectTimeoutMS: { env: 'PARSE_SERVER_DATABASE_CONNECT_TIMEOUT_MS', - help: - 'The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout.', + help: 'The MongoDB driver option to specify the amount of time, in milliseconds, to wait to establish a single TCP socket connection to the server before raising an error. Specifying 0 disables the connection timeout.', action: parsers.numberParser('connectTimeoutMS'), }, enableSchemaHooks: { env: 'PARSE_SERVER_DATABASE_ENABLE_SCHEMA_HOOKS', - help: - 'Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.', + help: 'Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.', action: parsers.booleanParser, default: false, }, maxPoolSize: { env: 'PARSE_SERVER_DATABASE_MAX_POOL_SIZE', - help: - 'The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver.', + help: 'The MongoDB driver option to set the maximum number of opened, cached, ready-to-use database connections maintained by the driver.', action: parsers.numberParser('maxPoolSize'), }, maxStalenessSeconds: { env: 'PARSE_SERVER_DATABASE_MAX_STALENESS_SECONDS', - help: - 'The MongoDB driver option to set the maximum replication lag for reads from secondary nodes.', + help: 'The MongoDB driver option to set the maximum replication lag for reads from secondary nodes.', action: parsers.numberParser('maxStalenessSeconds'), }, maxTimeMS: { env: 'PARSE_SERVER_DATABASE_MAX_TIME_MS', - help: - 'The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor.', + help: 'The MongoDB driver option to set a cumulative time limit in milliseconds for processing operations on a cursor.', action: parsers.numberParser('maxTimeMS'), }, minPoolSize: { env: 'PARSE_SERVER_DATABASE_MIN_POOL_SIZE', - help: - 'The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver.', + help: 'The MongoDB driver option to set the minimum number of opened, cached, ready-to-use database connections maintained by the driver.', action: parsers.numberParser('minPoolSize'), }, retryWrites: { @@ -1112,14 +1031,12 @@ module.exports.DatabaseOptions = { }, schemaCacheTtl: { env: 'PARSE_SERVER_DATABASE_SCHEMA_CACHE_TTL', - help: - 'The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires.', + help: 'The duration in seconds after which the schema cache expires and will be refetched from the database. Use this option if using multiple Parse Servers instances connected to the same database. A low duration will cause the schema cache to be updated too often, causing unnecessary database reads. A high duration will cause the schema to be updated too rarely, increasing the time required until schema changes propagate to all server instances. This feature can be used as an alternative or in conjunction with the option `enableSchemaHooks`. Default is infinite which means the schema cache never expires.', action: parsers.numberParser('schemaCacheTtl'), }, socketTimeoutMS: { env: 'PARSE_SERVER_DATABASE_SOCKET_TIMEOUT_MS', - help: - 'The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout.', + help: 'The MongoDB driver option to specify the amount of time, in milliseconds, spent attempting to send or receive on a socket before timing out. Specifying 0 means no timeout.', action: parsers.numberParser('socketTimeoutMS'), }, }; @@ -1143,20 +1060,17 @@ module.exports.LogLevels = { }, triggerAfter: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_AFTER', - help: - 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`.', + help: 'Log level used by the Cloud Code Triggers `afterSave`, `afterDelete`, `afterFind`, `afterLogout`. Default is `info`.', default: 'info', }, triggerBeforeError: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_ERROR', - help: - 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`.', + help: 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on error. Default is `error`.', default: 'error', }, triggerBeforeSuccess: { env: 'PARSE_SERVER_LOG_LEVELS_TRIGGER_BEFORE_SUCCESS', - help: - 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`.', + help: 'Log level used by the Cloud Code Triggers `beforeSave`, `beforeDelete`, `beforeFind`, `beforeLogin` on success. Default is `info`.', default: 'info', }, }; diff --git a/src/RestQuery.js b/src/RestQuery.js index 621700984b..323a83f2bf 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -749,7 +749,12 @@ _UnsafeRestQuery.prototype.runFind = async function (options = {}) { if (options.op) { findOptions.op = options.op; } - const results = await this.config.database.find(this.className, this.restWhere, findOptions, this.auth); + const results = await this.config.database.find( + this.className, + this.restWhere, + findOptions, + this.auth + ); if (this.className === '_User' && !findOptions.explain) { for (var result of results) { this.cleanResultAuthData(result); diff --git a/src/RestWrite.js b/src/RestWrite.js index 78dd8c8878..e149632b7d 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -523,7 +523,9 @@ RestWrite.prototype.ensureUniqueAuthDataId = async function () { key => this.data.authData[key] && this.data.authData[key].id ); - if (!hasAuthDataId) { return; } + if (!hasAuthDataId) { + return; + } const r = await Auth.findUsersWithAuthData(this.config, this.data.authData); const results = this.filteredObjectsByACL(r); @@ -566,7 +568,6 @@ RestWrite.prototype.handleAuthData = async function (authData) { // User found with provided authData if (results.length === 1) { - this.storage.authProvider = Object.keys(authData).join(','); const { hasMutatedAuthData, mutatedAuthData } = Auth.hasMutatedAuthData( @@ -828,7 +829,9 @@ RestWrite.prototype._validateEmail = function () { }; RestWrite.prototype._validatePasswordPolicy = function () { - if (!this.config.passwordPolicy) { return Promise.resolve(); } + if (!this.config.passwordPolicy) { + return Promise.resolve(); + } return this._validatePasswordRequirements().then(() => { return this._validatePasswordHistory(); }); @@ -862,18 +865,20 @@ RestWrite.prototype._validatePasswordRequirements = function () { if (this.config.passwordPolicy.doNotAllowUsername === true) { if (this.data.username) { // username is not passed during password reset - if (this.data.password.indexOf(this.data.username) >= 0) - { return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, containsUsernameError)); } + if (this.data.password.indexOf(this.data.username) >= 0) { + return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, containsUsernameError)); + } } else { // retrieve the User object using objectId during password reset return this.config.database.find('_User', { objectId: this.objectId() }).then(results => { if (results.length != 1) { throw undefined; } - if (this.data.password.indexOf(results[0].username) >= 0) - { return Promise.reject( - new Parse.Error(Parse.Error.VALIDATION_ERROR, containsUsernameError) - ); } + if (this.data.password.indexOf(results[0].username) >= 0) { + return Promise.reject( + new Parse.Error(Parse.Error.VALIDATION_ERROR, containsUsernameError) + ); + } return Promise.resolve(); }); } @@ -897,19 +902,21 @@ RestWrite.prototype._validatePasswordHistory = function () { } const user = results[0]; let oldPasswords = []; - if (user._password_history) - { oldPasswords = _.take( - user._password_history, - this.config.passwordPolicy.maxPasswordHistory - 1 - ); } + if (user._password_history) { + oldPasswords = _.take( + user._password_history, + this.config.passwordPolicy.maxPasswordHistory - 1 + ); + } oldPasswords.push(user.password); const newPassword = this.data.password; // compare the new password hash with all old password hashes const promises = oldPasswords.map(function (hash) { return passwordCrypto.compare(newPassword, hash).then(result => { - if (result) - // reject if there is a match - { return Promise.reject('REPEAT_PASSWORD'); } + if (result) { + // reject if there is a match + return Promise.reject('REPEAT_PASSWORD'); + } return Promise.resolve(); }); }); @@ -919,14 +926,15 @@ RestWrite.prototype._validatePasswordHistory = function () { return Promise.resolve(); }) .catch(err => { - if (err === 'REPEAT_PASSWORD') - // a match was found - { return Promise.reject( - new Parse.Error( - Parse.Error.VALIDATION_ERROR, - `New password should not be the same as last ${this.config.passwordPolicy.maxPasswordHistory} passwords.` - ) - ); } + if (err === 'REPEAT_PASSWORD') { + // a match was found + return Promise.reject( + new Parse.Error( + Parse.Error.VALIDATION_ERROR, + `New password should not be the same as last ${this.config.passwordPolicy.maxPasswordHistory} passwords.` + ) + ); + } throw err; }); }); @@ -960,10 +968,16 @@ RestWrite.prototype.createSessionTokenIfNeeded = async function () { // Get verification conditions which can be booleans or functions; the purpose of this async/await // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the // conditional statement below, as a developer may decide to execute expensive operations in them - const verifyUserEmails = async () => this.config.verifyUserEmails === true || (typeof this.config.verifyUserEmails === 'function' && await Promise.resolve(this.config.verifyUserEmails(request)) === true); - const preventLoginWithUnverifiedEmail = async () => this.config.preventLoginWithUnverifiedEmail === true || (typeof this.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(this.config.preventLoginWithUnverifiedEmail(request)) === true); + const verifyUserEmails = async () => + this.config.verifyUserEmails === true || + (typeof this.config.verifyUserEmails === 'function' && + (await Promise.resolve(this.config.verifyUserEmails(request))) === true); + const preventLoginWithUnverifiedEmail = async () => + this.config.preventLoginWithUnverifiedEmail === true || + (typeof this.config.preventLoginWithUnverifiedEmail === 'function' && + (await Promise.resolve(this.config.preventLoginWithUnverifiedEmail(request))) === true); // If verification is required - if (await verifyUserEmails() && await preventLoginWithUnverifiedEmail()) { + if ((await verifyUserEmails()) && (await preventLoginWithUnverifiedEmail())) { this.storage.rejectSignup = true; return; } diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 5a28b3bae1..31d38c8569 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -55,29 +55,67 @@ export class GlobalConfigRouter extends PromiseRouter { return acc; }, {}); const className = triggers.getClassName(Parse.Config); - const hasBeforeSaveHook = triggers.triggerExists(className, triggers.Types.beforeSave, req.config.applicationId); - const hasAfterSaveHook = triggers.triggerExists(className, triggers.Types.afterSave, req.config.applicationId); + const hasBeforeSaveHook = triggers.triggerExists( + className, + triggers.Types.beforeSave, + req.config.applicationId + ); + const hasAfterSaveHook = triggers.triggerExists( + className, + triggers.Types.afterSave, + req.config.applicationId + ); let originalConfigObject; let updatedConfigObject; const configObject = new Parse.Config(); configObject.attributes = params; - const results = await req.config.database.find('_GlobalConfig', { objectId: '1' }, { limit: 1 }); + const results = await req.config.database.find( + '_GlobalConfig', + { objectId: '1' }, + { limit: 1 } + ); const isNew = results.length !== 1; if (!isNew && (hasBeforeSaveHook || hasAfterSaveHook)) { originalConfigObject = getConfigFromParams(results[0].params); } try { - await triggers.maybeRunGlobalConfigTrigger(triggers.Types.beforeSave, req.auth, configObject, originalConfigObject, req.config, req.context); + await triggers.maybeRunGlobalConfigTrigger( + triggers.Types.beforeSave, + req.auth, + configObject, + originalConfigObject, + req.config, + req.context + ); if (isNew) { - await req.config.database.update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }, true) + await req.config.database.update( + '_GlobalConfig', + { objectId: '1' }, + update, + { upsert: true }, + true + ); updatedConfigObject = configObject; } else { - const result = await req.config.database.update('_GlobalConfig', { objectId: '1' }, update, {}, true); + const result = await req.config.database.update( + '_GlobalConfig', + { objectId: '1' }, + update, + {}, + true + ); updatedConfigObject = getConfigFromParams(result.params); } - await triggers.maybeRunGlobalConfigTrigger(triggers.Types.afterSave, req.auth, updatedConfigObject, originalConfigObject, req.config, req.context); - return { response: { result: true } } + await triggers.maybeRunGlobalConfigTrigger( + triggers.Types.afterSave, + req.auth, + updatedConfigObject, + originalConfigObject, + req.config, + req.context + ); + return { response: { result: true } }; } catch (err) { const error = triggers.resolveError(err, { code: Parse.Error.SCRIPT_FAILED, diff --git a/src/Routers/GraphQLRouter.js b/src/Routers/GraphQLRouter.js index d472ac9df5..71ca8b3ce8 100644 --- a/src/Routers/GraphQLRouter.js +++ b/src/Routers/GraphQLRouter.js @@ -19,7 +19,9 @@ export class GraphQLRouter extends PromiseRouter { "read-only masterKey isn't allowed to update the GraphQL config." ); } - const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {}); + const data = await req.config.parseGraphQLController.updateGraphQLConfig( + req.body?.params || {} + ); return { response: data, }; diff --git a/src/Routers/IAPValidationRouter.js b/src/Routers/IAPValidationRouter.js index bae6f593e9..17eae685d7 100644 --- a/src/Routers/IAPValidationRouter.js +++ b/src/Routers/IAPValidationRouter.js @@ -11,11 +11,14 @@ const APP_STORE_ERRORS = { 21000: 'The App Store could not read the JSON object you provided.', 21002: 'The data in the receipt-data property was malformed or missing.', 21003: 'The receipt could not be authenticated.', - 21004: 'The shared secret you provided does not match the shared secret on file for your account.', + 21004: + 'The shared secret you provided does not match the shared secret on file for your account.', 21005: 'The receipt server is not currently available.', 21006: 'This receipt is valid but the subscription has expired.', - 21007: 'This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.', - 21008: 'This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.', + 21007: + 'This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.', + 21008: + 'This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.', }; function appStoreError(status) { diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index 2ec993f390..98da1c4542 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -15,7 +15,7 @@ export class PublicAPIRouter extends PromiseRouter { super(); Deprecator.logRuntimeDeprecation({ usage: 'PublicAPIRouter', - solution: 'pages.enableRouter' + solution: 'pages.enableRouter', }); } verifyEmail(req) { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 7668562965..4afeb7bbd2 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -147,13 +147,23 @@ export class UsersRouter extends ClassesRouter { // If request doesn't use master or maintenance key with ignoring email verification if (!((req.auth.isMaster || req.auth.isMaintenance) && ignoreEmailVerification)) { - // Get verification conditions which can be booleans or functions; the purpose of this async/await // structure is to avoid unnecessarily executing subsequent functions if previous ones fail in the // conditional statement below, as a developer may decide to execute expensive operations in them - const verifyUserEmails = async () => req.config.verifyUserEmails === true || (typeof req.config.verifyUserEmails === 'function' && await Promise.resolve(req.config.verifyUserEmails(request)) === true); - const preventLoginWithUnverifiedEmail = async () => req.config.preventLoginWithUnverifiedEmail === true || (typeof req.config.preventLoginWithUnverifiedEmail === 'function' && await Promise.resolve(req.config.preventLoginWithUnverifiedEmail(request)) === true); - if (await verifyUserEmails() && await preventLoginWithUnverifiedEmail() && !user.emailVerified) { + const verifyUserEmails = async () => + req.config.verifyUserEmails === true || + (typeof req.config.verifyUserEmails === 'function' && + (await Promise.resolve(req.config.verifyUserEmails(request))) === true); + const preventLoginWithUnverifiedEmail = async () => + req.config.preventLoginWithUnverifiedEmail === true || + (typeof req.config.preventLoginWithUnverifiedEmail === 'function' && + (await Promise.resolve(req.config.preventLoginWithUnverifiedEmail(request))) === + true); + if ( + (await verifyUserEmails()) && + (await preventLoginWithUnverifiedEmail()) && + !user.emailVerified + ) { throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); } } @@ -252,12 +262,13 @@ export class UsersRouter extends ClassesRouter { const expiresAt = new Date( changedAt.getTime() + 86400000 * req.config.passwordPolicy.maxPasswordAge ); - if (expiresAt < new Date()) - // fail of current time is past password expiry time - { throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Your password has expired. Please reset your password.' - ); } + if (expiresAt < new Date()) { + // fail of current time is past password expiry time + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Your password has expired. Please reset your password.' + ); + } } } @@ -492,7 +503,12 @@ export class UsersRouter extends ClassesRouter { ); } - const results = await req.config.database.find('_User', { email: email }, {}, Auth.maintenance(req.config)); + const results = await req.config.database.find( + '_User', + { email: email }, + {}, + Auth.maintenance(req.config) + ); if (!results.length || results.length < 1) { throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`); } @@ -506,7 +522,12 @@ export class UsersRouter extends ClassesRouter { } const userController = req.config.userController; - const send = await userController.regenerateEmailVerifyToken(user, req.auth.isMaster, req.auth.installationId, req.ip); + const send = await userController.regenerateEmailVerifyToken( + user, + req.auth.isMaster, + req.auth.installationId, + req.ip + ); if (send) { userController.sendVerificationEmail(user, req); } diff --git a/src/SchemaMigrations/DefinedSchemas.js b/src/SchemaMigrations/DefinedSchemas.js index cf2b1761f4..14c975ee45 100644 --- a/src/SchemaMigrations/DefinedSchemas.js +++ b/src/SchemaMigrations/DefinedSchemas.js @@ -80,7 +80,9 @@ export class DefinedSchemas { logger.info('Running Migrations Completed'); } catch (e) { logger.error(`Failed to run migrations: ${e}`); - if (process.env.NODE_ENV === 'production') { process.exit(1); } + if (process.env.NODE_ENV === 'production') { + process.exit(1); + } } } @@ -108,7 +110,9 @@ export class DefinedSchemas { this.checkForMissingSchemas(); await this.enforceCLPForNonProvidedClass(); } catch (e) { - if (timeout) { clearTimeout(timeout); } + if (timeout) { + clearTimeout(timeout); + } if (this.retries < this.maxRetries) { this.retries++; // first retry 1sec, 2sec, 3sec total 6sec retry sequence @@ -118,7 +122,9 @@ export class DefinedSchemas { await this.executeMigrations(); } else { logger.error(`Failed to run migrations: ${e}`); - if (process.env.NODE_ENV === 'production') { process.exit(1); } + if (process.env.NODE_ENV === 'production') { + process.exit(1); + } } } } @@ -387,7 +393,7 @@ export class DefinedSchemas { logger.warn(`classLevelPermissions not provided for ${localSchema.className}.`); } // Use spread to avoid read only issue (encountered by Moumouls using directAccess) - const clp = ({ ...localSchema.classLevelPermissions || {} }: Parse.CLP.PermissionsMap); + const clp = ({ ...(localSchema.classLevelPermissions || {}) }: Parse.CLP.PermissionsMap); // To avoid inconsistency we need to remove all rights on addField clp.addField = {}; newLocalSchema.setCLP(clp); @@ -428,7 +434,9 @@ export class DefinedSchemas { const keysB: string[] = Object.keys(objB); // Check key name - if (keysA.length !== keysB.length) { return false; } + if (keysA.length !== keysB.length) { + return false; + } return keysA.every(k => objA[k] === objB[k]); } diff --git a/src/StatusHandler.js b/src/StatusHandler.js index fecfb268ec..59c77e4b95 100644 --- a/src/StatusHandler.js +++ b/src/StatusHandler.js @@ -248,7 +248,8 @@ export function pushStatusHandler(config, existingObjectId) { if ( error?.code === 'messaging/registration-token-not-registered' || error?.code === 'messaging/invalid-registration-token' || - (error?.code === 'messaging/invalid-argument' && error?.message === 'The registration token is not a valid FCM registration token') + (error?.code === 'messaging/invalid-argument' && + error?.message === 'The registration token is not a valid FCM registration token') ) { devicesToRemove.push(token); } diff --git a/src/TestUtils.js b/src/TestUtils.js index 912a459519..94eedd9034 100644 --- a/src/TestUtils.js +++ b/src/TestUtils.js @@ -40,5 +40,5 @@ export function resolvingPromise() { } export function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/src/Utils.js b/src/Utils.js index 72b49aeeb2..b676cbc248 100644 --- a/src/Utils.js +++ b/src/Utils.js @@ -389,7 +389,7 @@ class Utils { * addNestedKeysToRoot(obj, 'b'); * console.log(obj); * // Output: { a: 1, e: 4, c: 2, d: 3 } - */ + */ static addNestedKeysToRoot(obj, key) { if (obj[key] && typeof obj[key] === 'object') { // Add nested keys to root @@ -406,8 +406,9 @@ class Utils { * @returns {String} The encoded string. */ static encodeForUrl(input) { - return encodeURIComponent(input).replace(/[!'.()*]/g, char => - '%' + char.charCodeAt(0).toString(16).toUpperCase() + return encodeURIComponent(input).replace( + /[!'.()*]/g, + char => '%' + char.charCodeAt(0).toString(16).toUpperCase() ); } } diff --git a/src/cli/parse-server.js b/src/cli/parse-server.js index 7c7639b497..b0c574bfea 100755 --- a/src/cli/parse-server.js +++ b/src/cli/parse-server.js @@ -32,7 +32,6 @@ runner({ help, usage: '[options] ', start: function (program, options, logOptions) { - if (!options.appId || !options.masterKey) { program.outputHelp(); console.error(''); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 3f33e5100d..c2389b5e85 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -79,7 +79,7 @@ const getRoute = parseClass => { _User: 'users', _Session: 'sessions', '@File': 'files', - '@Config' : 'config', + '@Config': 'config', }[parseClass] || 'classes'; if (parseClass === '@File') { return `/${route}/:id?(.*)`; diff --git a/src/middlewares.js b/src/middlewares.js index bf8029844a..72b634219e 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -24,7 +24,9 @@ const getMountForRequest = function (req) { }; const getBlockList = (ipRangeList, store) => { - if (store.get('blockList')) { return store.get('blockList'); } + if (store.get('blockList')) { + return store.get('blockList'); + } const blockList = new BlockList(); ipRangeList.forEach(fullIp => { if (fullIp === '::/0' || fullIp === '::') { @@ -50,9 +52,15 @@ export const checkIp = (ip, ipRangeList, store) => { const incomingIpIsV4 = isIPv4(ip); const blockList = getBlockList(ipRangeList, store); - if (store.get(ip)) { return true; } - if (store.get('allowAllIpv4') && incomingIpIsV4) { return true; } - if (store.get('allowAllIpv6') && !incomingIpIsV4) { return true; } + if (store.get(ip)) { + return true; + } + if (store.get('allowAllIpv4') && incomingIpIsV4) { + return true; + } + if (store.get('allowAllIpv6') && !incomingIpIsV4) { + return true; + } const result = blockList.check(ip, incomingIpIsV4 ? 'ipv4' : 'ipv6'); // If the ip is in the list, we store the result in the store @@ -387,7 +395,9 @@ function getClientIp(req) { } function httpAuth(req) { - if (!(req.req || req).headers.authorization) { return; } + if (!(req.req || req).headers.authorization) { + return; + } var header = (req.req || req).headers.authorization; var appId, masterKey, javascriptKey; @@ -432,7 +442,9 @@ export function allowCrossDomain(appId) { } const baseOrigins = - typeof config?.allowOrigin === 'string' ? [config.allowOrigin] : config?.allowOrigin ?? ['*']; + typeof config?.allowOrigin === 'string' + ? [config.allowOrigin] + : (config?.allowOrigin ?? ['*']); const requestOrigin = req.headers.origin; const allowOrigins = requestOrigin && baseOrigins.includes(requestOrigin) ? requestOrigin : baseOrigins[0]; @@ -538,7 +550,9 @@ export const addRateLimit = (route, config, cloud) => { const client = createClient({ url: route.redisUrl, }); - client.on('error', err => { log.error('Middlewares addRateLimit Redis client error', { error: err }) }); + client.on('error', err => { + log.error('Middlewares addRateLimit Redis client error', { error: err }); + }); client.on('connect', () => {}); client.on('reconnecting', () => {}); client.on('ready', () => {}); diff --git a/src/vendor/mongodbUrl.js b/src/vendor/mongodbUrl.js index eaa25add02..9fdc764629 100644 --- a/src/vendor/mongodbUrl.js +++ b/src/vendor/mongodbUrl.js @@ -66,7 +66,9 @@ const querystring = require('querystring'); /* istanbul ignore next: improve coverage */ function urlParse(url, parseQueryString, slashesDenoteHost) { - if (url instanceof Url) { return url; } + if (url instanceof Url) { + return url; + } var u = new Url(); u.parse(url, parseQueryString, slashesDenoteHost); @@ -101,7 +103,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { code === 160 /*\u00A0*/ || code === 65279; /*\uFEFF*/ if (start === -1) { - if (isWs) { continue; } + if (isWs) { + continue; + } lastPos = start = i; } else { if (inWs) { @@ -125,7 +129,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { split = true; break; case 92: // '\\' - if (i - lastPos > 0) { rest += url.slice(lastPos, i); } + if (i - lastPos > 0) { + rest += url.slice(lastPos, i); + } rest += '/'; lastPos = i + 1; break; @@ -141,8 +147,11 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { // We didn't convert any backslashes if (end === -1) { - if (start === 0) { rest = url; } - else { rest = url.slice(start); } + if (start === 0) { + rest = url; + } else { + rest = url.slice(start); + } } else { rest = url.slice(start, end); } @@ -235,13 +244,17 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { case 124: // '|' case 125: // '}' // Characters that are never ever allowed in a hostname from RFC 2396 - if (nonHost === -1) { nonHost = i; } + if (nonHost === -1) { + nonHost = i; + } break; case 35: // '#' case 47: // '/' case 63: // '?' // Find the first instance of any host-ending characters - if (nonHost === -1) { nonHost = i; } + if (nonHost === -1) { + nonHost = i; + } hostEnd = i; break; case 64: // '@' @@ -251,7 +264,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { nonHost = -1; break; } - if (hostEnd !== -1) { break; } + if (hostEnd !== -1) { + break; + } } start = 0; if (atSign !== -1) { @@ -271,7 +286,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { // we've indicated that there is a hostname, // so even if it's empty, it has to be present. - if (typeof this.hostname !== 'string') { this.hostname = ''; } + if (typeof this.hostname !== 'string') { + this.hostname = ''; + } var hostname = this.hostname; @@ -283,7 +300,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { // validate a little. if (!ipv6Hostname) { const result = validateHostname(this, rest, hostname); - if (result !== undefined) { rest = result; } + if (result !== undefined) { + rest = result; + } } // hostnames are always lower case. @@ -318,7 +337,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { // escaped, even if encodeURIComponent doesn't think they // need to be. const result = autoEscapeStr(rest); - if (result !== undefined) { rest = result; } + if (result !== undefined) { + rest = result; + } } var questionIdx = -1; @@ -354,7 +375,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { var firstIdx = questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx) ? questionIdx : hashIdx; if (firstIdx === -1) { - if (rest.length > 0) { this.pathname = rest; } + if (rest.length > 0) { + this.pathname = rest; + } } else if (firstIdx > 0) { this.pathname = rest.slice(0, firstIdx); } @@ -378,7 +401,9 @@ Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { function validateHostname(self, rest, hostname) { for (var i = 0, lastPos; i <= hostname.length; ++i) { var code; - if (i < hostname.length) { code = hostname.charCodeAt(i); } + if (i < hostname.length) { + code = hostname.charCodeAt(i); + } if (code === 46 /*.*/ || i === hostname.length) { if (i - lastPos > 0) { if (i - lastPos > 63) { @@ -405,7 +430,9 @@ function validateHostname(self, rest, hostname) { } // Invalid host character self.hostname = hostname.slice(0, i); - if (i < hostname.length) { return '/' + hostname.slice(i) + rest; } + if (i < hostname.length) { + return '/' + hostname.slice(i) + rest; + } break; } } @@ -419,80 +446,113 @@ function autoEscapeStr(rest) { // Also escape single quotes in case of an XSS attack switch (rest.charCodeAt(i)) { case 9: // '\t' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%09'; lastPos = i + 1; break; case 10: // '\n' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%0A'; lastPos = i + 1; break; case 13: // '\r' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%0D'; lastPos = i + 1; break; case 32: // ' ' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%20'; lastPos = i + 1; break; case 34: // '"' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%22'; lastPos = i + 1; break; case 39: // '\'' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%27'; lastPos = i + 1; break; case 60: // '<' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%3C'; lastPos = i + 1; break; case 62: // '>' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%3E'; lastPos = i + 1; break; case 92: // '\\' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%5C'; lastPos = i + 1; break; case 94: // '^' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%5E'; lastPos = i + 1; break; case 96: // '`' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%60'; lastPos = i + 1; break; case 123: // '{' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%7B'; lastPos = i + 1; break; case 124: // '|' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%7C'; lastPos = i + 1; break; case 125: // '}' - if (i - lastPos > 0) { newRest += rest.slice(lastPos, i); } + if (i - lastPos > 0) { + newRest += rest.slice(lastPos, i); + } newRest += '%7D'; lastPos = i + 1; break; } } - if (lastPos === 0) { return; } - if (lastPos < rest.length) { return newRest + rest.slice(lastPos); } - else { return newRest; } + if (lastPos === 0) { + return; + } + if (lastPos < rest.length) { + return newRest + rest.slice(lastPos); + } else { + return newRest; + } } // format a parsed object into a url string @@ -502,12 +562,15 @@ function urlFormat(obj) { // If it's an obj, this is a no-op. // this way, you can call url_format() on strings // to clean up potentially wonky urls. - if (typeof obj === 'string') { obj = urlParse(obj); } - else if (typeof obj !== 'object' || obj === null) - { throw new TypeError( - 'Parameter "urlObj" must be an object, not ' + (obj === null ? 'null' : typeof obj) - ); } - else if (!(obj instanceof Url)) { return Url.prototype.format.call(obj); } + if (typeof obj === 'string') { + obj = urlParse(obj); + } else if (typeof obj !== 'object' || obj === null) { + throw new TypeError( + 'Parameter "urlObj" must be an object, not ' + (obj === null ? 'null' : typeof obj) + ); + } else if (!(obj instanceof Url)) { + return Url.prototype.format.call(obj); + } return obj.format(); } @@ -535,47 +598,63 @@ Url.prototype.format = function () { } } - if (this.query !== null && typeof this.query === 'object') - { query = querystring.stringify(this.query); } + if (this.query !== null && typeof this.query === 'object') { + query = querystring.stringify(this.query); + } var search = this.search || (query && '?' + query) || ''; - if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58 /*:*/) { protocol += ':'; } + if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58 /*:*/) { + protocol += ':'; + } var newPathname = ''; var lastPos = 0; for (var i = 0; i < pathname.length; ++i) { switch (pathname.charCodeAt(i)) { case 35: // '#' - if (i - lastPos > 0) { newPathname += pathname.slice(lastPos, i); } + if (i - lastPos > 0) { + newPathname += pathname.slice(lastPos, i); + } newPathname += '%23'; lastPos = i + 1; break; case 63: // '?' - if (i - lastPos > 0) { newPathname += pathname.slice(lastPos, i); } + if (i - lastPos > 0) { + newPathname += pathname.slice(lastPos, i); + } newPathname += '%3F'; lastPos = i + 1; break; } } if (lastPos > 0) { - if (lastPos !== pathname.length) { pathname = newPathname + pathname.slice(lastPos); } - else { pathname = newPathname; } + if (lastPos !== pathname.length) { + pathname = newPathname + pathname.slice(lastPos); + } else { + pathname = newPathname; + } } // only the slashedProtocols get the //. Not mailto:, xmpp:, etc. // unless they had them to begin with. if (this.slashes || ((!protocol || slashedProtocol[protocol]) && host !== false)) { host = '//' + (host || ''); - if (pathname && pathname.charCodeAt(0) !== 47 /*/*/) { pathname = '/' + pathname; } + if (pathname && pathname.charCodeAt(0) !== 47 /*/*/) { + pathname = '/' + pathname; + } } else if (!host) { host = ''; } search = search.replace('#', '%23'); - if (hash && hash.charCodeAt(0) !== 35 /*#*/) { hash = '#' + hash; } - if (search && search.charCodeAt(0) !== 63 /*?*/) { search = '?' + search; } + if (hash && hash.charCodeAt(0) !== 35 /*#*/) { + hash = '#' + hash; + } + if (search && search.charCodeAt(0) !== 63 /*?*/) { + search = '?' + search; + } return protocol + host + pathname + search + hash; }; @@ -592,7 +671,9 @@ Url.prototype.resolve = function (relative) { /* istanbul ignore next: improve coverage */ function urlResolveObject(source, relative) { - if (!source) { return relative; } + if (!source) { + return relative; + } return urlParse(source, false, true).resolveObject(relative); } @@ -627,7 +708,9 @@ Url.prototype.resolveObject = function (relative) { var rkeys = Object.keys(relative); for (var rk = 0; rk < rkeys.length; rk++) { var rkey = rkeys[rk]; - if (rkey !== 'protocol') { result[rkey] = relative[rkey]; } + if (rkey !== 'protocol') { + result[rkey] = relative[rkey]; + } } //urlParse appends trailing / to urls like http://www.example.com @@ -672,10 +755,18 @@ Url.prototype.resolveObject = function (relative) { break; } } - if (!relative.host) { relative.host = ''; } - if (!relative.hostname) { relative.hostname = ''; } - if (relPath[0] !== '') { relPath.unshift(''); } - if (relPath.length < 2) { relPath.unshift(''); } + if (!relative.host) { + relative.host = ''; + } + if (!relative.hostname) { + relative.hostname = ''; + } + if (relPath[0] !== '') { + relPath.unshift(''); + } + if (relPath.length < 2) { + relPath.unshift(''); + } result.pathname = relPath.join('/'); } else { result.pathname = relative.pathname; @@ -714,16 +805,22 @@ Url.prototype.resolveObject = function (relative) { result.hostname = ''; result.port = null; if (result.host) { - if (srcPath[0] === '') { srcPath[0] = result.host; } - else { srcPath.unshift(result.host); } + if (srcPath[0] === '') { + srcPath[0] = result.host; + } else { + srcPath.unshift(result.host); + } } result.host = ''; if (relative.protocol) { relative.hostname = null; relative.port = null; if (relative.host) { - if (relPath[0] === '') { relPath[0] = relative.host; } - else { relPath.unshift(relative.host); } + if (relPath[0] === '') { + relPath[0] = relative.host; + } else { + relPath.unshift(relative.host); + } } relative.host = null; } @@ -742,7 +839,9 @@ Url.prototype.resolveObject = function (relative) { } else if (relPath.length) { // it's relative // throw away the existing file, and take the new path instead. - if (!srcPath) { srcPath = []; } + if (!srcPath) { + srcPath = []; + } srcPath.pop(); srcPath = srcPath.concat(relPath); result.search = relative.search; @@ -879,19 +978,24 @@ Url.prototype.parseHost = function () { } host = host.slice(0, host.length - port.length); } - if (host) { this.hostname = host; } + if (host) { + this.hostname = host; + } }; // About 1.5x faster than the two-arg version of Array#splice(). /* istanbul ignore next: improve coverage */ function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) { list[i] = list[k]; } + for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) { + list[i] = list[k]; + } list.pop(); } var hexTable = new Array(256); -for (var i = 0; i < 256; ++i) -{ hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); } +for (var i = 0; i < 256; ++i) { + hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); +} /* istanbul ignore next: improve coverage */ function encodeAuth(str) { // faster encodeURIComponent alternative for encoding auth uri components @@ -920,7 +1024,9 @@ function encodeAuth(str) { continue; } - if (i - lastPos > 0) { out += str.slice(lastPos, i); } + if (i - lastPos > 0) { + out += str.slice(lastPos, i); + } lastPos = i + 1; @@ -945,8 +1051,11 @@ function encodeAuth(str) { // Surrogate pair ++i; var c2; - if (i < str.length) { c2 = str.charCodeAt(i) & 0x3ff; } - else { c2 = 0; } + if (i < str.length) { + c2 = str.charCodeAt(i) & 0x3ff; + } else { + c2 = 0; + } c = 0x10000 + (((c & 0x3ff) << 10) | c2); out += hexTable[0xf0 | (c >> 18)] + @@ -954,7 +1063,11 @@ function encodeAuth(str) { hexTable[0x80 | ((c >> 6) & 0x3f)] + hexTable[0x80 | (c & 0x3f)]; } - if (lastPos === 0) { return str; } - if (lastPos < str.length) { return out + str.slice(lastPos); } + if (lastPos === 0) { + return str; + } + if (lastPos < str.length) { + return out + str.slice(lastPos); + } return out; }