diff --git a/package-lock.json b/package-lock.json index b15e9761d3..d91fb23148 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7242,6 +7242,15 @@ "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", "dev": true }, + "jasmine-spec-reporter": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-6.0.0.tgz", + "integrity": "sha512-MvTOVoMxDZAftQYBApIlSfKnGMzi9cj351nXeqtnZTuXffPlbONN31+Es7F+Ke4okUeQ2xISukt4U1npfzLVrQ==", + "dev": true, + "requires": { + "colors": "1.4.0" + } + }, "jest-get-type": { "version": "25.2.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", diff --git a/package.json b/package.json index d404a60ce0..ff5cefddf6 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "form-data": "3.0.0", "husky": "4.2.5", "jasmine": "3.5.0", + "jasmine-spec-reporter": "6.0.0", "jsdoc": "3.6.3", "jsdoc-babel": "0.5.0", "lint-staged": "10.2.3", diff --git a/spec/CLI.spec.js b/spec/CLI.spec.js index 9cf42ca5ec..0cd54c2e8b 100644 --- a/spec/CLI.spec.js +++ b/spec/CLI.spec.js @@ -209,8 +209,12 @@ describe('execution', () => { const binPath = path.resolve(__dirname, '../bin/parse-server'); let childProcess; - afterEach(async () => { + afterEach(done => { if (childProcess) { + childProcess.on('close', () => { + childProcess = undefined; + done(); + }); childProcess.kill(); } }); diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 0ee0debbae..a62fc34009 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -920,7 +920,8 @@ describe('cloud validator', () => { const role2 = new Parse.Role('Admin2', roleACL); role2.getUsers().add(user); - await Promise.all([role.save({ useMasterKey: true }), role2.save({ useMasterKey: true })]); + await role.save({ useMasterKey: true }); + await role2.save({ useMasterKey: true }); await Parse.Cloud.run('cloudFunction'); done(); }); @@ -981,7 +982,8 @@ describe('cloud validator', () => { const role2 = new Parse.Role('AdminB', roleACL); role2.getUsers().add(user); - await Promise.all([role.save({ useMasterKey: true }), role2.save({ useMasterKey: true })]); + await role.save({ useMasterKey: true }); + await role2.save({ useMasterKey: true }); await Parse.Cloud.run('cloudFunction'); done(); }); diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c726bfd89e..c53a284273 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2740,45 +2740,193 @@ describe('beforeLogin hook', () => { expect(beforeFinds).toEqual(1); expect(afterFinds).toEqual(1); }); +}); - it('beforeSaveFile should not change file if nothing is returned', async () => { - await reconfigureServer({ filesAdapter: mockAdapter }); - Parse.Cloud.beforeSaveFile(() => { - return; +describe('afterLogin hook', () => { + it('should run afterLogin after successful login', async done => { + let hit = 0; + Parse.Cloud.afterLogin(req => { + hit++; + expect(req.object.get('username')).toEqual('testuser'); }); - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - const result = await file.save({ useMasterKey: true }); - expect(result).toBe(file); + + await Parse.User.signUp('testuser', 'p@ssword'); + const user = await Parse.User.logIn('testuser', 'p@ssword'); + expect(hit).toBe(1); + expect(user).toBeDefined(); + expect(user.getUsername()).toBe('testuser'); + expect(user.getSessionToken()).toBeDefined(); + done(); }); - it('throw custom error from beforeSaveFile', async done => { - Parse.Cloud.beforeSaveFile(() => { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); + it('should not run afterLogin after unsuccessful login', async done => { + let hit = 0; + Parse.Cloud.afterLogin(req => { + hit++; + expect(req.object.get('username')).toEqual('testuser'); }); + + await Parse.User.signUp('testuser', 'p@ssword'); try { - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save({ useMasterKey: true }); - fail('error should have thrown'); + await Parse.User.logIn('testuser', 'badpassword'); } catch (e) { - expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); - done(); + expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); } + expect(hit).toBe(0); + done(); }); - it('throw empty error from beforeSaveFile', async done => { - Parse.Cloud.beforeSaveFile(() => { - throw null; + it('should not run afterLogin on sign up', async done => { + let hit = 0; + Parse.Cloud.afterLogin(req => { + hit++; + expect(req.object.get('username')).toEqual('testuser'); }); - try { - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save({ useMasterKey: true }); - fail('error should have thrown'); - } catch (e) { - expect(e.code).toBe(130); - done(); - } + + const user = await Parse.User.signUp('testuser', 'p@ssword'); + expect(user).toBeDefined(); + expect(hit).toBe(0); + done(); + }); + + it('should have expected data in request', 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).toBeUndefined(); + }); + + await Parse.User.signUp('testuser', 'p@ssword'); + await Parse.User.logIn('testuser', 'p@ssword'); + done(); + }); + + it('should have access to context when saving a new object', async () => { + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + const obj = new TestObject(); + await obj.save(null, { context: { a: 'a' } }); + }); + + it('should have access to context when saving an existing object', async () => { + const obj = new TestObject(); + await obj.save(null); + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + await obj.save(null, { context: { a: 'a' } }); }); + it('should have access to context when saving a new object in a trigger', async () => { + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterSave('TriggerObject', async () => { + const obj = new TestObject(); + await obj.save(null, { context: { a: 'a' } }); + }); + const obj = new Parse.Object('TriggerObject'); + await obj.save(null); + }); + + it('should have access to context when cascade-saving objects', async () => { + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.beforeSave('TestObject2', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterSave('TestObject2', req => { + expect(req.context.a).toEqual('a'); + }); + const obj = new Parse.Object('TestObject'); + const obj2 = new Parse.Object('TestObject2'); + obj.set('obj2', obj2); + await obj.save(null, { context: { a: 'a' } }); + }); + + it('should have access to context as saveAll argument', async () => { + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + const obj1 = new TestObject(); + const obj2 = new TestObject(); + await Parse.Object.saveAll([obj1, obj2], { context: { a: 'a' } }); + }); + + it('should have access to context as destroyAll argument', async () => { + Parse.Cloud.beforeDelete('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterDelete('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + const obj1 = new TestObject(); + const obj2 = new TestObject(); + await Parse.Object.saveAll([obj1, obj2]); + await Parse.Object.destroyAll([obj1, obj2], { context: { a: 'a' } }); + }); + + it('should have access to context as destroy a object', async () => { + Parse.Cloud.beforeDelete('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterDelete('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + const obj = new TestObject(); + await obj.save(); + await obj.destroy({ context: { a: 'a' } }); + }); + + it('should have access to context in beforeFind hook', async () => { + Parse.Cloud.beforeFind('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + const query = new Parse.Query('TestObject'); + return query.find({ context: { a: 'a' } }); + }); + + it('should have access to context when cloud function is called.', async () => { + Parse.Cloud.define('contextTest', async req => { + expect(req.context.a).toEqual('a'); + return {}; + }); + + await Parse.Cloud.run('contextTest', {}, { context: { a: 'a' } }); + }); + + it('afterFind should have access to context', async () => { + Parse.Cloud.afterFind('TestObject', req => { + expect(req.context.a).toEqual('a'); + }); + const obj = new TestObject(); + await obj.save(); + const query = new Parse.Query(TestObject); + await query.find({ context: { a: 'a' } }); + }); +}); + +describe('saveFile hooks', () => { it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); @@ -3023,189 +3171,43 @@ describe('beforeLogin hook', () => { const file = new Parse.File('popeye.txt'); await file.destroy({ useMasterKey: true }); }); -}); -describe('afterLogin hook', () => { - it('should run afterLogin after successful login', async done => { - let hit = 0; - Parse.Cloud.afterLogin(req => { - hit++; - expect(req.object.get('username')).toEqual('testuser'); + it('beforeSaveFile should not change file if nothing is returned', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeSaveFile(() => { + return; }); - - await Parse.User.signUp('testuser', 'p@ssword'); - const user = await Parse.User.logIn('testuser', 'p@ssword'); - expect(hit).toBe(1); - expect(user).toBeDefined(); - expect(user.getUsername()).toBe('testuser'); - expect(user.getSessionToken()).toBeDefined(); - done(); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(file); }); - it('should not run afterLogin after unsuccessful login', async done => { - let hit = 0; - Parse.Cloud.afterLogin(req => { - hit++; - expect(req.object.get('username')).toEqual('testuser'); + it('throw custom error from beforeSaveFile', async done => { + Parse.Cloud.beforeSaveFile(() => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); }); - - await Parse.User.signUp('testuser', 'p@ssword'); try { - await Parse.User.logIn('testuser', 'badpassword'); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('error should have thrown'); } catch (e) { - expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + done(); } - expect(hit).toBe(0); - done(); - }); - - it('should not run afterLogin on sign up', async done => { - let hit = 0; - Parse.Cloud.afterLogin(req => { - hit++; - expect(req.object.get('username')).toEqual('testuser'); - }); - - const user = await Parse.User.signUp('testuser', 'p@ssword'); - expect(user).toBeDefined(); - expect(hit).toBe(0); - done(); - }); - - it('should have expected data in request', 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).toBeUndefined(); - }); - - await Parse.User.signUp('testuser', 'p@ssword'); - await Parse.User.logIn('testuser', 'p@ssword'); - done(); - }); - - it('should have access to context when saving a new object', async () => { - Parse.Cloud.beforeSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - const obj = new TestObject(); - await obj.save(null, { context: { a: 'a' } }); }); - it('should have access to context when saving an existing object', async () => { - const obj = new TestObject(); - await obj.save(null); - Parse.Cloud.beforeSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - await obj.save(null, { context: { a: 'a' } }); - }); - - it('should have access to context when saving a new object in a trigger', async () => { - Parse.Cloud.beforeSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterSave('TriggerObject', async () => { - const obj = new TestObject(); - await obj.save(null, { context: { a: 'a' } }); - }); - const obj = new Parse.Object('TriggerObject'); - await obj.save(null); - }); - - it('should have access to context when cascade-saving objects', async () => { - Parse.Cloud.beforeSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.beforeSave('TestObject2', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterSave('TestObject2', req => { - expect(req.context.a).toEqual('a'); - }); - const obj = new Parse.Object('TestObject'); - const obj2 = new Parse.Object('TestObject2'); - obj.set('obj2', obj2); - await obj.save(null, { context: { a: 'a' } }); - }); - - it('should have access to context as saveAll argument', async () => { - Parse.Cloud.beforeSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterSave('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - const obj1 = new TestObject(); - const obj2 = new TestObject(); - await Parse.Object.saveAll([obj1, obj2], { context: { a: 'a' } }); - }); - - it('should have access to context as destroyAll argument', async () => { - Parse.Cloud.beforeDelete('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterDelete('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - const obj1 = new TestObject(); - const obj2 = new TestObject(); - await Parse.Object.saveAll([obj1, obj2]); - await Parse.Object.destroyAll([obj1, obj2], { context: { a: 'a' } }); - }); - - it('should have access to context as destroy a object', async () => { - Parse.Cloud.beforeDelete('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - Parse.Cloud.afterDelete('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - const obj = new TestObject(); - await obj.save(); - await obj.destroy({ context: { a: 'a' } }); - }); - - it('should have access to context in beforeFind hook', async () => { - Parse.Cloud.beforeFind('TestObject', req => { - expect(req.context.a).toEqual('a'); - }); - const query = new Parse.Query('TestObject'); - return query.find({ context: { a: 'a' } }); - }); - - it('should have access to context when cloud function is called.', async () => { - Parse.Cloud.define('contextTest', async req => { - expect(req.context.a).toEqual('a'); - return {}; - }); - - await Parse.Cloud.run('contextTest', {}, { context: { a: 'a' } }); - }); - - it('afterFind should have access to context', async () => { - Parse.Cloud.afterFind('TestObject', req => { - expect(req.context.a).toEqual('a'); + it('throw empty error from beforeSaveFile', async done => { + Parse.Cloud.beforeSaveFile(() => { + throw null; }); - const obj = new TestObject(); - await obj.save(); - const query = new Parse.Query(TestObject); - await query.find({ context: { a: 'a' } }); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(130); + done(); + } }); }); @@ -3215,7 +3217,7 @@ describe('sendEmail', () => { sendMail: mailData => { expect(mailData).toBeDefined(); expect(mailData.to).toBe('test'); - done(); + reconfigureServer().then(done, done); }, }; await reconfigureServer({ diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index 7888819334..78626e4efb 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -14,6 +14,7 @@ describe('Cloud Code Logger', () => { return reconfigureServer({ // useful to flip to false for fine tuning :). silent: true, + logLevel: undefined, }) .then(() => { return Parse.User.signUp('tester', 'abc') diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index aa14170652..efd133a236 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -39,8 +39,12 @@ function startServer(done) { describe('httpRequest', () => { let server; - beforeAll(done => { - server = startServer(done); + beforeEach(done => { + if (!server) { + server = startServer(done); + } else { + done(); + } }); afterAll(done => { diff --git a/spec/LdapAuth.spec.js b/spec/LdapAuth.spec.js index 56e583e60b..09532b217b 100644 --- a/spec/LdapAuth.spec.js +++ b/spec/LdapAuth.spec.js @@ -4,255 +4,257 @@ const fs = require('fs'); const port = 12345; const sslport = 12346; -it('Should fail with missing options', done => { - ldap - .validateAuthData({ id: 'testuser', password: 'testpw' }) - .then(done.fail) - .catch(err => { - jequal(err.message, 'LDAP auth configuration missing'); - done(); - }); -}); +describe('Ldap Auth', () => { + it('Should fail with missing options', done => { + ldap + .validateAuthData({ id: 'testuser', password: 'testpw' }) + .then(done.fail) + .catch(err => { + jequal(err.message, 'LDAP auth configuration missing'); + done(); + }); + }); -it('Should return a resolved promise when validating the app id', done => { - ldap.validateAppId().then(done).catch(done.fail); -}); + it('Should return a resolved promise when validating the app id', done => { + ldap.validateAppId().then(done).catch(done.fail); + }); -it('Should succeed with right credentials', done => { - mockLdapServer(port, 'uid=testuser, o=example').then(server => { - const options = { - suffix: 'o=example', - url: `ldap://localhost:${port}`, - dn: 'uid={{id}}, o=example', - }; - ldap - .validateAuthData({ id: 'testuser', password: 'secret' }, options) - .then(done) - .catch(done.fail) - .finally(() => server.close()); + it('Should succeed with right credentials', done => { + mockLdapServer(port, 'uid=testuser, o=example').then(server => { + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + }; + ldap + .validateAuthData({ id: 'testuser', password: 'secret' }, options) + .then(done) + .catch(done.fail) + .finally(() => server.close()); + }); }); -}); -it('Should succeed with right credentials when LDAPS is used and certifcate is not checked', done => { - mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { - const options = { - suffix: 'o=example', - url: `ldaps://localhost:${sslport}`, - dn: 'uid={{id}}, o=example', - tlsOptions: { rejectUnauthorized: false }, - }; - ldap - .validateAuthData({ id: 'testuser', password: 'secret' }, options) - .then(done) - .catch(done.fail) - .finally(() => server.close()); + it('Should succeed with right credentials when LDAPS is used and certifcate is not checked', done => { + mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { + const options = { + suffix: 'o=example', + url: `ldaps://localhost:${sslport}`, + dn: 'uid={{id}}, o=example', + tlsOptions: { rejectUnauthorized: false }, + }; + ldap + .validateAuthData({ id: 'testuser', password: 'secret' }, options) + .then(done) + .catch(done.fail) + .finally(() => server.close()); + }); }); -}); -it('Should succeed when LDAPS is used and the presented certificate is the expected certificate', done => { - mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { - const options = { - suffix: 'o=example', - url: `ldaps://localhost:${sslport}`, - dn: 'uid={{id}}, o=example', - tlsOptions: { - ca: fs.readFileSync(__dirname + '/support/cert/cert.pem'), - rejectUnauthorized: true, - }, - }; - ldap - .validateAuthData({ id: 'testuser', password: 'secret' }, options) - .then(done) - .catch(done.fail) - .finally(() => server.close()); + it('Should succeed when LDAPS is used and the presented certificate is the expected certificate', done => { + mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { + const options = { + suffix: 'o=example', + url: `ldaps://localhost:${sslport}`, + dn: 'uid={{id}}, o=example', + tlsOptions: { + ca: fs.readFileSync(__dirname + '/support/cert/cert.pem'), + rejectUnauthorized: true, + }, + }; + ldap + .validateAuthData({ id: 'testuser', password: 'secret' }, options) + .then(done) + .catch(done.fail) + .finally(() => server.close()); + }); }); -}); -it('Should fail when LDAPS is used and the presented certificate is not the expected certificate', done => { - mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { - const options = { - suffix: 'o=example', - url: `ldaps://localhost:${sslport}`, - dn: 'uid={{id}}, o=example', - tlsOptions: { - ca: fs.readFileSync(__dirname + '/support/cert/anothercert.pem'), - rejectUnauthorized: true, - }, - }; - ldap - .validateAuthData({ id: 'testuser', password: 'secret' }, options) - .then(done.fail) - .catch(err => { - jequal(err.message, 'LDAPS: Certificate mismatch'); - done(); - }) - .finally(() => server.close()); + it('Should fail when LDAPS is used and the presented certificate is not the expected certificate', done => { + mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { + const options = { + suffix: 'o=example', + url: `ldaps://localhost:${sslport}`, + dn: 'uid={{id}}, o=example', + tlsOptions: { + ca: fs.readFileSync(__dirname + '/support/cert/anothercert.pem'), + rejectUnauthorized: true, + }, + }; + ldap + .validateAuthData({ id: 'testuser', password: 'secret' }, options) + .then(done.fail) + .catch(err => { + jequal(err.message, 'LDAPS: Certificate mismatch'); + done(); + }) + .finally(() => server.close()); + }); }); -}); -it('Should fail when LDAPS is used certifcate matches but credentials are wrong', done => { - mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { - const options = { - suffix: 'o=example', - url: `ldaps://localhost:${sslport}`, - dn: 'uid={{id}}, o=example', - tlsOptions: { - ca: fs.readFileSync(__dirname + '/support/cert/cert.pem'), - rejectUnauthorized: true, - }, - }; - ldap - .validateAuthData({ id: 'testuser', password: 'wrong!' }, options) - .then(done.fail) - .catch(err => { - jequal(err.message, 'LDAP: Wrong username or password'); - done(); - }) - .finally(() => server.close()); + it('Should fail when LDAPS is used certifcate matches but credentials are wrong', done => { + mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { + const options = { + suffix: 'o=example', + url: `ldaps://localhost:${sslport}`, + dn: 'uid={{id}}, o=example', + tlsOptions: { + ca: fs.readFileSync(__dirname + '/support/cert/cert.pem'), + rejectUnauthorized: true, + }, + }; + ldap + .validateAuthData({ id: 'testuser', password: 'wrong!' }, options) + .then(done.fail) + .catch(err => { + jequal(err.message, 'LDAP: Wrong username or password'); + done(); + }) + .finally(() => server.close()); + }); }); -}); -it('Should fail with wrong credentials', done => { - mockLdapServer(port, 'uid=testuser, o=example').then(server => { - const options = { - suffix: 'o=example', - url: `ldap://localhost:${port}`, - dn: 'uid={{id}}, o=example', - }; - ldap - .validateAuthData({ id: 'testuser', password: 'wrong!' }, options) - .then(done.fail) - .catch(err => { - jequal(err.message, 'LDAP: Wrong username or password'); - done(); - }) - .finally(() => server.close()); + it('Should fail with wrong credentials', done => { + mockLdapServer(port, 'uid=testuser, o=example').then(server => { + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + }; + ldap + .validateAuthData({ id: 'testuser', password: 'wrong!' }, options) + .then(done.fail) + .catch(err => { + jequal(err.message, 'LDAP: Wrong username or password'); + done(); + }) + .finally(() => server.close()); + }); }); -}); -it('Should succeed if user is in given group', done => { - mockLdapServer(port, 'uid=testuser, o=example').then(server => { - const options = { - suffix: 'o=example', - url: `ldap://localhost:${port}`, - dn: 'uid={{id}}, o=example', - groupCn: 'powerusers', - groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', - }; + it('Should succeed if user is in given group', done => { + mockLdapServer(port, 'uid=testuser, o=example').then(server => { + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + groupCn: 'powerusers', + groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', + }; - ldap - .validateAuthData({ id: 'testuser', password: 'secret' }, options) - .then(done) - .catch(done.fail) - .finally(() => server.close()); + ldap + .validateAuthData({ id: 'testuser', password: 'secret' }, options) + .then(done) + .catch(done.fail) + .finally(() => server.close()); + }); }); -}); -it('Should fail if user is not in given group', done => { - mockLdapServer(port, 'uid=testuser, o=example').then(server => { - const options = { - suffix: 'o=example', - url: `ldap://localhost:${port}`, - dn: 'uid={{id}}, o=example', - groupCn: 'groupTheUserIsNotIn', - groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', - }; + it('Should fail if user is not in given group', done => { + mockLdapServer(port, 'uid=testuser, o=example').then(server => { + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + groupCn: 'groupTheUserIsNotIn', + groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', + }; - ldap - .validateAuthData({ id: 'testuser', password: 'secret' }, options) - .then(done.fail) - .catch(err => { - jequal(err.message, 'LDAP: User not in group'); - done(); - }) - .finally(() => server.close()); + ldap + .validateAuthData({ id: 'testuser', password: 'secret' }, options) + .then(done.fail) + .catch(err => { + jequal(err.message, 'LDAP: User not in group'); + done(); + }) + .finally(() => server.close()); + }); }); -}); -it('Should fail if the LDAP server does not allow searching inside the provided suffix', done => { - mockLdapServer(port, 'uid=testuser, o=example').then(server => { - const options = { - suffix: 'o=invalid', - url: `ldap://localhost:${port}`, - dn: 'uid={{id}}, o=example', - groupCn: 'powerusers', - groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', - }; + it('Should fail if the LDAP server does not allow searching inside the provided suffix', done => { + mockLdapServer(port, 'uid=testuser, o=example').then(server => { + const options = { + suffix: 'o=invalid', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + groupCn: 'powerusers', + groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', + }; - ldap - .validateAuthData({ id: 'testuser', password: 'secret' }, options) - .then(done.fail) - .catch(err => { - jequal(err.message, 'LDAP group search failed'); - done(); - }) - .finally(() => server.close()); + ldap + .validateAuthData({ id: 'testuser', password: 'secret' }, options) + .then(done.fail) + .catch(err => { + jequal(err.message, 'LDAP group search failed'); + done(); + }) + .finally(() => server.close()); + }); }); -}); -it('Should fail if the LDAP server encounters an error while searching', done => { - mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { - const options = { - suffix: 'o=example', - url: `ldap://localhost:${port}`, - dn: 'uid={{id}}, o=example', - groupCn: 'powerusers', - groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', - }; + it('Should fail if the LDAP server encounters an error while searching', done => { + mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + groupCn: 'powerusers', + groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', + }; - ldap - .validateAuthData({ id: 'testuser', password: 'secret' }, options) - .then(done.fail) - .catch(err => { - jequal(err.message, 'LDAP group search failed'); - done(); - }) - .finally(() => server.close()); + ldap + .validateAuthData({ id: 'testuser', password: 'secret' }, options) + .then(done.fail) + .catch(err => { + jequal(err.message, 'LDAP group search failed'); + done(); + }) + .finally(() => server.close()); + }); }); -}); -it('Should delete the password from authData after validation', done => { - mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { - const options = { - suffix: 'o=example', - url: `ldap://localhost:${port}`, - dn: 'uid={{id}}, o=example', - }; + it('Should delete the password from authData after validation', done => { + mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + }; - const authData = { id: 'testuser', password: 'secret' }; + const authData = { id: 'testuser', password: 'secret' }; - ldap - .validateAuthData(authData, options) - .then(() => { - expect(authData).toEqual({ id: 'testuser' }); - done(); - }) - .catch(done.fail) - .finally(() => server.close()); + ldap + .validateAuthData(authData, options) + .then(() => { + expect(authData).toEqual({ id: 'testuser' }); + done(); + }) + .catch(done.fail) + .finally(() => server.close()); + }); }); -}); -it('Should not save the password in the user record after authentication', done => { - mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { - const options = { - suffix: 'o=example', - url: `ldap://localhost:${port}`, - dn: 'uid={{id}}, o=example', - }; - reconfigureServer({ auth: { ldap: options } }).then(() => { - const authData = { authData: { id: 'testuser', password: 'secret' } }; - Parse.User.logInWith('ldap', authData).then(returnedUser => { - const query = new Parse.Query('User'); - query - .equalTo('objectId', returnedUser.id) - .first({ useMasterKey: true }) - .then(user => { - expect(user.get('authData')).toEqual({ ldap: { id: 'testuser' } }); - expect(user.get('authData').ldap.password).toBeUndefined(); - done(); - }) - .catch(done.fail) - .finally(() => server.close()); + it('Should not save the password in the user record after authentication', done => { + mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + }; + reconfigureServer({ auth: { ldap: options } }).then(() => { + const authData = { authData: { id: 'testuser', password: 'secret' } }; + Parse.User.logInWith('ldap', authData).then(returnedUser => { + const query = new Parse.Query('User'); + query + .equalTo('objectId', returnedUser.id) + .first({ useMasterKey: true }) + .then(user => { + expect(user.get('authData')).toEqual({ ldap: { id: 'testuser' } }); + expect(user.get('authData').ldap.password).toBeUndefined(); + done(); + }) + .catch(done.fail) + .finally(() => server.close()); + }); }); }); }); diff --git a/spec/Logger.spec.js b/spec/Logger.spec.js index 1fbfe54178..865c5b0c5c 100644 --- a/spec/Logger.spec.js +++ b/spec/Logger.spec.js @@ -32,11 +32,13 @@ describe('WinstonLogger', () => { it('should disable files logs', done => { reconfigureServer({ logsFolder: null, - }).then(() => { - const transports = logging.logger.transports; - expect(transports.length).toBe(1); - done(); - }); + }) + .then(() => { + const transports = logging.logger.transports; + expect(transports.length).toBe(1); + return reconfigureServer(); + }) + .then(done); }); it('should have a timestamp', done => { diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index b63da31623..9cd4094698 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -376,15 +376,12 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { 'X-Parse-REST-API-Key': 'rest', }; - beforeAll(async () => { + beforeEach(async () => { await reconfigureServer({ databaseAdapter: undefined, databaseURI: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', }); - }); - - beforeEach(async () => { await TestUtils.destroyAllDataPermanently(true); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 2eab8cfde1..76143e0580 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -67,6 +67,7 @@ describe('miscellaneous', function () { }); it('fail to create a duplicate username', async () => { + await reconfigureServer(); let numFailed = 0; let numCreated = 0; const p1 = request({ @@ -114,6 +115,7 @@ describe('miscellaneous', function () { }); it('ensure that email is uniquely indexed', async () => { + await reconfigureServer(); let numFailed = 0; let numCreated = 0; const p1 = request({ @@ -246,7 +248,8 @@ describe('miscellaneous', function () { }); }); - 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', 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(); const config = Config.get('test'); config.database.adapter .addFieldIfNotExists('_User', 'randomField', { type: 'String' }) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 80d4a4cbdd..b55dd7404a 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -833,7 +833,8 @@ describe('Parse.File testing', () => { // Because GridStore is not loaded on PG, those are perfect // for fallback tests describe_only_db('postgres')('Default Range tests', () => { - it('fallback to regular request', done => { + it('fallback to regular request', async done => { + await reconfigureServer(); const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', diff --git a/spec/ParseGraphQLController.spec.js b/spec/ParseGraphQLController.spec.js index 4aff84732c..7a60e48ba5 100644 --- a/spec/ParseGraphQLController.spec.js +++ b/spec/ParseGraphQLController.spec.js @@ -28,42 +28,41 @@ describe('ParseGraphQLController', () => { return graphQLConfigRecord; }; - beforeAll(async () => { - parseServer = await global.reconfigureServer({ - schemaCacheTTL: 100, - }); - databaseController = parseServer.config.databaseController; - cacheController = parseServer.config.cacheController; - - const defaultFind = databaseController.find.bind(databaseController); - databaseController.find = async (className, query, ...args) => { - if (className === GraphQLConfigClassName && isEqual(query, { objectId: GraphQLConfigId })) { - const graphQLConfigRecord = getConfigFromDb(); - return graphQLConfigRecord ? [graphQLConfigRecord] : []; - } else { - return defaultFind(className, query, ...args); - } - }; + beforeEach(async () => { + if (!parseServer) { + parseServer = await global.reconfigureServer({ + schemaCacheTTL: 100, + }); + databaseController = parseServer.config.databaseController; + cacheController = parseServer.config.cacheController; - const defaultUpdate = databaseController.update.bind(databaseController); - databaseController.update = async (className, query, update, fullQueryOptions) => { - databaseUpdateArgs = [className, query, update, fullQueryOptions]; - if ( - className === GraphQLConfigClassName && - isEqual(query, { objectId: GraphQLConfigId }) && - update && - !!update[GraphQLConfigKey] && - fullQueryOptions && - isEqual(fullQueryOptions, { upsert: true }) - ) { - setConfigOnDb(update[GraphQLConfigKey]); - } else { - return defaultUpdate(...databaseUpdateArgs); - } - }; - }); + const defaultFind = databaseController.find.bind(databaseController); + databaseController.find = async (className, query, ...args) => { + if (className === GraphQLConfigClassName && isEqual(query, { objectId: GraphQLConfigId })) { + const graphQLConfigRecord = getConfigFromDb(); + return graphQLConfigRecord ? [graphQLConfigRecord] : []; + } else { + return defaultFind(className, query, ...args); + } + }; - beforeEach(() => { + const defaultUpdate = databaseController.update.bind(databaseController); + databaseController.update = async (className, query, update, fullQueryOptions) => { + databaseUpdateArgs = [className, query, update, fullQueryOptions]; + if ( + className === GraphQLConfigClassName && + isEqual(query, { objectId: GraphQLConfigId }) && + update && + !!update[GraphQLConfigKey] && + fullQueryOptions && + isEqual(fullQueryOptions, { upsert: true }) + ) { + setConfigOnDb(update[GraphQLConfigKey]); + } else { + return defaultUpdate(...databaseUpdateArgs); + } + }; + } databaseUpdateArgs = null; }); diff --git a/spec/ParseGraphQLSchema.spec.js b/spec/ParseGraphQLSchema.spec.js index 76f00b7c9b..ee815588a9 100644 --- a/spec/ParseGraphQLSchema.spec.js +++ b/spec/ParseGraphQLSchema.spec.js @@ -9,7 +9,7 @@ describe('ParseGraphQLSchema', () => { let parseGraphQLSchema; const appId = 'test'; - beforeAll(async () => { + beforeEach(async () => { parseServer = await global.reconfigureServer({ schemaCacheTTL: 100, }); diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index dad9bda3df..67bac737b7 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -43,7 +43,7 @@ describe('ParseGraphQLServer', () => { let parseServer; let parseGraphQLServer; - beforeAll(async () => { + beforeEach(async () => { parseServer = await global.reconfigureServer({}); parseGraphQLServer = new ParseGraphQLServer(parseServer, { graphQLPath: '/graphql', @@ -394,7 +394,7 @@ describe('ParseGraphQLServer', () => { objects.push(object1, object2, object3, object4); } - beforeAll(async () => { + beforeEach(async () => { const expressApp = express(); httpServer = http.createServer(expressApp); expressApp.use('/parse', parseServer.app); @@ -436,14 +436,11 @@ describe('ParseGraphQLServer', () => { }, }, }); - }); - - beforeEach(() => { spyOn(console, 'warn').and.callFake(() => {}); spyOn(console, 'error').and.callFake(() => {}); }); - afterAll(async () => { + afterEach(async () => { await parseLiveQueryServer.server.close(); await httpServer.close(); }); @@ -700,8 +697,12 @@ describe('ParseGraphQLServer', () => { }); describe('Relay Specific Types', () => { - beforeAll(async () => { - await resetGraphQLCache(); + let clearCache; + beforeEach(async () => { + if (!clearCache) { + await resetGraphQLCache(); + clearCache = true; + } }); afterAll(async () => { @@ -2175,11 +2176,7 @@ describe('ParseGraphQLServer', () => { }); describe('Relay Spec', () => { - beforeAll(async () => { - await resetGraphQLCache(); - }); - - afterAll(async () => { + beforeEach(async () => { await resetGraphQLCache(); }); @@ -10079,7 +10076,7 @@ describe('ParseGraphQLServer', () => { 'X-Parse-Javascript-Key': 'test', }; let apolloClient; - beforeAll(async () => { + beforeEach(async () => { const expressApp = express(); httpServer = http.createServer(expressApp); parseGraphQLServer = new ParseGraphQLServer(parseServer, { @@ -10112,7 +10109,7 @@ describe('ParseGraphQLServer', () => { }); }); - afterAll(async () => { + afterEach(async () => { await httpServer.close(); }); @@ -10203,97 +10200,99 @@ describe('ParseGraphQLServer', () => { }; let apolloClient; - beforeAll(async () => { - const expressApp = express(); - httpServer = http.createServer(expressApp); - const TypeEnum = new GraphQLEnumType({ - name: 'TypeEnum', - values: { - human: { value: 'human' }, - robot: { value: 'robot' }, - }, - }); - const SomeClassType = new GraphQLObjectType({ - name: 'SomeClass', - fields: { - nameUpperCase: { - type: new GraphQLNonNull(GraphQLString), - resolve: p => p.name.toUpperCase(), - }, - type: { type: TypeEnum }, - language: { - type: new GraphQLEnumType({ - name: 'LanguageEnum', - values: { - fr: { value: 'fr' }, - en: { value: 'en' }, - }, - }), - resolve: () => 'fr', - }, + beforeEach(async () => { + if (!httpServer) { + const expressApp = express(); + httpServer = http.createServer(expressApp); + const TypeEnum = new GraphQLEnumType({ + name: 'TypeEnum', + values: { + human: { value: 'human' }, + robot: { value: 'robot' }, }, - }), - parseGraphQLServer = new ParseGraphQLServer(parseServer, { - graphQLPath: '/graphql', - graphQLCustomTypeDefs: new GraphQLSchema({ - query: new GraphQLObjectType({ - name: 'Query', - fields: { - customQuery: { - type: new GraphQLNonNull(GraphQLString), - args: { - message: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: (p, { message }) => message, - }, - customQueryWithAutoTypeReturn: { - type: SomeClassType, - args: { - id: { type: new GraphQLNonNull(GraphQLString) }, - }, - resolve: async (p, { id }) => { - const obj = new Parse.Object('SomeClass'); - obj.id = id; - await obj.fetch(); - return obj.toJSON(); + }); + const SomeClassType = new GraphQLObjectType({ + name: 'SomeClass', + fields: { + nameUpperCase: { + type: new GraphQLNonNull(GraphQLString), + resolve: p => p.name.toUpperCase(), + }, + type: { type: TypeEnum }, + language: { + type: new GraphQLEnumType({ + name: 'LanguageEnum', + values: { + fr: { value: 'fr' }, + en: { value: 'en' }, }, - }, + }), + resolve: () => 'fr', }, - }), - types: [ - new GraphQLInputObjectType({ - name: 'CreateSomeClassFieldsInput', - fields: { - type: { type: TypeEnum }, - }, - }), - new GraphQLInputObjectType({ - name: 'UpdateSomeClassFieldsInput', + }, + }), + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: '/graphql', + graphQLCustomTypeDefs: new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', fields: { - type: { type: TypeEnum }, + customQuery: { + type: new GraphQLNonNull(GraphQLString), + args: { + message: { type: new GraphQLNonNull(GraphQLString) }, + }, + resolve: (p, { message }) => message, + }, + customQueryWithAutoTypeReturn: { + type: SomeClassType, + args: { + id: { type: new GraphQLNonNull(GraphQLString) }, + }, + resolve: async (p, { id }) => { + const obj = new Parse.Object('SomeClass'); + obj.id = id; + await obj.fetch(); + return obj.toJSON(); + }, + }, }, }), - SomeClassType, - ], - }), - }); + types: [ + new GraphQLInputObjectType({ + name: 'CreateSomeClassFieldsInput', + fields: { + type: { type: TypeEnum }, + }, + }), + new GraphQLInputObjectType({ + name: 'UpdateSomeClassFieldsInput', + fields: { + type: { type: TypeEnum }, + }, + }), + SomeClassType, + ], + }), + }); - parseGraphQLServer.applyGraphQL(expressApp); - await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); - const httpLink = createUploadLink({ - uri: 'http://localhost:13377/graphql', - fetch, - headers, - }); - apolloClient = new ApolloClient({ - link: httpLink, - cache: new InMemoryCache(), - defaultOptions: { - query: { - fetchPolicy: 'no-cache', + parseGraphQLServer.applyGraphQL(expressApp); + await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); + const httpLink = createUploadLink({ + uri: 'http://localhost:13377/graphql', + fetch, + headers, + }); + apolloClient = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + defaultOptions: { + query: { + fetchPolicy: 'no-cache', + }, }, - }, - }); + }); + } }); afterAll(async () => { @@ -10393,31 +10392,33 @@ describe('ParseGraphQLServer', () => { }; let apolloClient; - beforeAll(async () => { - const expressApp = express(); - httpServer = http.createServer(expressApp); - parseGraphQLServer = new ParseGraphQLServer(parseServer, { - graphQLPath: '/graphql', - graphQLCustomTypeDefs: ({ autoSchema, stitchSchemas }) => - stitchSchemas({ subschemas: [autoSchema] }), - }); + beforeEach(async () => { + if (!httpServer) { + const expressApp = express(); + httpServer = http.createServer(expressApp); + parseGraphQLServer = new ParseGraphQLServer(parseServer, { + graphQLPath: '/graphql', + graphQLCustomTypeDefs: ({ autoSchema, stitchSchemas }) => + stitchSchemas({ subschemas: [autoSchema] }), + }); - parseGraphQLServer.applyGraphQL(expressApp); - await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); - const httpLink = createUploadLink({ - uri: 'http://localhost:13377/graphql', - fetch, - headers, - }); - apolloClient = new ApolloClient({ - link: httpLink, - cache: new InMemoryCache(), - defaultOptions: { - query: { - fetchPolicy: 'no-cache', + parseGraphQLServer.applyGraphQL(expressApp); + await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); + const httpLink = createUploadLink({ + uri: 'http://localhost:13377/graphql', + fetch, + headers, + }); + apolloClient = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + defaultOptions: { + query: { + fetchPolicy: 'no-cache', + }, }, - }, - }); + }); + } }); afterAll(async () => { diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index de2918ba01..9434b6f10a 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -15,10 +15,14 @@ const AppCache = require('../lib/cache').AppCache; describe('Hooks', () => { let server; let app; - beforeAll(done => { - app = express(); - app.use(bodyParser.json({ type: '*/*' })); - server = app.listen(12345, undefined, done); + beforeEach(done => { + if (!app) { + app = express(); + app.use(bodyParser.json({ type: '*/*' })); + server = app.listen(12345, undefined, done); + } else { + done(); + } }); afterAll(done => { diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index fa259785ee..43e91e03bb 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -15,12 +15,14 @@ describe('ParseLiveQuery', function () { verbose: false, silent: true, }); + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null); const requestedUser = new Parse.User(); requestedUser.setUsername('username'); requestedUser.setPassword('password'); Parse.Cloud.onLiveQueryEvent(req => { const { event, sessionToken } = req; if (event === 'ws_disconnect') { + Parse.Cloud._removeAllHooks(); expect(sessionToken).toBeDefined(); expect(sessionToken).toBe(requestedUser.getSessionToken()); done(); @@ -356,185 +358,6 @@ describe('ParseLiveQuery', function () { await object.save(); }); - it('expect afterEvent create', async done => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - }); - Parse.Cloud.afterLiveQueryEvent('TestObject', req => { - expect(req.event).toBe('create'); - expect(req.user).toBeUndefined(); - expect(req.object.get('foo')).toBe('bar'); - }); - - const query = new Parse.Query(TestObject); - const subscription = await query.subscribe(); - subscription.on('create', object => { - expect(object.get('foo')).toBe('bar'); - done(); - }); - - const object = new TestObject(); - object.set('foo', 'bar'); - await object.save(); - }); - - it('expect afterEvent payload', async done => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - }); - const object = new TestObject(); - await object.save(); - - Parse.Cloud.afterLiveQueryEvent('TestObject', req => { - expect(req.event).toBe('update'); - expect(req.user).toBeUndefined(); - expect(req.object.get('foo')).toBe('bar'); - expect(req.original.get('foo')).toBeUndefined(); - done(); - }); - - const query = new Parse.Query(TestObject); - query.equalTo('objectId', object.id); - await query.subscribe(); - object.set({ foo: 'bar' }); - await object.save(); - }); - - it('expect afterEvent enter', async done => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - }); - Parse.Cloud.afterLiveQueryEvent('TestObject', req => { - expect(req.event).toBe('enter'); - expect(req.user).toBeUndefined(); - expect(req.object.get('foo')).toBe('bar'); - expect(req.original.get('foo')).toBeUndefined(); - }); - - const object = new TestObject(); - await object.save(); - - const query = new Parse.Query(TestObject); - query.equalTo('foo', 'bar'); - const subscription = await query.subscribe(); - subscription.on('enter', object => { - expect(object.get('foo')).toBe('bar'); - done(); - }); - - object.set('foo', 'bar'); - await object.save(); - }); - - it('expect afterEvent leave', async done => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - }); - Parse.Cloud.afterLiveQueryEvent('TestObject', req => { - expect(req.event).toBe('leave'); - expect(req.user).toBeUndefined(); - expect(req.object.get('foo')).toBeUndefined(); - expect(req.original.get('foo')).toBe('bar'); - }); - - const object = new TestObject(); - object.set('foo', 'bar'); - await object.save(); - - const query = new Parse.Query(TestObject); - query.equalTo('foo', 'bar'); - const subscription = await query.subscribe(); - subscription.on('leave', object => { - expect(object.get('foo')).toBeUndefined(); - done(); - }); - - object.unset('foo'); - await object.save(); - }); - - it('expect afterEvent delete', async done => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - }); - Parse.Cloud.afterLiveQueryEvent('TestObject', req => { - expect(req.event).toBe('delete'); - expect(req.user).toBeUndefined(); - req.object.set('foo', 'bar'); - }); - - const object = new TestObject(); - await object.save(); - - const query = new Parse.Query(TestObject); - query.equalTo('objectId', object.id); - - const subscription = await query.subscribe(); - subscription.on('delete', object => { - expect(object.get('foo')).toBe('bar'); - done(); - }); - - await object.destroy(); - }); - - it('can handle afterEvent modification', async done => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - }); - const object = new TestObject(); - await object.save(); - - Parse.Cloud.afterLiveQueryEvent('TestObject', req => { - const current = req.object; - current.set('foo', 'yolo'); - - const original = req.original; - original.set('yolo', 'foo'); - }); - - const query = new Parse.Query(TestObject); - query.equalTo('objectId', object.id); - const subscription = await query.subscribe(); - subscription.on('update', (object, original) => { - expect(object.get('foo')).toBe('yolo'); - expect(original.get('yolo')).toBe('foo'); - done(); - }); - object.set({ foo: 'bar' }); - await object.save(); - }); - it('can handle async afterEvent modification', async done => { await reconfigureServer({ liveQuery: { @@ -622,6 +445,7 @@ describe('ParseLiveQuery', function () { Parse.Cloud.beforeConnect(() => {}, validatorFail); let complete = false; Parse.LiveQuery.on('error', error => { + Parse.LiveQuery.removeAllListeners('error'); if (complete) { return; } @@ -695,6 +519,7 @@ describe('ParseLiveQuery', function () { throw new Error('You shall not pass!'); }); Parse.LiveQuery.on('error', error => { + Parse.LiveQuery.removeAllListeners('error'); expect(error).toBe('You shall not pass!'); done(); }); @@ -725,6 +550,7 @@ describe('ParseLiveQuery', function () { query.equalTo('objectId', object.id); const subscription = await query.subscribe(); subscription.on('error', error => { + Parse.LiveQuery.removeAllListeners('error'); expect(error).toBe('You shall not subscribe!'); done(); }); diff --git a/spec/ParsePolygon.spec.js b/spec/ParsePolygon.spec.js index 846752672b..e2319e75da 100644 --- a/spec/ParsePolygon.spec.js +++ b/spec/ParsePolygon.spec.js @@ -9,8 +9,6 @@ const defaultHeaders = { }; describe('Parse.Polygon testing', () => { - beforeAll(() => require('../lib/TestUtils').destroyAllDataPermanently()); - it('polygon save open path', done => { const coords = [ [0, 0], @@ -211,7 +209,9 @@ describe('Parse.Polygon testing', () => { }); describe('with location', () => { - beforeAll(() => require('../lib/TestUtils').destroyAllDataPermanently()); + if (process.env.PARSE_SERVER_TEST_DB !== 'postgres') { + beforeEach(() => require('../lib/TestUtils').destroyAllDataPermanently()); + } it('polygonContain query', done => { const points1 = [ @@ -236,13 +236,13 @@ describe('Parse.Polygon testing', () => { const polygon1 = new Parse.Polygon(points1); const polygon2 = new Parse.Polygon(points2); const polygon3 = new Parse.Polygon(points3); - const obj1 = new TestObject({ location: polygon1 }); - const obj2 = new TestObject({ location: polygon2 }); - const obj3 = new TestObject({ location: polygon3 }); + const obj1 = new TestObject({ boundary: polygon1 }); + const obj2 = new TestObject({ boundary: polygon2 }); + const obj3 = new TestObject({ boundary: polygon3 }); Parse.Object.saveAll([obj1, obj2, obj3]) .then(() => { const where = { - location: { + boundary: { $geoIntersects: { $point: { __type: 'GeoPoint', latitude: 0.5, longitude: 0.5 }, }, @@ -288,13 +288,13 @@ describe('Parse.Polygon testing', () => { const polygon1 = new Parse.Polygon(points1); const polygon2 = new Parse.Polygon(points2); const polygon3 = new Parse.Polygon(points3); - const obj1 = new TestObject({ location: polygon1 }); - const obj2 = new TestObject({ location: polygon2 }); - const obj3 = new TestObject({ location: polygon3 }); + const obj1 = new TestObject({ boundary: polygon1 }); + const obj2 = new TestObject({ boundary: polygon2 }); + const obj3 = new TestObject({ boundary: polygon3 }); Parse.Object.saveAll([obj1, obj2, obj3]) .then(() => { const where = { - location: { + boundary: { $geoIntersects: { $point: { __type: 'GeoPoint', latitude: 0.5, longitude: 1.0 }, }, @@ -326,12 +326,12 @@ describe('Parse.Polygon testing', () => { [42.631655189280224, -83.78406753121705], ]; const polygon = new Parse.Polygon(detroit); - const obj = new TestObject({ location: polygon }); + const obj = new TestObject({ boundary: polygon }); obj .save() .then(() => { const where = { - location: { + boundary: { $geoIntersects: { $point: { __type: 'GeoPoint', @@ -366,12 +366,12 @@ describe('Parse.Polygon testing', () => { [1, 0], ]; const polygon = new Parse.Polygon(points); - const obj = new TestObject({ location: polygon }); + const obj = new TestObject({ boundary: polygon }); obj .save() .then(() => { const where = { - location: { + boundary: { $geoIntersects: { $point: { __type: 'GeoPoint', latitude: 181, longitude: 181 }, }, @@ -398,12 +398,12 @@ describe('Parse.Polygon testing', () => { [1, 0], ]; const polygon = new Parse.Polygon(points); - const obj = new TestObject({ location: polygon }); + const obj = new TestObject({ boundary: polygon }); obj .save() .then(() => { const where = { - location: { + boundary: { $geoIntersects: { $point: [], }, diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 60b04b1fad..fc20edc140 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -1300,7 +1300,8 @@ describe('Parse.Query Aggregate testing', () => { }); }); - it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', done => { + it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', async done => { + await reconfigureServer(); const pointer1 = new TestObject({ value: 1 }); const pointer2 = new TestObject({ value: 2 }); const pointer3 = new TestObject({ value: 3 }); @@ -1403,20 +1404,16 @@ describe('Parse.Query Aggregate testing', () => { expect(results.length).toEqual(2); expect(results[0].value).toEqual(2); expect(results[1].value).toEqual(3); + await database.adapter.deleteAllClasses(false); }); it_only_db('mongo')('aggregate geoNear with near GeoJSON point', async () => { // Create geo index which is required for `geoNear` query const database = Config.get(Parse.applicationId).database; const schema = await new Parse.Schema('GeoObject').save(); - await database.adapter.ensureIndex( - 'GeoObject', - schema, - ['location'], - undefined, - false, - '2dsphere' - ); + await database.adapter.ensureIndex('GeoObject', schema, ['location'], undefined, false, { + indexType: '2dsphere', + }); // Create objects const GeoObject = Parse.Object.extend('GeoObject'); const obj1 = new GeoObject({ @@ -1453,20 +1450,16 @@ describe('Parse.Query Aggregate testing', () => { const results = await query.aggregate(pipeline); // Check results expect(results.length).toEqual(3); + await database.adapter.deleteAllClasses(false); }); it_only_db('mongo')('aggregate geoNear with near legacy coordinate pair', async () => { // Create geo index which is required for `geoNear` query const database = Config.get(Parse.applicationId).database; const schema = await new Parse.Schema('GeoObject').save(); - await database.adapter.ensureIndex( - 'GeoObject', - schema, - ['location'], - undefined, - false, - '2dsphere' - ); + await database.adapter.ensureIndex('GeoObject', schema, ['location'], undefined, false, { + indexType: '2dsphere', + }); // Create objects const GeoObject = Parse.Object.extend('GeoObject'); const obj1 = new GeoObject({ @@ -1500,5 +1493,6 @@ describe('Parse.Query Aggregate testing', () => { const results = await query.aggregate(pipeline); // Check results expect(results.length).toEqual(3); + await database.adapter.deleteAllClasses(false); }); }); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 97c81f86cb..9825fc2d95 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -18,6 +18,10 @@ const masterKeyOptions = { headers: masterKeyHeaders, }; +const BoxedNumber = Parse.Object.extend({ + className: 'BoxedNumber', +}); + describe('Parse.Query testing', () => { it('basic query', function (done) { const baz = new TestObject({ foo: 'baz' }); @@ -933,10 +937,6 @@ describe('Parse.Query testing', () => { }); }); - const BoxedNumber = Parse.Object.extend({ - className: 'BoxedNumber', - }); - it('equalTo queries', function (done) { const makeBoxedNumber = function (i) { return new BoxedNumber({ number: i }); @@ -2927,10 +2927,10 @@ describe('Parse.Query testing', () => { const saves = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function (x) { const obj = new Parse.Object('TestObject'); obj.set('x', x + 1); - return obj.save(); + return obj; }); - Promise.all(saves) + Parse.Object.saveAll(saves) .then(function () { const query = new Parse.Query('TestObject'); query.ascending('x'); diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 544b5f87d9..eee05d5717 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -6,6 +6,58 @@ const RestQuery = require('../lib/RestQuery'); const Auth = require('../lib/Auth').Auth; const Config = require('../lib/Config'); +function testLoadRoles(config, done) { + const rolesNames = ['FooRole', 'BarRole', 'BazRole']; + const roleIds = {}; + createTestUser() + .then(user => { + // Put the user on the 1st role + return createRole(rolesNames[0], null, user) + .then(aRole => { + roleIds[aRole.get('name')] = aRole.id; + // set the 1st role as a sibling of the second + // user will should have 2 role now + return createRole(rolesNames[1], aRole, null); + }) + .then(anotherRole => { + roleIds[anotherRole.get('name')] = anotherRole.id; + // set this role as a sibling of the last + // the user should now have 3 roles + return createRole(rolesNames[2], anotherRole, null); + }) + .then(lastRole => { + roleIds[lastRole.get('name')] = lastRole.id; + const auth = new Auth({ config, isMaster: true, user: user }); + return auth._loadRoles(); + }); + }) + .then( + roles => { + expect(roles.length).toEqual(3); + rolesNames.forEach(name => { + expect(roles.indexOf('role:' + name)).not.toBe(-1); + }); + done(); + }, + function () { + fail('should succeed'); + done(); + } + ); +} + +const createRole = function (name, sibling, user) { + const role = new Parse.Role(name, new Parse.ACL()); + if (user) { + const users = role.relation('users'); + users.add(user); + } + if (sibling) { + role.relation('roles').add(sibling); + } + return role.save({}, { useMasterKey: true }); +}; + describe('Parse Role testing', () => { it('Do a bunch of basic role testing', done => { let user; @@ -74,18 +126,6 @@ describe('Parse Role testing', () => { ); }); - const createRole = function (name, sibling, user) { - const role = new Parse.Role(name, new Parse.ACL()); - if (user) { - const users = role.relation('users'); - users.add(user); - } - if (sibling) { - role.relation('roles').add(sibling); - } - return role.save({}, { useMasterKey: true }); - }; - it('should not recursively load the same role multiple times', done => { const rootRole = 'RootRole'; const roleNames = ['FooRole', 'BarRole', 'BazRole']; @@ -157,46 +197,6 @@ describe('Parse Role testing', () => { }); }); - function testLoadRoles(config, done) { - const rolesNames = ['FooRole', 'BarRole', 'BazRole']; - const roleIds = {}; - createTestUser() - .then(user => { - // Put the user on the 1st role - return createRole(rolesNames[0], null, user) - .then(aRole => { - roleIds[aRole.get('name')] = aRole.id; - // set the 1st role as a sibling of the second - // user will should have 2 role now - return createRole(rolesNames[1], aRole, null); - }) - .then(anotherRole => { - roleIds[anotherRole.get('name')] = anotherRole.id; - // set this role as a sibling of the last - // the user should now have 3 roles - return createRole(rolesNames[2], anotherRole, null); - }) - .then(lastRole => { - roleIds[lastRole.get('name')] = lastRole.id; - const auth = new Auth({ config, isMaster: true, user: user }); - return auth._loadRoles(); - }); - }) - .then( - roles => { - expect(roles.length).toEqual(3); - rolesNames.forEach(name => { - expect(roles.indexOf('role:' + name)).not.toBe(-1); - }); - done(); - }, - function () { - fail('should succeed'); - done(); - } - ); - } - it('should recursively load roles', done => { testLoadRoles(Config.get('test'), done); }); @@ -227,7 +227,8 @@ describe('Parse Role testing', () => { ); }); - it('Different _Role objects cannot have the same name.', done => { + it('Different _Role objects cannot have the same name.', async done => { + await reconfigureServer(); const roleName = 'MyRole'; let aUser; createTestUser() diff --git a/spec/ParseServer.spec.js b/spec/ParseServer.spec.js index 03d1eaf498..904e0b164e 100644 --- a/spec/ParseServer.spec.js +++ b/spec/ParseServer.spec.js @@ -10,26 +10,32 @@ const { spawn } = require('child_process'); describe('Server Url Checks', () => { let server; - beforeAll(done => { - const app = express(); - app.get('/health', function (req, res) { - res.json({ - status: 'ok', + beforeEach(done => { + if (!server) { + const app = express(); + app.get('/health', function (req, res) { + res.json({ + status: 'ok', + }); }); - }); - server = app.listen(13376, undefined, done); + server = app.listen(13376, undefined, done); + } else { + done(); + } }); afterAll(done => { + Parse.serverURL = 'http://localhost:8378/1'; server.close(done); }); it('validate good server url', done => { Parse.serverURL = 'http://localhost:13376'; - ParseServer.verifyServerUrl(function (result) { + ParseServer.verifyServerUrl(async result => { if (!result) { done.fail('Did not pass valid url'); } + await reconfigureServer(); done(); }); }); @@ -37,10 +43,11 @@ describe('Server Url Checks', () => { it('mark bad server url', done => { spyOn(console, 'warn').and.callFake(() => {}); Parse.serverURL = 'notavalidurl'; - ParseServer.verifyServerUrl(function (result) { + ParseServer.verifyServerUrl(async result => { if (result) { done.fail('Did not mark invalid url'); } + await reconfigureServer(); done(); }); }); @@ -98,10 +105,11 @@ describe('Server Url Checks', () => { parseServerProcess.stderr.on('data', data => { stderr = data.toString(); }); - parseServerProcess.on('close', code => { + parseServerProcess.on('close', async code => { expect(code).toEqual(1); expect(stdout).toBeUndefined(); expect(stderr).toContain('MongoServerSelectionError'); + await reconfigureServer(); done(); }); }); diff --git a/spec/ParseServerRESTController.spec.js b/spec/ParseServerRESTController.spec.js index 87f3ffe81b..2a84edaa4e 100644 --- a/spec/ParseServerRESTController.spec.js +++ b/spec/ParseServerRESTController.spec.js @@ -3,6 +3,7 @@ const ParseServerRESTController = require('../lib/ParseServerRESTController') const ParseServer = require('../lib/ParseServer').default; const Parse = require('parse/node').Parse; const semver = require('semver'); +const TestUtils = require('../lib/TestUtils'); let RESTController; @@ -168,17 +169,21 @@ describe('ParseServerRESTController', () => { process.env.PARSE_SERVER_TEST_DB === 'postgres' ) { describe('transactions', () => { - beforeAll(async () => { + let parseServer; + beforeEach(async () => { if ( semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger' ) { - await reconfigureServer({ - databaseAdapter: undefined, - databaseURI: - 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', - }); + if (!parseServer) { + parseServer = await reconfigureServer({ + databaseAdapter: undefined, + databaseURI: + 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', + }); + } + await TestUtils.destroyAllDataPermanently(true); } }); @@ -215,7 +220,6 @@ describe('ParseServerRESTController', () => { const query = new Parse.Query('MyObject'); return query.find().then(results => { expect(databaseAdapter.createObject.calls.count() % 2).toBe(0); - expect(databaseAdapter.createObject.calls.count() > 0).toEqual(true); for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) { expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe( databaseAdapter.createObject.calls.argsFor(i + 1)[3] @@ -346,6 +350,7 @@ describe('ParseServerRESTController', () => { }); it('should generate separate session for each call', async () => { + await reconfigureServer(); const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections await myObject.save(); await myObject.destroy(); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index a44926caa4..be636e53cb 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -238,6 +238,7 @@ describe('Parse.User testing', () => { }); it_only_db('mongo')('should let legacy users without ACL login', async () => { + await reconfigureServer(); const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; const adapter = new MongoStorageAdapter({ collectionPrefix: 'test_', @@ -826,8 +827,9 @@ describe('Parse.User testing', () => { done(); }); - it('user modified while saving', done => { + it('user modified while saving', async done => { Parse.Object.disableSingleInstance(); + await reconfigureServer(); const user = new Parse.User(); user.set('username', 'alice'); user.set('password', 'password'); @@ -2907,7 +2909,8 @@ describe('Parse.User testing', () => { }); }); - it('should send email when upgrading from anon', done => { + it('should send email when upgrading from anon', async done => { + await reconfigureServer(); let emailCalled = false; let emailOptions; const emailAdapter = { @@ -3897,6 +3900,7 @@ describe('Parse.User testing', () => { }); it('should throw OBJECT_NOT_FOUND instead of SESSION_MISSING when using masterKey', async () => { + await reconfigureServer(); // create a fake user (just so we simulate an object not found) const non_existent_user = Parse.User.createWithoutData('fake_id'); try { @@ -3929,6 +3933,7 @@ describe('Parse.User testing', () => { it_only_db('mongo')('should be able to login with a legacy user (no ACL)', async () => { // This issue is a side effect of the locked users and legacy users which don't have ACL's // In this scenario, a legacy user wasn't be able to login as there's no ACL on it + await reconfigureServer(); const database = Config.get(Parse.applicationId).database; const collection = await database.adapter._adaptiveCollection('_User'); await collection.insertOne({ @@ -3962,6 +3967,7 @@ describe('Security Advisory GHSA-8w3j-g983-8jh5', function () { it_only_db('mongo')( 'should validate credentials first and check if account already linked afterwards ()', async done => { + await reconfigureServer(); // Add User to Database with authData const database = Config.get(Parse.applicationId).database; const collection = await database.adapter._adaptiveCollection('_User'); @@ -4000,6 +4006,7 @@ describe('Security Advisory GHSA-8w3j-g983-8jh5', function () { ); it_only_db('mongo')('should ignore authData field', async () => { // Add User to Database with authData + await reconfigureServer(); const database = Config.get(Parse.applicationId).database; const collection = await database.adapter._adaptiveCollection('_User'); await collection.insertOne({ diff --git a/spec/PostgresInitOptions.spec.js b/spec/PostgresInitOptions.spec.js index 29962710d5..73f040701f 100644 --- a/spec/PostgresInitOptions.spec.js +++ b/spec/PostgresInitOptions.spec.js @@ -50,9 +50,10 @@ function createParseServer(options) { describe_only_db('postgres')('Postgres database init options', () => { let server; - afterEach(() => { + afterAll(done => { if (server) { - server.close(); + Parse.serverURL = 'http://localhost:8378/1'; + server.close(done); } }); @@ -73,7 +74,10 @@ describe_only_db('postgres')('Postgres database init options', () => { }); return score.save(); }) - .then(done, done.fail); + .then(async () => { + await reconfigureServer(); + done(); + }, done.fail); }); it('should fail to create server if schema databaseOptions does not exist', done => { @@ -83,6 +87,9 @@ describe_only_db('postgres')('Postgres database init options', () => { databaseOptions: databaseOptions2, }); - createParseServer({ databaseAdapter: adapter }).then(done.fail, () => done()); + createParseServer({ databaseAdapter: adapter }).then(done.fail, async () => { + await reconfigureServer(); + done(); + }); }); }); diff --git a/spec/PostgresStorageAdapter.spec.js b/spec/PostgresStorageAdapter.spec.js index 44eb064012..8c372362b5 100644 --- a/spec/PostgresStorageAdapter.spec.js +++ b/spec/PostgresStorageAdapter.spec.js @@ -22,11 +22,11 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { beforeEach(async () => { const config = Config.get('test'); adapter = config.database.adapter; - await adapter.deleteAllClasses(); - await adapter.performInitialization({ VolatileClassesSchemas: [] }); }); - it('schemaUpgrade, upgrade the database schema when schema changes', done => { + it('schemaUpgrade, upgrade the database schema when schema changes', async done => { + await adapter.deleteAllClasses(); + await adapter.performInitialization({ VolatileClassesSchemas: [] }); const client = adapter._client; const className = '_PushStatus'; const schema = { @@ -50,11 +50,12 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { return adapter.schemaUpgrade(className, schema); }) .then(() => getColumns(client, className)) - .then(columns => { + .then(async columns => { expect(columns).toContain('pushTime'); expect(columns).toContain('source'); expect(columns).toContain('query'); expect(columns).toContain('expiration_interval'); + await reconfigureServer(); done(); }) .catch(error => done.fail(error)); @@ -153,6 +154,10 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { objectId: { type: 'String' }, username: { type: 'String' }, email: { type: 'String' }, + emailVerified: { type: 'Boolean' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + authData: { type: 'Object' }, }, }; const client = adapter._client; @@ -172,74 +177,66 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { const caseInsensitiveData = 'bugs'; const originalQuery = 'SELECT * FROM $1:name WHERE lower($2:name)=lower($3)'; const analyzedExplainQuery = adapter.createExplainableQuery(originalQuery, true); - await client - .one(analyzedExplainQuery, [tableName, 'objectId', caseInsensitiveData]) - .then(explained => { - const preIndexPlan = explained; - - preIndexPlan['QUERY PLAN'].forEach(element => { - //Make sure search returned with only 1 result - expect(element.Plan['Actual Rows']).toBe(1); - expect(element.Plan['Node Type']).toBe('Seq Scan'); - }); - const indexName = 'test_case_insensitive_column'; - - adapter.ensureIndex(tableName, schema, ['objectId'], indexName, true).then(() => { - client - .one(analyzedExplainQuery, [tableName, 'objectId', caseInsensitiveData]) - .then(explained => { - const postIndexPlan = explained; - - postIndexPlan['QUERY PLAN'].forEach(element => { - //Make sure search returned with only 1 result - expect(element.Plan['Actual Rows']).toBe(1); - //Should not be a sequential scan - expect(element.Plan['Node Type']).not.toContain('Seq Scan'); - - //Should be using the index created for this - element.Plan.Plans.forEach(innerElement => { - expect(innerElement['Index Name']).toBe(indexName); - }); - }); - - //These are the same query so should be the same size - for (let i = 0; i < preIndexPlan['QUERY PLAN'].length; i++) { - //Sequential should take more time to execute than indexed - expect(preIndexPlan['QUERY PLAN'][i]['Execution Time']).toBeGreaterThan( - postIndexPlan['QUERY PLAN'][i]['Execution Time'] - ); - } - - //Test explaining without analyzing - const basicExplainQuery = adapter.createExplainableQuery(originalQuery); - client - .one(basicExplainQuery, [tableName, 'objectId', caseInsensitiveData]) - .then(explained => { - explained['QUERY PLAN'].forEach(element => { - //Check that basic query plans isn't a sequential scan - expect(element.Plan['Node Type']).not.toContain('Seq Scan'); - - //Basic query plans shouldn't have an execution time - expect(element['Execution Time']).toBeUndefined(); - }); - }); - }); - }); - }) - .catch(error => { - // Query on non existing table, don't crash - if (error.code !== '42P01') { - throw error; - } - return []; + const preIndexPlan = await client.one(analyzedExplainQuery, [ + tableName, + 'objectId', + caseInsensitiveData, + ]); + preIndexPlan['QUERY PLAN'].forEach(element => { + //Make sure search returned with only 1 result + expect(element.Plan['Actual Rows']).toBe(1); + expect(element.Plan['Node Type']).toBe('Seq Scan'); + }); + const indexName = 'test_case_insensitive_column'; + await adapter.ensureIndex(tableName, schema, ['objectId'], indexName, true); + + const postIndexPlan = await client.one(analyzedExplainQuery, [ + tableName, + 'objectId', + caseInsensitiveData, + ]); + postIndexPlan['QUERY PLAN'].forEach(element => { + //Make sure search returned with only 1 result + expect(element.Plan['Actual Rows']).toBe(1); + //Should not be a sequential scan + expect(element.Plan['Node Type']).not.toContain('Seq Scan'); + + //Should be using the index created for this + element.Plan.Plans.forEach(innerElement => { + expect(innerElement['Index Name']).toBe(indexName); }); + }); + + //These are the same query so should be the same size + for (let i = 0; i < preIndexPlan['QUERY PLAN'].length; i++) { + //Sequential should take more time to execute than indexed + expect(preIndexPlan['QUERY PLAN'][i]['Execution Time']).toBeGreaterThan( + postIndexPlan['QUERY PLAN'][i]['Execution Time'] + ); + } + //Test explaining without analyzing + const basicExplainQuery = adapter.createExplainableQuery(originalQuery); + const explained = await client.one(basicExplainQuery, [ + tableName, + 'objectId', + caseInsensitiveData, + ]); + explained['QUERY PLAN'].forEach(element => { + //Check that basic query plans isn't a sequential scan + expect(element.Plan['Node Type']).not.toContain('Seq Scan'); + + //Basic query plans shouldn't have an execution time + expect(element['Execution Time']).toBeUndefined(); + }); + await dropTable(client, tableName); }); it('should use index for caseInsensitive query', async () => { const tableName = '_User'; + const user = new Parse.User(); - user.set('username', 'Bugs'); - user.set('password', 'Bunny'); + user.set('username', 'Elmer'); + user.set('password', 'Fudd'); await user.signUp(); const database = Config.get(Parse.applicationId).database; @@ -249,7 +246,7 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { 'INSERT INTO $1:name ($2:name, $3:name) SELECT gen_random_uuid(), gen_random_uuid() FROM generate_series(1,5000)', [tableName, 'objectId', 'username'] ); - const caseInsensitiveData = 'bugs'; + const caseInsensitiveData = 'elmer'; const fieldToSearch = 'username'; //Check using find method for Parse const preIndexPlan = await database.find( @@ -292,8 +289,8 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { it('should use index for caseInsensitive query using default indexname', async () => { const tableName = '_User'; const user = new Parse.User(); - user.set('username', 'Bugs'); - user.set('password', 'Bunny'); + user.set('username', 'Tweety'); + user.set('password', 'Bird'); await user.signUp(); const database = Config.get(Parse.applicationId).database; const fieldToSearch = 'username'; @@ -308,7 +305,7 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { [tableName, 'objectId', 'username'] ); - const caseInsensitiveData = 'buGs'; + const caseInsensitiveData = 'tweeTy'; //Check using find method for Parse const indexPlan = await database.find( tableName, diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 629cc7e5e1..c8e3adb49d 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -315,6 +315,9 @@ describe('rest query', () => { }); describe('RestQuery.each', () => { + beforeEach(() => { + config = Config.get('test'); + }); it('should run each', async () => { const objects = []; while (objects.length != 10) { diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index bb1cace408..21378c1100 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -3,7 +3,6 @@ const Config = require('../lib/Config'); const SchemaController = require('../lib/Controllers/SchemaController'); const dd = require('deep-diff'); -const TestUtils = require('../lib/TestUtils'); let config; @@ -27,8 +26,6 @@ describe('SchemaController', () => { afterEach(async () => { await config.database.schemaCache.clear(); - await TestUtils.destroyAllDataPermanently(false); - await config.database.adapter.performInitialization({ VolatileClassesSchemas: [] }); }); it('can validate one object', done => { @@ -854,7 +851,8 @@ describe('SchemaController', () => { }); }); - it('creates non-custom classes which include relation field', done => { + it('creates non-custom classes which include relation field', async done => { + await reconfigureServer(); config.database .loadSchema() //as `_Role` is always created by default, we only get it here @@ -1313,7 +1311,8 @@ describe('SchemaController', () => { ); }); - it('properly handles volatile _Schemas', done => { + it('properly handles volatile _Schemas', async done => { + await reconfigureServer(); function validateSchemaStructure(schema) { expect(Object.prototype.hasOwnProperty.call(schema, 'className')).toBe(true); expect(Object.prototype.hasOwnProperty.call(schema, 'fields')).toBe(true); diff --git a/spec/UserPII.spec.js b/spec/UserPII.spec.js index 764c681544..87bfe15e4e 100644 --- a/spec/UserPII.spec.js +++ b/spec/UserPII.spec.js @@ -13,6 +13,7 @@ describe('Personally Identifiable Information', () => { let user; beforeEach(async done => { + await reconfigureServer(); user = await Parse.User.signUp('tester', 'abc'); user = await Parse.User.logIn(user.get('username'), 'abc'); await user.set('email', EMAIL).set('zip', ZIP).set('ssn', SSN).save(); diff --git a/spec/batch.spec.js b/spec/batch.spec.js index 9af8b059aa..a1041e4ccc 100644 --- a/spec/batch.spec.js +++ b/spec/batch.spec.js @@ -1,6 +1,7 @@ const batch = require('../lib/batch'); const request = require('../lib/request'); const semver = require('semver'); +const TestUtils = require('../lib/TestUtils'); const originalURL = '/parse/batch'; const serverURL = 'http://localhost:1234/parse'; @@ -88,7 +89,7 @@ describe('batch', () => { expect(internalURL).toEqual('/classes/Object'); }); - it('should handle a batch request without transaction', done => { + it('should handle a batch request without transaction', async done => { spyOn(databaseAdapter, 'createObject').and.callThrough(); request({ @@ -126,7 +127,8 @@ describe('batch', () => { }); }); - it('should handle a batch request with transaction = false', done => { + it('should handle a batch request with transaction = false', async done => { + await reconfigureServer(); spyOn(databaseAdapter, 'createObject').and.callThrough(); request({ @@ -172,7 +174,7 @@ describe('batch', () => { process.env.PARSE_SERVER_TEST_DB === 'postgres' ) { describe('transactions', () => { - beforeAll(async () => { + beforeEach(async () => { if ( semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && @@ -183,10 +185,12 @@ describe('batch', () => { databaseURI: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', }); + await TestUtils.destroyAllDataPermanently(true); } }); - it('should handle a batch request with transaction = true', done => { + it('should handle a batch request with transaction = true', async done => { + await reconfigureServer(); const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections myObject .save() @@ -224,7 +228,6 @@ describe('batch', () => { const query = new Parse.Query('MyObject'); query.find().then(results => { expect(databaseAdapter.createObject.calls.count() % 2).toBe(0); - expect(databaseAdapter.createObject.calls.count() > 0).toEqual(true); for (let i = 0; i + 1 < databaseAdapter.createObject.calls.length; i = i + 2) { expect(databaseAdapter.createObject.calls.argsFor(i)[3]).toBe( databaseAdapter.createObject.calls.argsFor(i + 1)[3] @@ -359,6 +362,7 @@ describe('batch', () => { }); it('should generate separate session for each call', async () => { + await reconfigureServer(); const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections await myObject.save(); await myObject.destroy(); diff --git a/spec/helper.js b/spec/helper.js index dc28ecdc76..68254f518f 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,8 +1,14 @@ 'use strict'; const semver = require('semver'); +const CurrentSpecReporter = require('./support/CurrentSpecReporter.js'); // Sets up a Parse API server for testing. -jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 5000; +jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000; +jasmine.getEnv().addReporter(new CurrentSpecReporter()); +if (process.env.PARSE_SERVER_LOG_LEVEL === 'debug') { + const { SpecReporter } = require('jasmine-spec-reporter'); + jasmine.getEnv().addReporter(new SpecReporter()); +} global.on_db = (db, callback, elseCallback) => { if (process.env.PARSE_SERVER_TEST_DB == db) { @@ -32,6 +38,7 @@ const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/Postgre .default; const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default; +const { VolatileClassesSchemas } = require('../lib/Controllers/SchemaController'); const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; @@ -120,8 +127,10 @@ const openConnections = {}; // Set up a default API server for testing with default configuration. let server; +let didChangeConfiguration = false; + // Allows testing specific configurations of Parse Server -const reconfigureServer = changedConfiguration => { +const reconfigureServer = (changedConfiguration = {}) => { return new Promise((resolve, reject) => { if (server) { return server.close(() => { @@ -131,6 +140,7 @@ const reconfigureServer = changedConfiguration => { } try { let parseServer = undefined; + didChangeConfiguration = Object.keys(changedConfiguration).length !== 0; const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { serverStartComplete: error => { if (error) { @@ -167,7 +177,7 @@ const reconfigureServer = changedConfiguration => { const Parse = require('parse/node'); Parse.serverURL = 'http://localhost:' + port + '/1'; -beforeEach(async () => { +beforeAll(async () => { try { Parse.User.enableUnsafeCurrentUser(); } catch (error) { @@ -182,11 +192,17 @@ beforeEach(async () => { }); afterEach(function (done) { - const afterLogOut = () => { + const afterLogOut = async () => { if (Object.keys(openConnections).length > 0) { fail('There were open connections to the server left after the test finished'); } - TestUtils.destroyAllDataPermanently(true).then(done, done); + await TestUtils.destroyAllDataPermanently(true); + if (didChangeConfiguration) { + await reconfigureServer(); + } else { + await databaseAdapter.performInitialization({ VolatileClassesSchemas }); + } + done(); }; Parse.Cloud._removeAllHooks(); databaseAdapter diff --git a/spec/index.spec.js b/spec/index.spec.js index 1b542926c1..65636ee13d 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -163,7 +163,8 @@ describe('server', () => { }); }); - it('can report the server version', done => { + it('can report the server version', async done => { + await reconfigureServer(); request({ url: 'http://localhost:8378/1/serverInfo', headers: { @@ -177,7 +178,8 @@ describe('server', () => { }); }); - it('can properly sets the push support', done => { + it('can properly sets the push support', async done => { + await reconfigureServer(); // default config passes push options const config = Config.get('test'); expect(config.hasPushSupport).toEqual(true); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index f8d68e4923..db3082ec74 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -821,42 +821,42 @@ describe('read-only masterKey', () => { }).toThrow(); }); - it('properly blocks writes', done => { - reconfigureServer({ + it('properly blocks writes', async () => { + await reconfigureServer({ readOnlyMasterKey: 'yolo-read-only', - }) - .then(() => { - return request({ - url: `${Parse.serverURL}/classes/MyYolo`, - method: 'POST', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': 'yolo-read-only', - 'Content-Type': 'application/json', - }, - body: { foo: 'bar' }, - }); - }) - .then(done.fail) - .catch(res => { - expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.data.error).toBe( - "read-only masterKey isn't allowed to perform the create operation." - ); - done(); + }); + try { + await request({ + url: `${Parse.serverURL}/classes/MyYolo`, + method: 'POST', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'yolo-read-only', + 'Content-Type': 'application/json', + }, + body: { foo: 'bar' }, }); - }); - - it('should throw when masterKey and readOnlyMasterKey are the same', done => { - reconfigureServer({ - masterKey: 'yolo', - readOnlyMasterKey: 'yolo', - }) - .then(done.fail) - .catch(err => { - expect(err).toEqual(new Error('masterKey and readOnlyMasterKey should be different')); - done(); + fail(); + } catch (res) { + expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.data.error).toBe( + "read-only masterKey isn't allowed to perform the create operation." + ); + } + await reconfigureServer(); + }); + + it('should throw when masterKey and readOnlyMasterKey are the same', async () => { + try { + await reconfigureServer({ + masterKey: 'yolo', + readOnlyMasterKey: 'yolo', }); + fail(); + } catch (err) { + expect(err).toEqual(new Error('masterKey and readOnlyMasterKey should be different')); + } + await reconfigureServer(); }); it('should throw when trying to create RestWrite', () => { @@ -872,7 +872,7 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create schema', done => { - return request({ + request({ method: 'POST', url: `${Parse.serverURL}/schemas`, headers: { @@ -891,7 +891,7 @@ describe('read-only masterKey', () => { }); it('should throw when trying to create schema with a name', done => { - return request({ + request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'POST', headers: { @@ -910,7 +910,7 @@ describe('read-only masterKey', () => { }); it('should throw when trying to update schema', done => { - return request({ + request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'PUT', headers: { @@ -929,7 +929,7 @@ describe('read-only masterKey', () => { }); it('should throw when trying to delete schema', done => { - return request({ + request({ url: `${Parse.serverURL}/schemas/MyClass`, method: 'DELETE', headers: { @@ -948,7 +948,7 @@ describe('read-only masterKey', () => { }); it('should throw when trying to update the global config', done => { - return request({ + request({ url: `${Parse.serverURL}/config`, method: 'PUT', headers: { @@ -967,7 +967,7 @@ describe('read-only masterKey', () => { }); it('should throw when trying to send push', done => { - return request({ + request({ url: `${Parse.serverURL}/push`, method: 'POST', headers: { diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 994864ab0f..cba50f387f 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -140,14 +140,13 @@ const masterKeyHeaders = { }; describe('schemas', () => { - beforeEach(() => { + beforeEach(async () => { + await reconfigureServer(); config = Config.get('test'); }); afterEach(async () => { await config.database.schemaCache.clear(); - await TestUtils.destroyAllDataPermanently(false); - await config.database.adapter.performInitialization({ VolatileClassesSchemas: [] }); }); it('requires the master key to get all schemas', done => { diff --git a/spec/support/CurrentSpecReporter.js b/spec/support/CurrentSpecReporter.js new file mode 100755 index 0000000000..3158e21eae --- /dev/null +++ b/spec/support/CurrentSpecReporter.js @@ -0,0 +1,15 @@ +// Sets a global variable to the current test spec +// ex: global.currentSpec.description + +global.currentSpec = null; + +class CurrentSpecReporter { + specStarted(spec) { + global.currentSpec = spec; + } + specDone() { + global.currentSpec = null; + } +} + +module.exports = CurrentSpecReporter; diff --git a/src/Adapters/Storage/Postgres/PostgresClient.js b/src/Adapters/Storage/Postgres/PostgresClient.js index bbae91f5e4..062dc207d1 100644 --- a/src/Adapters/Storage/Postgres/PostgresClient.js +++ b/src/Adapters/Storage/Postgres/PostgresClient.js @@ -20,7 +20,12 @@ export function createClient(uri, databaseOptions) { if (process.env.PARSE_SERVER_LOG_LEVEL === 'debug') { const monitor = require('pg-monitor'); - monitor.attach(initOptions); + try { + monitor.attach(initOptions); + } catch (e) { + monitor.detach(); + monitor.attach(initOptions); + } } if (dbOptions.pgOptions) {