diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92ead63551..2dcff18263 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ env: jobs: check-ci: name: CI Self-Check - timeout-minutes: 30 + timeout-minutes: 15 runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -34,7 +34,7 @@ jobs: run: npm run ci:check check-lint: name: Lint - timeout-minutes: 30 + timeout-minutes: 15 runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 @@ -97,8 +97,9 @@ jobs: MONGODB_TOPOLOGY: standalone MONGODB_STORAGE_ENGINE: wiredTiger NODE_VERSION: 15.11.0 + fail-fast: false name: ${{ matrix.name }} - timeout-minutes: 30 + timeout-minutes: 15 runs-on: ubuntu-18.04 services: redis: @@ -145,8 +146,9 @@ jobs: POSTGRES_IMAGE: postgis/postgis:12-3.0 - name: Postgres 13, Postgis 3.1 POSTGRES_IMAGE: postgis/postgis:13-3.1 + fail-fast: false name: ${{ matrix.name }} - timeout-minutes: 30 + timeout-minutes: 15 runs-on: ubuntu-18.04 services: redis: diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d4010789..c462fc24fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,8 @@ ___ - Allow Cloud Validator `options` to be async (dblythy) [#7155](https://github.com/parse-community/parse-server/pull/7155) - Optimize queries on classes with pointer permissions (Pedro Diaz) [#7061](https://github.com/parse-community/parse-server/pull/7061) - Test Parse Server continuously against all relevant Postgres versions (minor versions), added Postgres compatibility table to Parse Server docs (Corey Baker) [#7176](https://github.com/parse-community/parse-server/pull/7176) +- Randomize test suite (Diamond Lewis) [#7265](https://github.com/parse-community/parse-server/pull/7265) +- LDAP: Properly unbind client on group search error (Diamond Lewis) [#7265](https://github.com/parse-community/parse-server/pull/7265) ___ ## 4.5.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd2955430f..c2254e4cc9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,6 +84,14 @@ Once you have babel running in watch mode, you can start making changes to parse * All the tests should point to sources in the `lib/` folder. * The `lib/` folder is produced by `babel` using either the `npm run build`, `npm run watch`, or the `npm run prepare` step. * The `npm run prepare` step is automatically invoked when your package depends on forked parse-server installed via git for example using `npm install --save git+https://github.com/[username]/parse-server#[branch/commit]`. +* The tests are run against a single server instance. You can change the server configurations using `await reconfigureServer({ ... some configuration })` found in `spec/helper.js`. +* The tests are ran at random. +* Caches and Configurations are reset after every test. +* Users are logged out after every test. +* Cloud Code hooks are removed after every test. +* Database is deleted after every test (indexes are not removed for speed) +* Tests are located in the `spec` folder +* For better test reporting enable `PARSE_SERVER_LOG_LEVEL=debug` ### Troubleshooting @@ -108,6 +116,7 @@ Once you have babel running in watch mode, you can start making changes to parse * Run the tests for the whole project to make sure the code passes all tests. This can be done by running the test command for a single file but removing the test file argument. The results can be seen at */coverage/lcov-report/index.html*. * Lint your code by running `npm run lint` to make sure the code is not going to be rejected by the CI. * **Do not** publish the *lib* folder. +* Mocks belong in the `spec/support` folder. * Please consider if any changes to the [docs](http://docs.parseplatform.org) are needed or add additional sections in the case of an enhancement or feature. ### Test against Postgres diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js index af23c146f6..b02e07e708 100644 --- a/spec/AdapterLoader.spec.js +++ b/spec/AdapterLoader.spec.js @@ -6,7 +6,7 @@ const Config = require('../lib/Config'); describe('AdapterLoader', () => { it('should instantiate an adapter from string in object', done => { - const adapterPath = require('path').resolve('./spec/MockAdapter'); + const adapterPath = require('path').resolve('./spec/support/MockAdapter'); const adapter = loadAdapter({ adapter: adapterPath, @@ -23,7 +23,7 @@ describe('AdapterLoader', () => { }); it('should instantiate an adapter from string', done => { - const adapterPath = require('path').resolve('./spec/MockAdapter'); + const adapterPath = require('path').resolve('./spec/support/MockAdapter'); const adapter = loadAdapter(adapterPath); expect(adapter instanceof Object).toBe(true); @@ -119,7 +119,7 @@ describe('AdapterLoader', () => { }); it('should load custom push adapter from string (#3544)', done => { - const adapterPath = require('path').resolve('./spec/MockPushAdapter'); + const adapterPath = require('path').resolve('./spec/support/MockPushAdapter'); const options = { ios: { bundleId: 'bundle.id', diff --git a/spec/CLI.spec.js b/spec/CLI.spec.js index 0cd54c2e8b..540f02abf6 100644 --- a/spec/CLI.spec.js +++ b/spec/CLI.spec.js @@ -227,6 +227,8 @@ describe('execution', () => { 'test', '--databaseURI', 'mongodb://localhost/test', + '--port', + '1339', ]); childProcess.stdout.on('data', data => { data = data.toString(); @@ -247,6 +249,8 @@ describe('execution', () => { 'test', '--databaseURI', 'mongodb://localhost/test', + '--port', + '1340', '--mountGraphQL', ]); let output = ''; @@ -271,6 +275,8 @@ describe('execution', () => { 'test', '--databaseURI', 'mongodb://localhost/test', + '--port', + '1341', '--mountGraphQL', '--mountPlayground', ]); diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 5b6a3d4ab2..996e4c3961 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -80,6 +80,9 @@ describe('FilesController', () => { expect(typeof error).toBe('object'); expect(error.message.indexOf('biscuit')).toBe(13); expect(error.code).toBe(Parse.Error.INVALID_FILE_NAME); + mockAdapter.validateFilename = () => { + return null; + }; done(); }); diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index 92f7aae388..8431d6d7f5 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -4,7 +4,6 @@ const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter') const { randomString } = require('../lib/cryptoUtils'); const databaseURI = 'mongodb://localhost:27017/parse'; const request = require('../lib/request'); -const Config = require('../lib/Config'); async function expectMissingFile(gfsAdapter, name) { try { @@ -395,8 +394,9 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { }); it('should handle getMetadata error', async () => { - const config = Config.get('test'); - config.filesController.getMetadata = () => Promise.reject(); + const gfsAdapter = new GridFSBucketAdapter(databaseURI); + await reconfigureServer({ filesAdapter: gfsAdapter }); + gfsAdapter.getMetadata = () => Promise.reject(); const headers = { 'X-Parse-Application-Id': 'test', diff --git a/spec/LdapAuth.spec.js b/spec/LdapAuth.spec.js index 09532b217b..ea30f59f0c 100644 --- a/spec/LdapAuth.spec.js +++ b/spec/LdapAuth.spec.js @@ -1,5 +1,5 @@ const ldap = require('../lib/Adapters/Auth/ldap'); -const mockLdapServer = require('./MockLdapServer'); +const mockLdapServer = require('./support/MockLdapServer'); const fs = require('fs'); const port = 12345; const sslport = 12346; @@ -19,243 +19,194 @@ describe('Ldap Auth', () => { 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', async done => { + const server = await mockLdapServer(port, 'uid=testuser, o=example'); + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + }; + await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options); + server.close(done); }); - 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', async done => { + const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true); + const options = { + suffix: 'o=example', + url: `ldaps://localhost:${sslport}`, + dn: 'uid={{id}}, o=example', + tlsOptions: { rejectUnauthorized: false }, + }; + await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options); + server.close(done); }); - 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', async done => { + const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true); + 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, + }, + }; + await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options); + server.close(done); }); - 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', async done => { + const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true); + 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, + }, + }; + try { + await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options); + fail(); + } catch (err) { + expect(err.message).toBe('LDAPS: Certificate mismatch'); + } + server.close(done); }); - 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', async done => { + const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true); + 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, + }, + }; + try { + await ldap.validateAuthData({ id: 'testuser', password: 'wrong!' }, options); + fail(); + } catch (err) { + expect(err.message).toBe('LDAP: Wrong username or password'); + } + server.close(done); }); - 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', async done => { + const server = await mockLdapServer(port, 'uid=testuser, o=example'); + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + }; + try { + await ldap.validateAuthData({ id: 'testuser', password: 'wrong!' }, options); + fail(); + } catch (err) { + expect(err.message).toBe('LDAP: Wrong username or password'); + } + server.close(done); }); - 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()); - }); + it('Should succeed if user is in given group', async done => { + const server = await mockLdapServer(port, 'uid=testuser, o=example'); + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + groupCn: 'powerusers', + groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', + }; + await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options); + server.close(done); }); - 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()); - }); + it('Should fail if user is not in given group', async done => { + const server = await mockLdapServer(port, 'uid=testuser, o=example'); + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + groupCn: 'groupTheUserIsNotIn', + groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', + }; + try { + await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options); + fail(); + } catch (err) { + expect(err.message).toBe('LDAP: User not in group'); + } + server.close(done); }); - 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()); - }); + it('Should fail if the LDAP server does not allow searching inside the provided suffix', async done => { + const server = await mockLdapServer(port, 'uid=testuser, o=example'); + const options = { + suffix: 'o=invalid', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + groupCn: 'powerusers', + groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', + }; + try { + await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options); + fail(); + } catch (err) { + expect(err.message).toBe('LDAP group search failed'); + } + server.close(done); }); - 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()); - }); + it('Should fail if the LDAP server encounters an error while searching', async done => { + const server = await mockLdapServer(port, 'uid=testuser, o=example', true); + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + groupCn: 'powerusers', + groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', + }; + try { + await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options); + fail(); + } catch (err) { + expect(err.message).toBe('LDAP group search failed'); + } + server.close(done); }); - 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' }; - - ldap - .validateAuthData(authData, options) - .then(() => { - expect(authData).toEqual({ id: 'testuser' }); - done(); - }) - .catch(done.fail) - .finally(() => server.close()); - }); + it('Should delete the password from authData after validation', async done => { + const server = await mockLdapServer(port, 'uid=testuser, o=example', true); + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + }; + const authData = { id: 'testuser', password: 'secret' }; + await ldap.validateAuthData(authData, options); + expect(authData).toEqual({ id: 'testuser' }); + server.close(done); }); - 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', async done => { + const server = await mockLdapServer(port, 'uid=testuser, o=example', true); + const options = { + suffix: 'o=example', + url: `ldap://localhost:${port}`, + dn: 'uid={{id}}, o=example', + }; + await reconfigureServer({ auth: { ldap: options } }); + const authData = { authData: { id: 'testuser', password: 'secret' } }; + const returnedUser = await Parse.User.logInWith('ldap', authData); + const query = new Parse.Query('User'); + const user = await query.equalTo('objectId', returnedUser.id).first({ useMasterKey: true }); + expect(user.get('authData')).toEqual({ ldap: { id: 'testuser' } }); + expect(user.get('authData').ldap.password).toBeUndefined(); + server.close(done); }); }); diff --git a/spec/LoggerController.spec.js b/spec/LoggerController.spec.js index 7b2e786789..37d477444c 100644 --- a/spec/LoggerController.spec.js +++ b/spec/LoggerController.spec.js @@ -70,6 +70,7 @@ describe('LoggerController', () => { }; const loggerController = new LoggerController(new WinstonLoggerAdapter()); + loggerController.error('can process an ascending query without throwing'); expect(() => { loggerController @@ -115,6 +116,7 @@ describe('LoggerController', () => { }; const loggerController = new LoggerController(new WinstonLoggerAdapter()); + loggerController.error('can process a descending query without throwing'); expect(() => { loggerController diff --git a/spec/Parse.Push.spec.js b/spec/Parse.Push.spec.js index df2b002862..1732e426e3 100644 --- a/spec/Parse.Push.spec.js +++ b/spec/Parse.Push.spec.js @@ -2,10 +2,15 @@ const request = require('../lib/request'); +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + const pushCompleted = async pushId => { - let result = await Parse.Push.getPushStatus(pushId); + const query = new Parse.Query('_PushStatus'); + query.equalTo('objectId', pushId); + let result = await query.first({ useMasterKey: true }); while (!(result && result.get('status') === 'succeeded')) { - result = await Parse.Push.getPushStatus(pushId); + await sleep(100); + result = await query.first({ useMasterKey: true }); } }; diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 67bac737b7..7afd8a549d 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -5,7 +5,7 @@ const fetch = require('node-fetch'); const FormData = require('form-data'); const ws = require('ws'); require('./helper'); -const { updateCLP } = require('./dev'); +const { updateCLP } = require('./support/dev'); const pluralize = require('pluralize'); const { getMainDefinition } = require('apollo-utilities'); @@ -9033,7 +9033,7 @@ describe('ParseGraphQLServer', () => { it('should support object values', async () => { try { - const someFieldValue = { + const someObjectFieldValue = { foo: { bar: 'baz' }, number: 10, }; @@ -9048,7 +9048,7 @@ describe('ParseGraphQLServer', () => { `, variables: { schemaFields: { - addObjects: [{ name: 'someField' }], + addObjects: [{ name: 'someObjectField' }], }, }, context: { @@ -9057,11 +9057,10 @@ describe('ParseGraphQLServer', () => { }, }, }); - await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const schema = await new Parse.Schema('SomeClass').get(); - expect(schema.fields.someField.type).toEqual('Object'); + expect(schema.fields.someObjectField.type).toEqual('Object'); const createResult = await apolloClient.mutate({ mutation: gql` @@ -9075,13 +9074,13 @@ describe('ParseGraphQLServer', () => { `, variables: { fields: { - someField: someFieldValue, + someObjectField: someObjectFieldValue, }, }, }); const where = { - someField: { + someObjectField: { equalTo: { key: 'foo.bar', value: 'baz' }, notEqualTo: { key: 'foo.bar', value: 'bat' }, greaterThan: { key: 'number', value: 9 }, @@ -9093,13 +9092,13 @@ describe('ParseGraphQLServer', () => { query GetSomeObject($id: ID!, $where: SomeClassWhereInput) { someClass(id: $id) { id - someField + someObjectField } someClasses(where: $where) { edges { node { id - someField + someObjectField } } } @@ -9113,13 +9112,13 @@ describe('ParseGraphQLServer', () => { const { someClass: getResult, someClasses } = queryResult.data; - const { someField } = getResult; - expect(typeof someField).toEqual('object'); - expect(someField).toEqual(someFieldValue); + const { someObjectField } = getResult; + expect(typeof someObjectField).toEqual('object'); + expect(someObjectField).toEqual(someObjectFieldValue); // Checks class query results expect(someClasses.edges.length).toEqual(1); - expect(someClasses.edges[0].node.someField).toEqual(someFieldValue); + expect(someClasses.edges[0].node.someObjectField).toEqual(someObjectFieldValue); } catch (e) { handleError(e); } @@ -9127,11 +9126,11 @@ describe('ParseGraphQLServer', () => { it('should support object composed queries', async () => { try { - const someFieldValue = { + const someObjectFieldValue1 = { lorem: 'ipsum', number: 10, }; - const someFieldValue2 = { + const someObjectFieldValue2 = { foo: { test: 'bar', }, @@ -9144,7 +9143,7 @@ describe('ParseGraphQLServer', () => { createClass( input: { name: "SomeClass" - schemaFields: { addObjects: [{ name: "someField" }] } + schemaFields: { addObjects: [{ name: "someObjectField" }] } } ) { clientMutationId @@ -9180,10 +9179,10 @@ describe('ParseGraphQLServer', () => { `, variables: { fields1: { - someField: someFieldValue, + someObjectField: someObjectFieldValue1, }, fields2: { - someField: someFieldValue2, + someObjectField: someObjectFieldValue2, }, }, }); @@ -9191,24 +9190,24 @@ describe('ParseGraphQLServer', () => { const where = { AND: [ { - someField: { + someObjectField: { greaterThan: { key: 'number', value: 9 }, }, }, { - someField: { + someObjectField: { lessThan: { key: 'number', value: 11 }, }, }, { OR: [ { - someField: { + someObjectField: { equalTo: { key: 'lorem', value: 'ipsum' }, }, }, { - someField: { + someObjectField: { equalTo: { key: 'foo.test', value: 'bar' }, }, }, @@ -9223,7 +9222,7 @@ describe('ParseGraphQLServer', () => { edges { node { id - someField + someObjectField } } } @@ -9241,11 +9240,11 @@ describe('ParseGraphQLServer', () => { const { edges } = someClasses; expect(edges.length).toEqual(2); expect( - edges.find(result => result.node.id === create1.someClass.id).node.someField - ).toEqual(someFieldValue); + edges.find(result => result.node.id === create1.someClass.id).node.someObjectField + ).toEqual(someObjectFieldValue1); expect( - edges.find(result => result.node.id === create2.someClass.id).node.someField - ).toEqual(someFieldValue2); + edges.find(result => result.node.id === create2.someClass.id).node.someObjectField + ).toEqual(someObjectFieldValue2); } catch (e) { handleError(e); } @@ -9253,7 +9252,7 @@ describe('ParseGraphQLServer', () => { it('should support array values', async () => { try { - const someFieldValue = [1, 'foo', ['bar'], { lorem: 'ipsum' }, true]; + const someArrayFieldValue = [1, 'foo', ['bar'], { lorem: 'ipsum' }, true]; await apolloClient.mutate({ mutation: gql` @@ -9265,7 +9264,7 @@ describe('ParseGraphQLServer', () => { `, variables: { schemaFields: { - addArrays: [{ name: 'someField' }], + addArrays: [{ name: 'someArrayField' }], }, }, context: { @@ -9278,7 +9277,7 @@ describe('ParseGraphQLServer', () => { await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); const schema = await new Parse.Schema('SomeClass').get(); - expect(schema.fields.someField.type).toEqual('Array'); + expect(schema.fields.someArrayField.type).toEqual('Array'); const createResult = await apolloClient.mutate({ mutation: gql` @@ -9292,7 +9291,7 @@ describe('ParseGraphQLServer', () => { `, variables: { fields: { - someField: someFieldValue, + someArrayField: someArrayFieldValue, }, }, }); @@ -9301,17 +9300,17 @@ describe('ParseGraphQLServer', () => { query: gql` query GetSomeObject($id: ID!) { someClass(id: $id) { - someField { + someArrayField { ... on Element { value } } } - someClasses(where: { someField: { exists: true } }) { + someClasses(where: { someArrayField: { exists: true } }) { edges { node { id - someField { + someArrayField { ... on Element { value } @@ -9326,9 +9325,9 @@ describe('ParseGraphQLServer', () => { }, }); - const { someField } = getResult.data.someClass; - expect(Array.isArray(someField)).toBeTruthy(); - expect(someField.map(element => element.value)).toEqual(someFieldValue); + const { someArrayField } = getResult.data.someClass; + expect(Array.isArray(someArrayField)).toBeTruthy(); + expect(someArrayField.map(element => element.value)).toEqual(someArrayFieldValue); expect(getResult.data.someClasses.edges.length).toEqual(1); } catch (e) { handleError(e); @@ -10201,101 +10200,99 @@ describe('ParseGraphQLServer', () => { let apolloClient; beforeEach(async () => { - if (!httpServer) { - 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', - }, + 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', }, - }), - 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(); - }, + }, + }), + 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, }, - }), - types: [ - new GraphQLInputObjectType({ - name: 'CreateSomeClassFieldsInput', - fields: { - type: { type: TypeEnum }, + customQueryWithAutoTypeReturn: { + type: SomeClassType, + args: { + id: { type: new GraphQLNonNull(GraphQLString) }, }, - }), - new GraphQLInputObjectType({ - name: 'UpdateSomeClassFieldsInput', - fields: { - type: { type: TypeEnum }, + resolve: async (p, { id }) => { + const obj = new Parse.Object('SomeClass'); + obj.id = id; + await obj.fetch(); + return obj.toJSON(); }, - }), - SomeClassType, - ], + }, + }, }), - }); - - parseGraphQLServer.applyGraphQL(expressApp); - await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); - const httpLink = createUploadLink({ - uri: 'http://localhost:13377/graphql', - fetch, - headers, + types: [ + new GraphQLInputObjectType({ + name: 'CreateSomeClassFieldsInput', + fields: { + type: { type: TypeEnum }, + }, + }), + new GraphQLInputObjectType({ + name: 'UpdateSomeClassFieldsInput', + fields: { + type: { type: TypeEnum }, + }, + }), + SomeClassType, + ], + }), }); - 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 () => { + afterEach(async () => { await httpServer.close(); }); diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index 9434b6f10a..a0f354f8ca 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -8,9 +8,8 @@ const bodyParser = require('body-parser'); const auth = require('../lib/Auth'); const Config = require('../lib/Config'); -const port = 12345; +const port = 34567; const hookServerURL = 'http://localhost:' + port; -const AppCache = require('../lib/cache').AppCache; describe('Hooks', () => { let server; @@ -19,7 +18,7 @@ describe('Hooks', () => { if (!app) { app = express(); app.use(bodyParser.json({ type: '*/*' })); - server = app.listen(12345, undefined, done); + server = app.listen(port, undefined, done); } else { done(); } @@ -383,7 +382,7 @@ describe('Hooks', () => { } const hooksController = new HooksController( Parse.applicationId, - AppCache.get('test').databaseController + Config.get('test').database ); return hooksController.load(); }, diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 66fae587d8..5e9c71d731 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -127,7 +127,10 @@ describe('ParseLiveQueryServer', function () { serverStartComplete: () => { expect(parseServer.liveQueryServer).not.toBeUndefined(); expect(parseServer.liveQueryServer.server).toBe(parseServer.server); - parseServer.server.close(done); + parseServer.server.close(async () => { + await reconfigureServer(); + done(); + }); }, }); }); @@ -149,7 +152,10 @@ describe('ParseLiveQueryServer', function () { expect(parseServer.liveQueryServer).not.toBeUndefined(); expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server); parseServer.liveQueryServer.server.close( - parseServer.server.close.bind(parseServer.server, done) + parseServer.server.close.bind(parseServer.server, async () => { + await reconfigureServer(); + done(); + }) ); }, }); diff --git a/spec/ParseServerRESTController.spec.js b/spec/ParseServerRESTController.spec.js index 2a84edaa4e..90fe383257 100644 --- a/spec/ParseServerRESTController.spec.js +++ b/spec/ParseServerRESTController.spec.js @@ -169,25 +169,25 @@ describe('ParseServerRESTController', () => { process.env.PARSE_SERVER_TEST_DB === 'postgres' ) { describe('transactions', () => { - let parseServer; beforeEach(async () => { + await TestUtils.destroyAllDataPermanently(true); if ( semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger' ) { - if (!parseServer) { - parseServer = await reconfigureServer({ - databaseAdapter: undefined, - databaseURI: - 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', - }); - } - await TestUtils.destroyAllDataPermanently(true); + await reconfigureServer({ + databaseAdapter: undefined, + databaseURI: + 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', + }); + } else { + await reconfigureServer(); } }); - 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() @@ -236,117 +236,113 @@ describe('ParseServerRESTController', () => { .catch(done.fail); }); - it('should not save anything when one operation fails in a transaction', done => { + it('should not save anything when one operation fails in a transaction', async () => { const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections - myObject - .save() - .then(() => { - return myObject.destroy(); - }) - .then(() => { - RESTController.request('POST', 'batch', { - requests: [ - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - ], - transaction: true, - }).catch(error => { - expect(error).toBeDefined(); - const query = new Parse.Query('MyObject'); - query.find().then(results => { - expect(results.length).toBe(0); - done(); - }); - }); + await myObject.save(); + await myObject.destroy(); + try { + await RESTController.request('POST', 'batch', { + requests: [ + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + ], + transaction: true, }); + fail(); + } catch (error) { + expect(error).toBeDefined(); + const query = new Parse.Query('MyObject'); + const results = await query.find(); + expect(results.length).toBe(0); + } }); it('should generate separate session for each call', async () => { diff --git a/spec/PostgresInitOptions.spec.js b/spec/PostgresInitOptions.spec.js index 73f040701f..069d5a6437 100644 --- a/spec/PostgresInitOptions.spec.js +++ b/spec/PostgresInitOptions.spec.js @@ -28,7 +28,7 @@ function createParseServer(options) { return new Promise((resolve, reject) => { const parseServer = new ParseServer.default( Object.assign({}, defaultConfiguration, options, { - serverURL: 'http://localhost:12666/parse', + serverURL: 'http://localhost:12668/parse', serverStartComplete: error => { if (error) { reject(error); @@ -37,8 +37,8 @@ function createParseServer(options) { const app = express(); app.use('/parse', parseServer.app); - const server = app.listen(12666); - Parse.serverURL = 'http://localhost:12666/parse'; + const server = app.listen(12668); + Parse.serverURL = 'http://localhost:12668/parse'; resolve(server); } }, diff --git a/spec/ProtectedFields.spec.js b/spec/ProtectedFields.spec.js index 1bdc3177f1..c3374923db 100644 --- a/spec/ProtectedFields.spec.js +++ b/spec/ProtectedFields.spec.js @@ -1,7 +1,7 @@ const Config = require('../lib/Config'); const Parse = require('parse/node'); const request = require('../lib/request'); -const { className, createRole, createUser, logIn, updateCLP } = require('./dev'); +const { className, createRole, createUser, logIn, updateCLP } = require('./support/dev'); describe('ProtectedFields', function () { it('should handle and empty protectedFields', async function () { diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 873116bcd4..4626b5e0a7 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -26,10 +26,15 @@ const successfulIOS = function (body, installations) { return Promise.all(promises); }; +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + const pushCompleted = async pushId => { - let result = await Parse.Push.getPushStatus(pushId); + const query = new Parse.Query('_PushStatus'); + query.equalTo('objectId', pushId); + let result = await query.first({ useMasterKey: true }); while (!(result && result.get('status') === 'succeeded')) { - result = await Parse.Push.getPushStatus(pushId); + await sleep(100); + result = await query.first({ useMasterKey: true }); } }; @@ -568,7 +573,7 @@ describe('PushController', () => { await pushCompleted(pushStatusId); }); - it('should properly report failures in _PushStatus', done => { + it('should properly report failures in _PushStatus', async () => { const pushAdapter = { send: function (body, installations) { return installations.map(installation => { @@ -593,30 +598,27 @@ describe('PushController', () => { badge: 1, }, }; - const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushController = new PushController(); - reconfigureServer({ + await reconfigureServer({ push: { adapter: pushAdapter }, - }) - .then(() => { - return pushController.sendPush(payload, where, config, auth); - }) - .then(() => { - fail('should not succeed'); - done(); - }) - .catch(() => { - const query = new Parse.Query('_PushStatus'); - query.find({ useMasterKey: true }).then(results => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('status')).toBe('failed'); - done(); - }); - }); + }); + const config = Config.get(Parse.applicationId); + try { + await pushController.sendPush(payload, where, config, auth); + fail(); + } catch (e) { + const query = new Parse.Query('_PushStatus'); + let results = await query.find({ useMasterKey: true }); + while (results.length === 0) { + results = await query.find({ useMasterKey: true }); + } + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('status')).toBe('failed'); + } }); it('should support full RESTQuery for increment', async () => { @@ -1237,7 +1239,7 @@ describe('PushController', () => { const auth = { isMaster: true }; const pushController = new PushController(); - let config = Config.get(Parse.applicationId); + let config; const pushes = []; const pushAdapter = { diff --git a/spec/RedisCacheAdapter.spec.js b/spec/RedisCacheAdapter.spec.js index ac5c209c82..c980e85b7e 100644 --- a/spec/RedisCacheAdapter.spec.js +++ b/spec/RedisCacheAdapter.spec.js @@ -1,6 +1,11 @@ const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default; const Config = require('../lib/Config'); +function wait(sleep) { + return new Promise(function (resolve) { + setTimeout(resolve, sleep); + }); +} /* To run this test part of the complete suite set PARSE_SERVER_TEST_CACHE='redis' @@ -11,31 +16,30 @@ describe_only(() => { })('RedisCacheAdapter', function () { const KEY = 'hello'; const VALUE = 'world'; + let cache; - function wait(sleep) { - return new Promise(function (resolve) { - setTimeout(resolve, sleep); - }); - } + beforeEach(async () => { + cache = new RedisCacheAdapter(null, 100); + await cache.clear(); + }); it('should get/set/clear', done => { - const cache = new RedisCacheAdapter({ + const cacheNaN = new RedisCacheAdapter({ ttl: NaN, }); - cache + cacheNaN .put(KEY, VALUE) - .then(() => cache.get(KEY)) + .then(() => cacheNaN.get(KEY)) .then(value => expect(value).toEqual(VALUE)) - .then(() => cache.clear()) - .then(() => cache.get(KEY)) + .then(() => cacheNaN.clear()) + .then(() => cacheNaN.get(KEY)) .then(value => expect(value).toEqual(null)) + .then(() => cacheNaN.clear()) .then(done); }); it('should expire after ttl', done => { - const cache = new RedisCacheAdapter(null, 100); - cache .put(KEY, VALUE) .then(() => cache.get(KEY)) @@ -47,8 +51,6 @@ describe_only(() => { }); it('should not store value for ttl=0', done => { - const cache = new RedisCacheAdapter(null, 100); - cache .put(KEY, VALUE, 0) .then(() => cache.get(KEY)) @@ -57,8 +59,6 @@ describe_only(() => { }); it('should not expire when ttl=Infinity', done => { - const cache = new RedisCacheAdapter(null, 100); - cache .put(KEY, VALUE, Infinity) .then(() => cache.get(KEY)) @@ -70,7 +70,6 @@ describe_only(() => { }); it('should fallback to default ttl', done => { - const cache = new RedisCacheAdapter(null, 100); let promise = Promise.resolve(); [-100, null, undefined, 'not number', true].forEach(ttl => { @@ -89,8 +88,6 @@ describe_only(() => { }); it('should find un-expired records', done => { - const cache = new RedisCacheAdapter(null, 100); - cache .put(KEY, VALUE) .then(() => cache.get(KEY)) @@ -102,8 +99,6 @@ describe_only(() => { }); it('handleShutdown, close connection', async () => { - const cache = new RedisCacheAdapter(null, 100); - await cache.handleShutdown(); setTimeout(() => { expect(cache.client.connected).toBe(false); diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 94846c59bb..6bcc454baf 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -1,6 +1,5 @@ const UserController = require('../lib/Controllers/UserController').UserController; -const emailAdapter = require('./MockEmailAdapter'); -const AppCache = require('../lib/cache').AppCache; +const emailAdapter = require('./support/MockEmailAdapter'); describe('UserController', () => { const user = { @@ -11,55 +10,45 @@ describe('UserController', () => { describe('sendVerificationEmail', () => { describe('parseFrameURL not provided', () => { - it('uses publicServerURL', done => { - AppCache.put( - defaultConfiguration.appId, - Object.assign({}, defaultConfiguration, { - publicServerURL: 'http://www.example.com', - customPages: { - parseFrameURL: undefined, - }, - }) - ); - + it('uses publicServerURL', async done => { + await reconfigureServer({ + publicServerURL: 'http://www.example.com', + customPages: { + parseFrameURL: undefined, + }, + }); emailAdapter.sendVerificationEmail = options => { expect(options.link).toEqual( 'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser' ); + emailAdapter.sendVerificationEmail = () => Promise.resolve(); done(); }; - const userController = new UserController(emailAdapter, 'test', { verifyUserEmails: true, }); - userController.sendVerificationEmail(user); }); }); describe('parseFrameURL provided', () => { - it('uses parseFrameURL and includes the destination in the link parameter', done => { - AppCache.put( - defaultConfiguration.appId, - Object.assign({}, defaultConfiguration, { - publicServerURL: 'http://www.example.com', - customPages: { - parseFrameURL: 'http://someother.example.com/handle-parse-iframe', - }, - }) - ); - + it('uses parseFrameURL and includes the destination in the link parameter', async done => { + await reconfigureServer({ + publicServerURL: 'http://www.example.com', + customPages: { + parseFrameURL: 'http://someother.example.com/handle-parse-iframe', + }, + }); emailAdapter.sendVerificationEmail = options => { expect(options.link).toEqual( 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser' ); + emailAdapter.sendVerificationEmail = () => Promise.resolve(); done(); }; - const userController = new UserController(emailAdapter, 'test', { verifyUserEmails: true, }); - userController.sendVerificationEmail(user); }); }); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 94d9793a39..d7cc72c7b0 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -1,6 +1,6 @@ 'use strict'; -const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); +const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions'); const request = require('../lib/request'); const Config = require('../lib/Config'); diff --git a/spec/VerifyUserPassword.spec.js b/spec/VerifyUserPassword.spec.js index e77657b8e9..ad4571226e 100644 --- a/spec/VerifyUserPassword.spec.js +++ b/spec/VerifyUserPassword.spec.js @@ -1,7 +1,7 @@ 'use strict'; const request = require('../lib/request'); -const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); +const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions'); const verifyPassword = function (login, password, isEmail = false) { const body = !isEmail ? { username: login, password } : { email: login, password }; diff --git a/spec/batch.spec.js b/spec/batch.spec.js index a1041e4ccc..98050254ba 100644 --- a/spec/batch.spec.js +++ b/spec/batch.spec.js @@ -175,6 +175,7 @@ describe('batch', () => { ) { describe('transactions', () => { beforeEach(async () => { + await TestUtils.destroyAllDataPermanently(true); if ( semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && @@ -185,7 +186,8 @@ describe('batch', () => { databaseURI: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', }); - await TestUtils.destroyAllDataPermanently(true); + } else { + await reconfigureServer(); } }); @@ -243,122 +245,117 @@ describe('batch', () => { }); }); - it('should not save anything when one operation fails in a transaction', done => { + it('should not save anything when one operation fails in a transaction', async () => { const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections - myObject - .save() - .then(() => { - return myObject.destroy(); - }) - .then(() => { - request({ - method: 'POST', - headers: headers, - url: 'http://localhost:8378/1/batch', - body: JSON.stringify({ - requests: [ - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 'value1' }, - }, - { - method: 'POST', - path: '/1/classes/MyObject', - body: { key: 10 }, - }, - ], - transaction: true, - }), - }).catch(error => { - expect(error.data).toBeDefined(); - const query = new Parse.Query('MyObject'); - query.find().then(results => { - expect(results.length).toBe(0); - done(); - }); - }); + await myObject.save(); + await myObject.destroy(); + try { + await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/batch', + body: JSON.stringify({ + requests: [ + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 'value1' }, + }, + { + method: 'POST', + path: '/1/classes/MyObject', + body: { key: 10 }, + }, + ], + transaction: true, + }), }); + } catch (error) { + expect(error).toBeDefined(); + const query = new Parse.Query('MyObject'); + const results = await query.find(); + expect(results.length).toBe(0); + } }); it('should generate separate session for each call', async () => { diff --git a/spec/helper.js b/spec/helper.js index 68254f518f..8ee874a93f 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,14 +1,12 @@ 'use strict'; const semver = require('semver'); const CurrentSpecReporter = require('./support/CurrentSpecReporter.js'); +const { SpecReporter } = require('jasmine-spec-reporter'); // Sets up a Parse API server for testing. 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()); -} +jasmine.getEnv().addReporter(new SpecReporter()); global.on_db = (db, callback, elseCallback) => { if (process.env.PARSE_SERVER_TEST_DB == db) { @@ -28,6 +26,7 @@ if (global._babelPolyfill) { process.noDeprecation = true; const cache = require('../lib/cache').default; +const defaults = require('../lib/defaults').default; const ParseServer = require('../lib/index').ParseServer; const path = require('path'); const TestUtils = require('../lib/TestUtils'); @@ -113,7 +112,7 @@ const defaultConfiguration = { custom: mockCustom(), facebook: mockFacebook(), myoauth: { - module: path.resolve(__dirname, 'myoauth'), // relative path as it's run from src + module: path.resolve(__dirname, 'support/myoauth'), // relative path as it's run from src }, shortLivedAuth: mockShortLivedAuth(), }, @@ -124,6 +123,16 @@ if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') { } const openConnections = {}; +const destroyAliveConnections = function () { + for (const socketId in openConnections) { + try { + openConnections[socketId].destroy(); + delete openConnections[socketId]; + } catch (e) { + /* */ + } + } +}; // Set up a default API server for testing with default configuration. let server; @@ -146,7 +155,6 @@ const reconfigureServer = (changedConfiguration = {}) => { if (error) { reject(error); } else { - Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1); resolve(parseServer); } }, @@ -194,8 +202,9 @@ beforeAll(async () => { afterEach(function (done) { const afterLogOut = async () => { if (Object.keys(openConnections).length > 0) { - fail('There were open connections to the server left after the test finished'); + console.warn('There were open connections to the server left after the test finished'); } + destroyAliveConnections(); await TestUtils.destroyAllDataPermanently(true); if (didChangeConfiguration) { await reconfigureServer(); @@ -205,6 +214,7 @@ afterEach(function (done) { done(); }; Parse.Cloud._removeAllHooks(); + defaults.protectedFields = { _User: { '*': ['email'] } }; databaseAdapter .getAllClasses() .then(allSchemas => { diff --git a/spec/index.spec.js b/spec/index.spec.js index 65636ee13d..73be17cb51 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -1,7 +1,7 @@ 'use strict'; const request = require('../lib/request'); const parseServerPackage = require('../package.json'); -const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); +const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions'); const ParseServer = require('../lib/index'); const Config = require('../lib/Config'); const express = require('express'); @@ -317,10 +317,16 @@ describe('server', () => { }) .then(obj => { expect(obj.id).toEqual(objId); - server.close(done); + server.close(async () => { + await reconfigureServer(); + done(); + }); }) .catch(() => { - server.close(done); + server.close(async () => { + await reconfigureServer(); + done(); + }); }); }, }) @@ -354,12 +360,18 @@ describe('server', () => { }) .then(obj => { expect(obj.id).toEqual(objId); - server.close(done); + server.close(async () => { + await reconfigureServer(); + done(); + }); }) .catch(error => { fail(JSON.stringify(error)); if (server) { - server.close(done); + server.close(async () => { + await reconfigureServer(); + done(); + }); } else { done(); } diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index cba50f387f..e535b07a07 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1274,6 +1274,7 @@ describe('schemas', () => { }, }, }).then(response => { + delete response.data.indexes; expect( dd(response.data, { className: '_User', @@ -1302,6 +1303,7 @@ describe('schemas', () => { headers: masterKeyHeaders, json: true, }).then(response => { + delete response.data.indexes; expect( dd(response.data, { className: '_User', diff --git a/spec/MockAdapter.js b/spec/support/MockAdapter.js similarity index 100% rename from spec/MockAdapter.js rename to spec/support/MockAdapter.js diff --git a/spec/MockEmailAdapter.js b/spec/support/MockEmailAdapter.js similarity index 100% rename from spec/MockEmailAdapter.js rename to spec/support/MockEmailAdapter.js diff --git a/spec/MockEmailAdapterWithOptions.js b/spec/support/MockEmailAdapterWithOptions.js similarity index 100% rename from spec/MockEmailAdapterWithOptions.js rename to spec/support/MockEmailAdapterWithOptions.js diff --git a/spec/MockLdapServer.js b/spec/support/MockLdapServer.js similarity index 90% rename from spec/MockLdapServer.js rename to spec/support/MockLdapServer.js index c621ee7f1e..badc28fbab 100644 --- a/spec/MockLdapServer.js +++ b/spec/support/MockLdapServer.js @@ -2,8 +2,8 @@ const ldapjs = require('ldapjs'); const fs = require('fs'); const tlsOptions = { - key: fs.readFileSync(__dirname + '/support/cert/key.pem'), - certificate: fs.readFileSync(__dirname + '/support/cert/cert.pem'), + key: fs.readFileSync(__dirname + '/cert/key.pem'), + certificate: fs.readFileSync(__dirname + '/cert/cert.pem'), }; function newServer(port, dn, provokeSearchError = false, ssl = false) { diff --git a/spec/MockPushAdapter.js b/spec/support/MockPushAdapter.js similarity index 100% rename from spec/MockPushAdapter.js rename to spec/support/MockPushAdapter.js diff --git a/spec/dev.js b/spec/support/dev.js similarity index 98% rename from spec/dev.js rename to spec/support/dev.js index c58879a533..93f73aa5a5 100644 --- a/spec/dev.js +++ b/spec/support/dev.js @@ -1,4 +1,4 @@ -const Config = require('../lib/Config'); +const Config = require('../../lib/Config'); const Parse = require('parse/node'); const className = 'AnObject'; diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index 1fbe0c31bf..84d7629c1b 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -2,5 +2,5 @@ "spec_dir": "spec", "spec_files": ["*spec.js"], "helpers": ["helper.js"], - "random": false + "random": true } diff --git a/spec/myoauth.js b/spec/support/myoauth.js similarity index 100% rename from spec/myoauth.js rename to spec/support/myoauth.js diff --git a/src/Adapters/Auth/ldap.js b/src/Adapters/Auth/ldap.js index 38a64927ff..7cea9e3f2b 100644 --- a/src/Adapters/Auth/ldap.js +++ b/src/Adapters/Auth/ldap.js @@ -95,6 +95,8 @@ function searchForGroup(client, options, id, resolve, reject) { } }); res.on('error', () => { + client.unbind(); + client.destroy(); return reject(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'LDAP group search failed')); }); }); diff --git a/src/Adapters/Cache/RedisCacheAdapter/index.js b/src/Adapters/Cache/RedisCacheAdapter/index.js index 17662628af..dfe89e1621 100644 --- a/src/Adapters/Cache/RedisCacheAdapter/index.js +++ b/src/Adapters/Cache/RedisCacheAdapter/index.js @@ -5,8 +5,9 @@ import { KeyPromiseQueue } from './KeyPromiseQueue'; const DEFAULT_REDIS_TTL = 30 * 1000; // 30 seconds in milliseconds const FLUSH_DB_KEY = '__flush_db__'; -function debug() { - logger.debug.apply(logger, ['RedisCacheAdapter', ...arguments]); +function debug(...args: any) { + const message = ['RedisCacheAdapter: ' + arguments[0]].concat(args.slice(1, args.length)); + logger.debug.apply(logger, message); } const isValidTTL = ttl => typeof ttl === 'number' && ttl > 0; @@ -33,13 +34,13 @@ export class RedisCacheAdapter { } get(key) { - debug('get', key); + debug('get', { key }); return this.queue.enqueue( key, () => new Promise(resolve => { this.client.get(key, function (err, res) { - debug('-> get', key, res); + debug('-> get', { key, res }); if (!res) { return resolve(null); } @@ -51,7 +52,7 @@ export class RedisCacheAdapter { put(key, value, ttl = this.ttl) { value = JSON.stringify(value); - debug('put', key, value, ttl); + debug('put', { key, value, ttl }); if (ttl === 0) { // ttl of zero is a logical no-op, but redis cannot set expire time of zero @@ -86,7 +87,7 @@ export class RedisCacheAdapter { } del(key) { - debug('del', key); + debug('del', { key }); return this.queue.enqueue( key, () => diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index a5e7d2838a..8253746f8a 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1215,16 +1215,10 @@ export default class SchemaController { const promises = []; for (const fieldName in object) { - if (object[fieldName] === undefined) { - continue; - } - const expected = getType(object[fieldName]); - if (expected === 'GeoPoint') { + if (object[fieldName] && getType(object[fieldName]) === 'GeoPoint') { geocount++; } if (geocount > 1) { - // Make sure all field validation operations run before we return. - // If not - we are continuing to run logic, but already provided response from the server. return Promise.reject( new Parse.Error( Parse.Error.INCORRECT_TYPE, @@ -1232,6 +1226,12 @@ export default class SchemaController { ) ); } + } + for (const fieldName in object) { + if (object[fieldName] === undefined) { + continue; + } + const expected = getType(object[fieldName]); if (!expected) { continue; }