[WIP] Enable test suite to be randomized (#7265)

* initial run

* Update ParseGraphQLServer.spec.js

* temporarily enable reporter

* Bump retry limit

* fix undefined database

* try to catch error

* Handle LiveQueryServers

* Update Config.js

* fast-fail false

* Remove usage of AppCache

* oops

* Update contributing guide

* enable debugger, try network retry attempt 1

* Fix ldap unbinding

* move non specs to support

* add missing mock adapter

* fix Parse.Push

* RestController should match batch.spec.js

* Remove request attempt limit

* handle index.spec.js

* Update CHANGELOG.md

* Handle error: tuple concurrently updated

* test transactions

* Clear RedisCache after every test

* LoggerController.spec.js

* Update schemas.spec.js

* finally fix transactions

* fix geopoint deadlock

* transaction with clean database

* batch.spec.js
This commit is contained in:
Diamond Lewis
2021-03-15 02:04:09 -05:00
committed by GitHub
parent 9563793303
commit 1666c3e382
36 changed files with 688 additions and 700 deletions

View File

@@ -13,7 +13,7 @@ env:
jobs: jobs:
check-ci: check-ci:
name: CI Self-Check name: CI Self-Check
timeout-minutes: 30 timeout-minutes: 15
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -34,7 +34,7 @@ jobs:
run: npm run ci:check run: npm run ci:check
check-lint: check-lint:
name: Lint name: Lint
timeout-minutes: 30 timeout-minutes: 15
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -97,8 +97,9 @@ jobs:
MONGODB_TOPOLOGY: standalone MONGODB_TOPOLOGY: standalone
MONGODB_STORAGE_ENGINE: wiredTiger MONGODB_STORAGE_ENGINE: wiredTiger
NODE_VERSION: 15.11.0 NODE_VERSION: 15.11.0
fail-fast: false
name: ${{ matrix.name }} name: ${{ matrix.name }}
timeout-minutes: 30 timeout-minutes: 15
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
services: services:
redis: redis:
@@ -145,8 +146,9 @@ jobs:
POSTGRES_IMAGE: postgis/postgis:12-3.0 POSTGRES_IMAGE: postgis/postgis:12-3.0
- name: Postgres 13, Postgis 3.1 - name: Postgres 13, Postgis 3.1
POSTGRES_IMAGE: postgis/postgis:13-3.1 POSTGRES_IMAGE: postgis/postgis:13-3.1
fail-fast: false
name: ${{ matrix.name }} name: ${{ matrix.name }}
timeout-minutes: 30 timeout-minutes: 15
runs-on: ubuntu-18.04 runs-on: ubuntu-18.04
services: services:
redis: redis:

View File

@@ -114,6 +114,8 @@ ___
- Allow Cloud Validator `options` to be async (dblythy) [#7155](https://github.com/parse-community/parse-server/pull/7155) - 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) - 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) - 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 ## 4.5.0
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0) [Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)

View File

@@ -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. * 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 `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 `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 ### 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 *<PROJECT_ROOT>/coverage/lcov-report/index.html*. * 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 *<PROJECT_ROOT>/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. * 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. * **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. * 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 ### Test against Postgres

View File

@@ -6,7 +6,7 @@ const Config = require('../lib/Config');
describe('AdapterLoader', () => { describe('AdapterLoader', () => {
it('should instantiate an adapter from string in object', done => { 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({ const adapter = loadAdapter({
adapter: adapterPath, adapter: adapterPath,
@@ -23,7 +23,7 @@ describe('AdapterLoader', () => {
}); });
it('should instantiate an adapter from string', done => { 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); const adapter = loadAdapter(adapterPath);
expect(adapter instanceof Object).toBe(true); expect(adapter instanceof Object).toBe(true);
@@ -119,7 +119,7 @@ describe('AdapterLoader', () => {
}); });
it('should load custom push adapter from string (#3544)', done => { 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 = { const options = {
ios: { ios: {
bundleId: 'bundle.id', bundleId: 'bundle.id',

View File

@@ -227,6 +227,8 @@ describe('execution', () => {
'test', 'test',
'--databaseURI', '--databaseURI',
'mongodb://localhost/test', 'mongodb://localhost/test',
'--port',
'1339',
]); ]);
childProcess.stdout.on('data', data => { childProcess.stdout.on('data', data => {
data = data.toString(); data = data.toString();
@@ -247,6 +249,8 @@ describe('execution', () => {
'test', 'test',
'--databaseURI', '--databaseURI',
'mongodb://localhost/test', 'mongodb://localhost/test',
'--port',
'1340',
'--mountGraphQL', '--mountGraphQL',
]); ]);
let output = ''; let output = '';
@@ -271,6 +275,8 @@ describe('execution', () => {
'test', 'test',
'--databaseURI', '--databaseURI',
'mongodb://localhost/test', 'mongodb://localhost/test',
'--port',
'1341',
'--mountGraphQL', '--mountGraphQL',
'--mountPlayground', '--mountPlayground',
]); ]);

View File

@@ -80,6 +80,9 @@ describe('FilesController', () => {
expect(typeof error).toBe('object'); expect(typeof error).toBe('object');
expect(error.message.indexOf('biscuit')).toBe(13); expect(error.message.indexOf('biscuit')).toBe(13);
expect(error.code).toBe(Parse.Error.INVALID_FILE_NAME); expect(error.code).toBe(Parse.Error.INVALID_FILE_NAME);
mockAdapter.validateFilename = () => {
return null;
};
done(); done();
}); });

View File

@@ -4,7 +4,6 @@ const GridFSBucketAdapter = require('../lib/Adapters/Files/GridFSBucketAdapter')
const { randomString } = require('../lib/cryptoUtils'); const { randomString } = require('../lib/cryptoUtils');
const databaseURI = 'mongodb://localhost:27017/parse'; const databaseURI = 'mongodb://localhost:27017/parse';
const request = require('../lib/request'); const request = require('../lib/request');
const Config = require('../lib/Config');
async function expectMissingFile(gfsAdapter, name) { async function expectMissingFile(gfsAdapter, name) {
try { try {
@@ -395,8 +394,9 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => {
}); });
it('should handle getMetadata error', async () => { it('should handle getMetadata error', async () => {
const config = Config.get('test'); const gfsAdapter = new GridFSBucketAdapter(databaseURI);
config.filesController.getMetadata = () => Promise.reject(); await reconfigureServer({ filesAdapter: gfsAdapter });
gfsAdapter.getMetadata = () => Promise.reject();
const headers = { const headers = {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',

View File

@@ -1,5 +1,5 @@
const ldap = require('../lib/Adapters/Auth/ldap'); const ldap = require('../lib/Adapters/Auth/ldap');
const mockLdapServer = require('./MockLdapServer'); const mockLdapServer = require('./support/MockLdapServer');
const fs = require('fs'); const fs = require('fs');
const port = 12345; const port = 12345;
const sslport = 12346; const sslport = 12346;
@@ -19,39 +19,31 @@ describe('Ldap Auth', () => {
ldap.validateAppId().then(done).catch(done.fail); ldap.validateAppId().then(done).catch(done.fail);
}); });
it('Should succeed with right credentials', done => { it('Should succeed with right credentials', async done => {
mockLdapServer(port, 'uid=testuser, o=example').then(server => { const server = await mockLdapServer(port, 'uid=testuser, o=example');
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldap://localhost:${port}`, url: `ldap://localhost:${port}`,
dn: 'uid={{id}}, o=example', dn: 'uid={{id}}, o=example',
}; };
ldap await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
.validateAuthData({ id: 'testuser', password: 'secret' }, options) server.close(done);
.then(done)
.catch(done.fail)
.finally(() => server.close());
});
}); });
it('Should succeed with right credentials when LDAPS is used and certifcate is not checked', done => { it('Should succeed with right credentials when LDAPS is used and certifcate is not checked', async done => {
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true);
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldaps://localhost:${sslport}`, url: `ldaps://localhost:${sslport}`,
dn: 'uid={{id}}, o=example', dn: 'uid={{id}}, o=example',
tlsOptions: { rejectUnauthorized: false }, tlsOptions: { rejectUnauthorized: false },
}; };
ldap await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
.validateAuthData({ id: 'testuser', password: 'secret' }, options) server.close(done);
.then(done)
.catch(done.fail)
.finally(() => server.close());
});
}); });
it('Should succeed when LDAPS is used and the presented certificate is the expected certificate', done => { it('Should succeed when LDAPS is used and the presented certificate is the expected certificate', async done => {
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true);
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldaps://localhost:${sslport}`, url: `ldaps://localhost:${sslport}`,
@@ -61,16 +53,12 @@ describe('Ldap Auth', () => {
rejectUnauthorized: true, rejectUnauthorized: true,
}, },
}; };
ldap await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
.validateAuthData({ id: 'testuser', password: 'secret' }, options) server.close(done);
.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 => { it('Should fail when LDAPS is used and the presented certificate is not the expected certificate', async done => {
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true);
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldaps://localhost:${sslport}`, url: `ldaps://localhost:${sslport}`,
@@ -80,19 +68,17 @@ describe('Ldap Auth', () => {
rejectUnauthorized: true, rejectUnauthorized: true,
}, },
}; };
ldap try {
.validateAuthData({ id: 'testuser', password: 'secret' }, options) await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
.then(done.fail) fail();
.catch(err => { } catch (err) {
jequal(err.message, 'LDAPS: Certificate mismatch'); expect(err.message).toBe('LDAPS: Certificate mismatch');
done(); }
}) server.close(done);
.finally(() => server.close());
});
}); });
it('Should fail when LDAPS is used certifcate matches but credentials are wrong', done => { it('Should fail when LDAPS is used certifcate matches but credentials are wrong', async done => {
mockLdapServer(sslport, 'uid=testuser, o=example', false, true).then(server => { const server = await mockLdapServer(sslport, 'uid=testuser, o=example', false, true);
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldaps://localhost:${sslport}`, url: `ldaps://localhost:${sslport}`,
@@ -102,37 +88,33 @@ describe('Ldap Auth', () => {
rejectUnauthorized: true, rejectUnauthorized: true,
}, },
}; };
ldap try {
.validateAuthData({ id: 'testuser', password: 'wrong!' }, options) await ldap.validateAuthData({ id: 'testuser', password: 'wrong!' }, options);
.then(done.fail) fail();
.catch(err => { } catch (err) {
jequal(err.message, 'LDAP: Wrong username or password'); expect(err.message).toBe('LDAP: Wrong username or password');
done(); }
}) server.close(done);
.finally(() => server.close());
});
}); });
it('Should fail with wrong credentials', done => { it('Should fail with wrong credentials', async done => {
mockLdapServer(port, 'uid=testuser, o=example').then(server => { const server = await mockLdapServer(port, 'uid=testuser, o=example');
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldap://localhost:${port}`, url: `ldap://localhost:${port}`,
dn: 'uid={{id}}, o=example', dn: 'uid={{id}}, o=example',
}; };
ldap try {
.validateAuthData({ id: 'testuser', password: 'wrong!' }, options) await ldap.validateAuthData({ id: 'testuser', password: 'wrong!' }, options);
.then(done.fail) fail();
.catch(err => { } catch (err) {
jequal(err.message, 'LDAP: Wrong username or password'); expect(err.message).toBe('LDAP: Wrong username or password');
done(); }
}) server.close(done);
.finally(() => server.close());
});
}); });
it('Should succeed if user is in given group', done => { it('Should succeed if user is in given group', async done => {
mockLdapServer(port, 'uid=testuser, o=example').then(server => { const server = await mockLdapServer(port, 'uid=testuser, o=example');
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldap://localhost:${port}`, url: `ldap://localhost:${port}`,
@@ -140,17 +122,12 @@ describe('Ldap Auth', () => {
groupCn: 'powerusers', groupCn: 'powerusers',
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
}; };
await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
ldap server.close(done);
.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 => { it('Should fail if user is not in given group', async done => {
mockLdapServer(port, 'uid=testuser, o=example').then(server => { const server = await mockLdapServer(port, 'uid=testuser, o=example');
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldap://localhost:${port}`, url: `ldap://localhost:${port}`,
@@ -158,20 +135,17 @@ describe('Ldap Auth', () => {
groupCn: 'groupTheUserIsNotIn', groupCn: 'groupTheUserIsNotIn',
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
}; };
try {
ldap await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
.validateAuthData({ id: 'testuser', password: 'secret' }, options) fail();
.then(done.fail) } catch (err) {
.catch(err => { expect(err.message).toBe('LDAP: User not in group');
jequal(err.message, 'LDAP: User not in group'); }
done(); server.close(done);
})
.finally(() => server.close());
});
}); });
it('Should fail if the LDAP server does not allow searching inside the provided suffix', done => { it('Should fail if the LDAP server does not allow searching inside the provided suffix', async done => {
mockLdapServer(port, 'uid=testuser, o=example').then(server => { const server = await mockLdapServer(port, 'uid=testuser, o=example');
const options = { const options = {
suffix: 'o=invalid', suffix: 'o=invalid',
url: `ldap://localhost:${port}`, url: `ldap://localhost:${port}`,
@@ -179,20 +153,17 @@ describe('Ldap Auth', () => {
groupCn: 'powerusers', groupCn: 'powerusers',
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
}; };
try {
ldap await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
.validateAuthData({ id: 'testuser', password: 'secret' }, options) fail();
.then(done.fail) } catch (err) {
.catch(err => { expect(err.message).toBe('LDAP group search failed');
jequal(err.message, 'LDAP group search failed'); }
done(); server.close(done);
})
.finally(() => server.close());
});
}); });
it('Should fail if the LDAP server encounters an error while searching', done => { it('Should fail if the LDAP server encounters an error while searching', async done => {
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { const server = await mockLdapServer(port, 'uid=testuser, o=example', true);
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldap://localhost:${port}`, url: `ldap://localhost:${port}`,
@@ -200,62 +171,42 @@ describe('Ldap Auth', () => {
groupCn: 'powerusers', groupCn: 'powerusers',
groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))', groupFilter: '(&(uniqueMember=uid={{id}}, o=example)(objectClass=groupOfUniqueNames))',
}; };
try {
ldap await ldap.validateAuthData({ id: 'testuser', password: 'secret' }, options);
.validateAuthData({ id: 'testuser', password: 'secret' }, options) fail();
.then(done.fail) } catch (err) {
.catch(err => { expect(err.message).toBe('LDAP group search failed');
jequal(err.message, 'LDAP group search failed'); }
done(); server.close(done);
})
.finally(() => server.close());
});
}); });
it('Should delete the password from authData after validation', done => { it('Should delete the password from authData after validation', async done => {
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { const server = await mockLdapServer(port, 'uid=testuser, o=example', true);
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldap://localhost:${port}`, url: `ldap://localhost:${port}`,
dn: 'uid={{id}}, o=example', dn: 'uid={{id}}, o=example',
}; };
const authData = { id: 'testuser', password: 'secret' }; const authData = { id: 'testuser', password: 'secret' };
await ldap.validateAuthData(authData, options);
ldap
.validateAuthData(authData, options)
.then(() => {
expect(authData).toEqual({ id: 'testuser' }); expect(authData).toEqual({ id: 'testuser' });
done(); server.close(done);
})
.catch(done.fail)
.finally(() => server.close());
});
}); });
it('Should not save the password in the user record after authentication', done => { it('Should not save the password in the user record after authentication', async done => {
mockLdapServer(port, 'uid=testuser, o=example', true).then(server => { const server = await mockLdapServer(port, 'uid=testuser, o=example', true);
const options = { const options = {
suffix: 'o=example', suffix: 'o=example',
url: `ldap://localhost:${port}`, url: `ldap://localhost:${port}`,
dn: 'uid={{id}}, o=example', dn: 'uid={{id}}, o=example',
}; };
reconfigureServer({ auth: { ldap: options } }).then(() => { await reconfigureServer({ auth: { ldap: options } });
const authData = { authData: { id: 'testuser', password: 'secret' } }; const authData = { authData: { id: 'testuser', password: 'secret' } };
Parse.User.logInWith('ldap', authData).then(returnedUser => { const returnedUser = await Parse.User.logInWith('ldap', authData);
const query = new Parse.Query('User'); const query = new Parse.Query('User');
query const user = await query.equalTo('objectId', returnedUser.id).first({ useMasterKey: true });
.equalTo('objectId', returnedUser.id)
.first({ useMasterKey: true })
.then(user => {
expect(user.get('authData')).toEqual({ ldap: { id: 'testuser' } }); expect(user.get('authData')).toEqual({ ldap: { id: 'testuser' } });
expect(user.get('authData').ldap.password).toBeUndefined(); expect(user.get('authData').ldap.password).toBeUndefined();
done(); server.close(done);
})
.catch(done.fail)
.finally(() => server.close());
});
});
});
}); });
}); });

View File

@@ -70,6 +70,7 @@ describe('LoggerController', () => {
}; };
const loggerController = new LoggerController(new WinstonLoggerAdapter()); const loggerController = new LoggerController(new WinstonLoggerAdapter());
loggerController.error('can process an ascending query without throwing');
expect(() => { expect(() => {
loggerController loggerController
@@ -115,6 +116,7 @@ describe('LoggerController', () => {
}; };
const loggerController = new LoggerController(new WinstonLoggerAdapter()); const loggerController = new LoggerController(new WinstonLoggerAdapter());
loggerController.error('can process a descending query without throwing');
expect(() => { expect(() => {
loggerController loggerController

View File

@@ -2,10 +2,15 @@
const request = require('../lib/request'); const request = require('../lib/request');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const pushCompleted = async pushId => { 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')) { while (!(result && result.get('status') === 'succeeded')) {
result = await Parse.Push.getPushStatus(pushId); await sleep(100);
result = await query.first({ useMasterKey: true });
} }
}; };

View File

@@ -5,7 +5,7 @@ const fetch = require('node-fetch');
const FormData = require('form-data'); const FormData = require('form-data');
const ws = require('ws'); const ws = require('ws');
require('./helper'); require('./helper');
const { updateCLP } = require('./dev'); const { updateCLP } = require('./support/dev');
const pluralize = require('pluralize'); const pluralize = require('pluralize');
const { getMainDefinition } = require('apollo-utilities'); const { getMainDefinition } = require('apollo-utilities');
@@ -9033,7 +9033,7 @@ describe('ParseGraphQLServer', () => {
it('should support object values', async () => { it('should support object values', async () => {
try { try {
const someFieldValue = { const someObjectFieldValue = {
foo: { bar: 'baz' }, foo: { bar: 'baz' },
number: 10, number: 10,
}; };
@@ -9048,7 +9048,7 @@ describe('ParseGraphQLServer', () => {
`, `,
variables: { variables: {
schemaFields: { schemaFields: {
addObjects: [{ name: 'someField' }], addObjects: [{ name: 'someObjectField' }],
}, },
}, },
context: { context: {
@@ -9057,11 +9057,10 @@ describe('ParseGraphQLServer', () => {
}, },
}, },
}); });
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); 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({ const createResult = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -9075,13 +9074,13 @@ describe('ParseGraphQLServer', () => {
`, `,
variables: { variables: {
fields: { fields: {
someField: someFieldValue, someObjectField: someObjectFieldValue,
}, },
}, },
}); });
const where = { const where = {
someField: { someObjectField: {
equalTo: { key: 'foo.bar', value: 'baz' }, equalTo: { key: 'foo.bar', value: 'baz' },
notEqualTo: { key: 'foo.bar', value: 'bat' }, notEqualTo: { key: 'foo.bar', value: 'bat' },
greaterThan: { key: 'number', value: 9 }, greaterThan: { key: 'number', value: 9 },
@@ -9093,13 +9092,13 @@ describe('ParseGraphQLServer', () => {
query GetSomeObject($id: ID!, $where: SomeClassWhereInput) { query GetSomeObject($id: ID!, $where: SomeClassWhereInput) {
someClass(id: $id) { someClass(id: $id) {
id id
someField someObjectField
} }
someClasses(where: $where) { someClasses(where: $where) {
edges { edges {
node { node {
id id
someField someObjectField
} }
} }
} }
@@ -9113,13 +9112,13 @@ describe('ParseGraphQLServer', () => {
const { someClass: getResult, someClasses } = queryResult.data; const { someClass: getResult, someClasses } = queryResult.data;
const { someField } = getResult; const { someObjectField } = getResult;
expect(typeof someField).toEqual('object'); expect(typeof someObjectField).toEqual('object');
expect(someField).toEqual(someFieldValue); expect(someObjectField).toEqual(someObjectFieldValue);
// Checks class query results // Checks class query results
expect(someClasses.edges.length).toEqual(1); expect(someClasses.edges.length).toEqual(1);
expect(someClasses.edges[0].node.someField).toEqual(someFieldValue); expect(someClasses.edges[0].node.someObjectField).toEqual(someObjectFieldValue);
} catch (e) { } catch (e) {
handleError(e); handleError(e);
} }
@@ -9127,11 +9126,11 @@ describe('ParseGraphQLServer', () => {
it('should support object composed queries', async () => { it('should support object composed queries', async () => {
try { try {
const someFieldValue = { const someObjectFieldValue1 = {
lorem: 'ipsum', lorem: 'ipsum',
number: 10, number: 10,
}; };
const someFieldValue2 = { const someObjectFieldValue2 = {
foo: { foo: {
test: 'bar', test: 'bar',
}, },
@@ -9144,7 +9143,7 @@ describe('ParseGraphQLServer', () => {
createClass( createClass(
input: { input: {
name: "SomeClass" name: "SomeClass"
schemaFields: { addObjects: [{ name: "someField" }] } schemaFields: { addObjects: [{ name: "someObjectField" }] }
} }
) { ) {
clientMutationId clientMutationId
@@ -9180,10 +9179,10 @@ describe('ParseGraphQLServer', () => {
`, `,
variables: { variables: {
fields1: { fields1: {
someField: someFieldValue, someObjectField: someObjectFieldValue1,
}, },
fields2: { fields2: {
someField: someFieldValue2, someObjectField: someObjectFieldValue2,
}, },
}, },
}); });
@@ -9191,24 +9190,24 @@ describe('ParseGraphQLServer', () => {
const where = { const where = {
AND: [ AND: [
{ {
someField: { someObjectField: {
greaterThan: { key: 'number', value: 9 }, greaterThan: { key: 'number', value: 9 },
}, },
}, },
{ {
someField: { someObjectField: {
lessThan: { key: 'number', value: 11 }, lessThan: { key: 'number', value: 11 },
}, },
}, },
{ {
OR: [ OR: [
{ {
someField: { someObjectField: {
equalTo: { key: 'lorem', value: 'ipsum' }, equalTo: { key: 'lorem', value: 'ipsum' },
}, },
}, },
{ {
someField: { someObjectField: {
equalTo: { key: 'foo.test', value: 'bar' }, equalTo: { key: 'foo.test', value: 'bar' },
}, },
}, },
@@ -9223,7 +9222,7 @@ describe('ParseGraphQLServer', () => {
edges { edges {
node { node {
id id
someField someObjectField
} }
} }
} }
@@ -9241,11 +9240,11 @@ describe('ParseGraphQLServer', () => {
const { edges } = someClasses; const { edges } = someClasses;
expect(edges.length).toEqual(2); expect(edges.length).toEqual(2);
expect( expect(
edges.find(result => result.node.id === create1.someClass.id).node.someField edges.find(result => result.node.id === create1.someClass.id).node.someObjectField
).toEqual(someFieldValue); ).toEqual(someObjectFieldValue1);
expect( expect(
edges.find(result => result.node.id === create2.someClass.id).node.someField edges.find(result => result.node.id === create2.someClass.id).node.someObjectField
).toEqual(someFieldValue2); ).toEqual(someObjectFieldValue2);
} catch (e) { } catch (e) {
handleError(e); handleError(e);
} }
@@ -9253,7 +9252,7 @@ describe('ParseGraphQLServer', () => {
it('should support array values', async () => { it('should support array values', async () => {
try { try {
const someFieldValue = [1, 'foo', ['bar'], { lorem: 'ipsum' }, true]; const someArrayFieldValue = [1, 'foo', ['bar'], { lorem: 'ipsum' }, true];
await apolloClient.mutate({ await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -9265,7 +9264,7 @@ describe('ParseGraphQLServer', () => {
`, `,
variables: { variables: {
schemaFields: { schemaFields: {
addArrays: [{ name: 'someField' }], addArrays: [{ name: 'someArrayField' }],
}, },
}, },
context: { context: {
@@ -9278,7 +9277,7 @@ describe('ParseGraphQLServer', () => {
await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear();
const schema = await new Parse.Schema('SomeClass').get(); 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({ const createResult = await apolloClient.mutate({
mutation: gql` mutation: gql`
@@ -9292,7 +9291,7 @@ describe('ParseGraphQLServer', () => {
`, `,
variables: { variables: {
fields: { fields: {
someField: someFieldValue, someArrayField: someArrayFieldValue,
}, },
}, },
}); });
@@ -9301,17 +9300,17 @@ describe('ParseGraphQLServer', () => {
query: gql` query: gql`
query GetSomeObject($id: ID!) { query GetSomeObject($id: ID!) {
someClass(id: $id) { someClass(id: $id) {
someField { someArrayField {
... on Element { ... on Element {
value value
} }
} }
} }
someClasses(where: { someField: { exists: true } }) { someClasses(where: { someArrayField: { exists: true } }) {
edges { edges {
node { node {
id id
someField { someArrayField {
... on Element { ... on Element {
value value
} }
@@ -9326,9 +9325,9 @@ describe('ParseGraphQLServer', () => {
}, },
}); });
const { someField } = getResult.data.someClass; const { someArrayField } = getResult.data.someClass;
expect(Array.isArray(someField)).toBeTruthy(); expect(Array.isArray(someArrayField)).toBeTruthy();
expect(someField.map(element => element.value)).toEqual(someFieldValue); expect(someArrayField.map(element => element.value)).toEqual(someArrayFieldValue);
expect(getResult.data.someClasses.edges.length).toEqual(1); expect(getResult.data.someClasses.edges.length).toEqual(1);
} catch (e) { } catch (e) {
handleError(e); handleError(e);
@@ -10201,7 +10200,6 @@ describe('ParseGraphQLServer', () => {
let apolloClient; let apolloClient;
beforeEach(async () => { beforeEach(async () => {
if (!httpServer) {
const expressApp = express(); const expressApp = express();
httpServer = http.createServer(expressApp); httpServer = http.createServer(expressApp);
const TypeEnum = new GraphQLEnumType({ const TypeEnum = new GraphQLEnumType({
@@ -10292,10 +10290,9 @@ describe('ParseGraphQLServer', () => {
}, },
}, },
}); });
}
}); });
afterAll(async () => { afterEach(async () => {
await httpServer.close(); await httpServer.close();
}); });

View File

@@ -8,9 +8,8 @@ const bodyParser = require('body-parser');
const auth = require('../lib/Auth'); const auth = require('../lib/Auth');
const Config = require('../lib/Config'); const Config = require('../lib/Config');
const port = 12345; const port = 34567;
const hookServerURL = 'http://localhost:' + port; const hookServerURL = 'http://localhost:' + port;
const AppCache = require('../lib/cache').AppCache;
describe('Hooks', () => { describe('Hooks', () => {
let server; let server;
@@ -19,7 +18,7 @@ describe('Hooks', () => {
if (!app) { if (!app) {
app = express(); app = express();
app.use(bodyParser.json({ type: '*/*' })); app.use(bodyParser.json({ type: '*/*' }));
server = app.listen(12345, undefined, done); server = app.listen(port, undefined, done);
} else { } else {
done(); done();
} }
@@ -383,7 +382,7 @@ describe('Hooks', () => {
} }
const hooksController = new HooksController( const hooksController = new HooksController(
Parse.applicationId, Parse.applicationId,
AppCache.get('test').databaseController Config.get('test').database
); );
return hooksController.load(); return hooksController.load();
}, },

View File

@@ -127,7 +127,10 @@ describe('ParseLiveQueryServer', function () {
serverStartComplete: () => { serverStartComplete: () => {
expect(parseServer.liveQueryServer).not.toBeUndefined(); expect(parseServer.liveQueryServer).not.toBeUndefined();
expect(parseServer.liveQueryServer.server).toBe(parseServer.server); 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).not.toBeUndefined();
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server); expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
parseServer.liveQueryServer.server.close( parseServer.liveQueryServer.server.close(
parseServer.server.close.bind(parseServer.server, done) parseServer.server.close.bind(parseServer.server, async () => {
await reconfigureServer();
done();
})
); );
}, },
}); });

View File

@@ -169,25 +169,25 @@ describe('ParseServerRESTController', () => {
process.env.PARSE_SERVER_TEST_DB === 'postgres' process.env.PARSE_SERVER_TEST_DB === 'postgres'
) { ) {
describe('transactions', () => { describe('transactions', () => {
let parseServer;
beforeEach(async () => { beforeEach(async () => {
await TestUtils.destroyAllDataPermanently(true);
if ( if (
semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_TOPOLOGY === 'replicaset' &&
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger' process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
) { ) {
if (!parseServer) { await reconfigureServer({
parseServer = await reconfigureServer({
databaseAdapter: undefined, databaseAdapter: undefined,
databaseURI: databaseURI:
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
}); });
} } else {
await TestUtils.destroyAllDataPermanently(true); 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 const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
myObject myObject
.save() .save()
@@ -236,15 +236,12 @@ describe('ParseServerRESTController', () => {
.catch(done.fail); .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 const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
myObject await myObject.save();
.save() await myObject.destroy();
.then(() => { try {
return myObject.destroy(); await RESTController.request('POST', 'batch', {
})
.then(() => {
RESTController.request('POST', 'batch', {
requests: [ requests: [
{ {
method: 'POST', method: 'POST',
@@ -338,15 +335,14 @@ describe('ParseServerRESTController', () => {
}, },
], ],
transaction: true, transaction: true,
}).catch(error => { });
fail();
} catch (error) {
expect(error).toBeDefined(); expect(error).toBeDefined();
const query = new Parse.Query('MyObject'); const query = new Parse.Query('MyObject');
query.find().then(results => { const results = await query.find();
expect(results.length).toBe(0); expect(results.length).toBe(0);
done(); }
});
});
});
}); });
it('should generate separate session for each call', async () => { it('should generate separate session for each call', async () => {

View File

@@ -28,7 +28,7 @@ function createParseServer(options) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const parseServer = new ParseServer.default( const parseServer = new ParseServer.default(
Object.assign({}, defaultConfiguration, options, { Object.assign({}, defaultConfiguration, options, {
serverURL: 'http://localhost:12666/parse', serverURL: 'http://localhost:12668/parse',
serverStartComplete: error => { serverStartComplete: error => {
if (error) { if (error) {
reject(error); reject(error);
@@ -37,8 +37,8 @@ function createParseServer(options) {
const app = express(); const app = express();
app.use('/parse', parseServer.app); app.use('/parse', parseServer.app);
const server = app.listen(12666); const server = app.listen(12668);
Parse.serverURL = 'http://localhost:12666/parse'; Parse.serverURL = 'http://localhost:12668/parse';
resolve(server); resolve(server);
} }
}, },

View File

@@ -1,7 +1,7 @@
const Config = require('../lib/Config'); const Config = require('../lib/Config');
const Parse = require('parse/node'); const Parse = require('parse/node');
const request = require('../lib/request'); 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 () { describe('ProtectedFields', function () {
it('should handle and empty protectedFields', async function () { it('should handle and empty protectedFields', async function () {

View File

@@ -26,10 +26,15 @@ const successfulIOS = function (body, installations) {
return Promise.all(promises); return Promise.all(promises);
}; };
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const pushCompleted = async pushId => { 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')) { 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); await pushCompleted(pushStatusId);
}); });
it('should properly report failures in _PushStatus', done => { it('should properly report failures in _PushStatus', async () => {
const pushAdapter = { const pushAdapter = {
send: function (body, installations) { send: function (body, installations) {
return installations.map(installation => { return installations.map(installation => {
@@ -593,30 +598,27 @@ describe('PushController', () => {
badge: 1, badge: 1,
}, },
}; };
const config = Config.get(Parse.applicationId);
const auth = { const auth = {
isMaster: true, isMaster: true,
}; };
const pushController = new PushController(); const pushController = new PushController();
reconfigureServer({ await reconfigureServer({
push: { adapter: pushAdapter }, push: { adapter: pushAdapter },
}) });
.then(() => { const config = Config.get(Parse.applicationId);
return pushController.sendPush(payload, where, config, auth); try {
}) await pushController.sendPush(payload, where, config, auth);
.then(() => { fail();
fail('should not succeed'); } catch (e) {
done();
})
.catch(() => {
const query = new Parse.Query('_PushStatus'); const query = new Parse.Query('_PushStatus');
query.find({ useMasterKey: true }).then(results => { let results = await query.find({ useMasterKey: true });
while (results.length === 0) {
results = await query.find({ useMasterKey: true });
}
expect(results.length).toBe(1); expect(results.length).toBe(1);
const pushStatus = results[0]; const pushStatus = results[0];
expect(pushStatus.get('status')).toBe('failed'); expect(pushStatus.get('status')).toBe('failed');
done(); }
});
});
}); });
it('should support full RESTQuery for increment', async () => { it('should support full RESTQuery for increment', async () => {
@@ -1237,7 +1239,7 @@ describe('PushController', () => {
const auth = { isMaster: true }; const auth = { isMaster: true };
const pushController = new PushController(); const pushController = new PushController();
let config = Config.get(Parse.applicationId); let config;
const pushes = []; const pushes = [];
const pushAdapter = { const pushAdapter = {

View File

@@ -1,6 +1,11 @@
const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default; const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default;
const Config = require('../lib/Config'); 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 To run this test part of the complete suite
set PARSE_SERVER_TEST_CACHE='redis' set PARSE_SERVER_TEST_CACHE='redis'
@@ -11,31 +16,30 @@ describe_only(() => {
})('RedisCacheAdapter', function () { })('RedisCacheAdapter', function () {
const KEY = 'hello'; const KEY = 'hello';
const VALUE = 'world'; const VALUE = 'world';
let cache;
function wait(sleep) { beforeEach(async () => {
return new Promise(function (resolve) { cache = new RedisCacheAdapter(null, 100);
setTimeout(resolve, sleep); await cache.clear();
}); });
}
it('should get/set/clear', done => { it('should get/set/clear', done => {
const cache = new RedisCacheAdapter({ const cacheNaN = new RedisCacheAdapter({
ttl: NaN, ttl: NaN,
}); });
cache cacheNaN
.put(KEY, VALUE) .put(KEY, VALUE)
.then(() => cache.get(KEY)) .then(() => cacheNaN.get(KEY))
.then(value => expect(value).toEqual(VALUE)) .then(value => expect(value).toEqual(VALUE))
.then(() => cache.clear()) .then(() => cacheNaN.clear())
.then(() => cache.get(KEY)) .then(() => cacheNaN.get(KEY))
.then(value => expect(value).toEqual(null)) .then(value => expect(value).toEqual(null))
.then(() => cacheNaN.clear())
.then(done); .then(done);
}); });
it('should expire after ttl', done => { it('should expire after ttl', done => {
const cache = new RedisCacheAdapter(null, 100);
cache cache
.put(KEY, VALUE) .put(KEY, VALUE)
.then(() => cache.get(KEY)) .then(() => cache.get(KEY))
@@ -47,8 +51,6 @@ describe_only(() => {
}); });
it('should not store value for ttl=0', done => { it('should not store value for ttl=0', done => {
const cache = new RedisCacheAdapter(null, 100);
cache cache
.put(KEY, VALUE, 0) .put(KEY, VALUE, 0)
.then(() => cache.get(KEY)) .then(() => cache.get(KEY))
@@ -57,8 +59,6 @@ describe_only(() => {
}); });
it('should not expire when ttl=Infinity', done => { it('should not expire when ttl=Infinity', done => {
const cache = new RedisCacheAdapter(null, 100);
cache cache
.put(KEY, VALUE, Infinity) .put(KEY, VALUE, Infinity)
.then(() => cache.get(KEY)) .then(() => cache.get(KEY))
@@ -70,7 +70,6 @@ describe_only(() => {
}); });
it('should fallback to default ttl', done => { it('should fallback to default ttl', done => {
const cache = new RedisCacheAdapter(null, 100);
let promise = Promise.resolve(); let promise = Promise.resolve();
[-100, null, undefined, 'not number', true].forEach(ttl => { [-100, null, undefined, 'not number', true].forEach(ttl => {
@@ -89,8 +88,6 @@ describe_only(() => {
}); });
it('should find un-expired records', done => { it('should find un-expired records', done => {
const cache = new RedisCacheAdapter(null, 100);
cache cache
.put(KEY, VALUE) .put(KEY, VALUE)
.then(() => cache.get(KEY)) .then(() => cache.get(KEY))
@@ -102,8 +99,6 @@ describe_only(() => {
}); });
it('handleShutdown, close connection', async () => { it('handleShutdown, close connection', async () => {
const cache = new RedisCacheAdapter(null, 100);
await cache.handleShutdown(); await cache.handleShutdown();
setTimeout(() => { setTimeout(() => {
expect(cache.client.connected).toBe(false); expect(cache.client.connected).toBe(false);

View File

@@ -1,6 +1,5 @@
const UserController = require('../lib/Controllers/UserController').UserController; const UserController = require('../lib/Controllers/UserController').UserController;
const emailAdapter = require('./MockEmailAdapter'); const emailAdapter = require('./support/MockEmailAdapter');
const AppCache = require('../lib/cache').AppCache;
describe('UserController', () => { describe('UserController', () => {
const user = { const user = {
@@ -11,55 +10,45 @@ describe('UserController', () => {
describe('sendVerificationEmail', () => { describe('sendVerificationEmail', () => {
describe('parseFrameURL not provided', () => { describe('parseFrameURL not provided', () => {
it('uses publicServerURL', done => { it('uses publicServerURL', async done => {
AppCache.put( await reconfigureServer({
defaultConfiguration.appId,
Object.assign({}, defaultConfiguration, {
publicServerURL: 'http://www.example.com', publicServerURL: 'http://www.example.com',
customPages: { customPages: {
parseFrameURL: undefined, parseFrameURL: undefined,
}, },
}) });
);
emailAdapter.sendVerificationEmail = options => { emailAdapter.sendVerificationEmail = options => {
expect(options.link).toEqual( expect(options.link).toEqual(
'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser' 'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser'
); );
emailAdapter.sendVerificationEmail = () => Promise.resolve();
done(); done();
}; };
const userController = new UserController(emailAdapter, 'test', { const userController = new UserController(emailAdapter, 'test', {
verifyUserEmails: true, verifyUserEmails: true,
}); });
userController.sendVerificationEmail(user); userController.sendVerificationEmail(user);
}); });
}); });
describe('parseFrameURL provided', () => { describe('parseFrameURL provided', () => {
it('uses parseFrameURL and includes the destination in the link parameter', done => { it('uses parseFrameURL and includes the destination in the link parameter', async done => {
AppCache.put( await reconfigureServer({
defaultConfiguration.appId,
Object.assign({}, defaultConfiguration, {
publicServerURL: 'http://www.example.com', publicServerURL: 'http://www.example.com',
customPages: { customPages: {
parseFrameURL: 'http://someother.example.com/handle-parse-iframe', parseFrameURL: 'http://someother.example.com/handle-parse-iframe',
}, },
}) });
);
emailAdapter.sendVerificationEmail = options => { emailAdapter.sendVerificationEmail = options => {
expect(options.link).toEqual( expect(options.link).toEqual(
'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser' 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser'
); );
emailAdapter.sendVerificationEmail = () => Promise.resolve();
done(); done();
}; };
const userController = new UserController(emailAdapter, 'test', { const userController = new UserController(emailAdapter, 'test', {
verifyUserEmails: true, verifyUserEmails: true,
}); });
userController.sendVerificationEmail(user); userController.sendVerificationEmail(user);
}); });
}); });

View File

@@ -1,6 +1,6 @@
'use strict'; 'use strict';
const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions');
const request = require('../lib/request'); const request = require('../lib/request');
const Config = require('../lib/Config'); const Config = require('../lib/Config');

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
const request = require('../lib/request'); const request = require('../lib/request');
const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions');
const verifyPassword = function (login, password, isEmail = false) { const verifyPassword = function (login, password, isEmail = false) {
const body = !isEmail ? { username: login, password } : { email: login, password }; const body = !isEmail ? { username: login, password } : { email: login, password };

View File

@@ -175,6 +175,7 @@ describe('batch', () => {
) { ) {
describe('transactions', () => { describe('transactions', () => {
beforeEach(async () => { beforeEach(async () => {
await TestUtils.destroyAllDataPermanently(true);
if ( if (
semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_TOPOLOGY === 'replicaset' &&
@@ -185,7 +186,8 @@ describe('batch', () => {
databaseURI: databaseURI:
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset', 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
}); });
await TestUtils.destroyAllDataPermanently(true); } else {
await reconfigureServer();
} }
}); });
@@ -243,15 +245,12 @@ 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 const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
myObject await myObject.save();
.save() await myObject.destroy();
.then(() => { try {
return myObject.destroy(); await request({
})
.then(() => {
request({
method: 'POST', method: 'POST',
headers: headers, headers: headers,
url: 'http://localhost:8378/1/batch', url: 'http://localhost:8378/1/batch',
@@ -350,15 +349,13 @@ describe('batch', () => {
], ],
transaction: true, transaction: true,
}), }),
}).catch(error => { });
expect(error.data).toBeDefined(); } catch (error) {
expect(error).toBeDefined();
const query = new Parse.Query('MyObject'); const query = new Parse.Query('MyObject');
query.find().then(results => { const results = await query.find();
expect(results.length).toBe(0); expect(results.length).toBe(0);
done(); }
});
});
});
}); });
it('should generate separate session for each call', async () => { it('should generate separate session for each call', async () => {

View File

@@ -1,14 +1,12 @@
'use strict'; 'use strict';
const semver = require('semver'); const semver = require('semver');
const CurrentSpecReporter = require('./support/CurrentSpecReporter.js'); const CurrentSpecReporter = require('./support/CurrentSpecReporter.js');
const { SpecReporter } = require('jasmine-spec-reporter');
// Sets up a Parse API server for testing. // Sets up a Parse API server for testing.
jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000; jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 10000;
jasmine.getEnv().addReporter(new CurrentSpecReporter()); 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) => { global.on_db = (db, callback, elseCallback) => {
if (process.env.PARSE_SERVER_TEST_DB == db) { if (process.env.PARSE_SERVER_TEST_DB == db) {
@@ -28,6 +26,7 @@ if (global._babelPolyfill) {
process.noDeprecation = true; process.noDeprecation = true;
const cache = require('../lib/cache').default; const cache = require('../lib/cache').default;
const defaults = require('../lib/defaults').default;
const ParseServer = require('../lib/index').ParseServer; const ParseServer = require('../lib/index').ParseServer;
const path = require('path'); const path = require('path');
const TestUtils = require('../lib/TestUtils'); const TestUtils = require('../lib/TestUtils');
@@ -113,7 +112,7 @@ const defaultConfiguration = {
custom: mockCustom(), custom: mockCustom(),
facebook: mockFacebook(), facebook: mockFacebook(),
myoauth: { 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(), shortLivedAuth: mockShortLivedAuth(),
}, },
@@ -124,6 +123,16 @@ if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') {
} }
const openConnections = {}; 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. // Set up a default API server for testing with default configuration.
let server; let server;
@@ -146,7 +155,6 @@ const reconfigureServer = (changedConfiguration = {}) => {
if (error) { if (error) {
reject(error); reject(error);
} else { } else {
Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1);
resolve(parseServer); resolve(parseServer);
} }
}, },
@@ -194,8 +202,9 @@ beforeAll(async () => {
afterEach(function (done) { afterEach(function (done) {
const afterLogOut = async () => { const afterLogOut = async () => {
if (Object.keys(openConnections).length > 0) { 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); await TestUtils.destroyAllDataPermanently(true);
if (didChangeConfiguration) { if (didChangeConfiguration) {
await reconfigureServer(); await reconfigureServer();
@@ -205,6 +214,7 @@ afterEach(function (done) {
done(); done();
}; };
Parse.Cloud._removeAllHooks(); Parse.Cloud._removeAllHooks();
defaults.protectedFields = { _User: { '*': ['email'] } };
databaseAdapter databaseAdapter
.getAllClasses() .getAllClasses()
.then(allSchemas => { .then(allSchemas => {

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
const request = require('../lib/request'); const request = require('../lib/request');
const parseServerPackage = require('../package.json'); const parseServerPackage = require('../package.json');
const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions');
const ParseServer = require('../lib/index'); const ParseServer = require('../lib/index');
const Config = require('../lib/Config'); const Config = require('../lib/Config');
const express = require('express'); const express = require('express');
@@ -317,10 +317,16 @@ describe('server', () => {
}) })
.then(obj => { .then(obj => {
expect(obj.id).toEqual(objId); expect(obj.id).toEqual(objId);
server.close(done); server.close(async () => {
await reconfigureServer();
done();
});
}) })
.catch(() => { .catch(() => {
server.close(done); server.close(async () => {
await reconfigureServer();
done();
});
}); });
}, },
}) })
@@ -354,12 +360,18 @@ describe('server', () => {
}) })
.then(obj => { .then(obj => {
expect(obj.id).toEqual(objId); expect(obj.id).toEqual(objId);
server.close(done); server.close(async () => {
await reconfigureServer();
done();
});
}) })
.catch(error => { .catch(error => {
fail(JSON.stringify(error)); fail(JSON.stringify(error));
if (server) { if (server) {
server.close(done); server.close(async () => {
await reconfigureServer();
done();
});
} else { } else {
done(); done();
} }

View File

@@ -1274,6 +1274,7 @@ describe('schemas', () => {
}, },
}, },
}).then(response => { }).then(response => {
delete response.data.indexes;
expect( expect(
dd(response.data, { dd(response.data, {
className: '_User', className: '_User',
@@ -1302,6 +1303,7 @@ describe('schemas', () => {
headers: masterKeyHeaders, headers: masterKeyHeaders,
json: true, json: true,
}).then(response => { }).then(response => {
delete response.data.indexes;
expect( expect(
dd(response.data, { dd(response.data, {
className: '_User', className: '_User',

View File

@@ -2,8 +2,8 @@ const ldapjs = require('ldapjs');
const fs = require('fs'); const fs = require('fs');
const tlsOptions = { const tlsOptions = {
key: fs.readFileSync(__dirname + '/support/cert/key.pem'), key: fs.readFileSync(__dirname + '/cert/key.pem'),
certificate: fs.readFileSync(__dirname + '/support/cert/cert.pem'), certificate: fs.readFileSync(__dirname + '/cert/cert.pem'),
}; };
function newServer(port, dn, provokeSearchError = false, ssl = false) { function newServer(port, dn, provokeSearchError = false, ssl = false) {

View File

@@ -1,4 +1,4 @@
const Config = require('../lib/Config'); const Config = require('../../lib/Config');
const Parse = require('parse/node'); const Parse = require('parse/node');
const className = 'AnObject'; const className = 'AnObject';

View File

@@ -2,5 +2,5 @@
"spec_dir": "spec", "spec_dir": "spec",
"spec_files": ["*spec.js"], "spec_files": ["*spec.js"],
"helpers": ["helper.js"], "helpers": ["helper.js"],
"random": false "random": true
} }

View File

@@ -95,6 +95,8 @@ function searchForGroup(client, options, id, resolve, reject) {
} }
}); });
res.on('error', () => { res.on('error', () => {
client.unbind();
client.destroy();
return reject(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'LDAP group search failed')); return reject(new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'LDAP group search failed'));
}); });
}); });

View File

@@ -5,8 +5,9 @@ import { KeyPromiseQueue } from './KeyPromiseQueue';
const DEFAULT_REDIS_TTL = 30 * 1000; // 30 seconds in milliseconds const DEFAULT_REDIS_TTL = 30 * 1000; // 30 seconds in milliseconds
const FLUSH_DB_KEY = '__flush_db__'; const FLUSH_DB_KEY = '__flush_db__';
function debug() { function debug(...args: any) {
logger.debug.apply(logger, ['RedisCacheAdapter', ...arguments]); const message = ['RedisCacheAdapter: ' + arguments[0]].concat(args.slice(1, args.length));
logger.debug.apply(logger, message);
} }
const isValidTTL = ttl => typeof ttl === 'number' && ttl > 0; const isValidTTL = ttl => typeof ttl === 'number' && ttl > 0;
@@ -33,13 +34,13 @@ export class RedisCacheAdapter {
} }
get(key) { get(key) {
debug('get', key); debug('get', { key });
return this.queue.enqueue( return this.queue.enqueue(
key, key,
() => () =>
new Promise(resolve => { new Promise(resolve => {
this.client.get(key, function (err, res) { this.client.get(key, function (err, res) {
debug('-> get', key, res); debug('-> get', { key, res });
if (!res) { if (!res) {
return resolve(null); return resolve(null);
} }
@@ -51,7 +52,7 @@ export class RedisCacheAdapter {
put(key, value, ttl = this.ttl) { put(key, value, ttl = this.ttl) {
value = JSON.stringify(value); value = JSON.stringify(value);
debug('put', key, value, ttl); debug('put', { key, value, ttl });
if (ttl === 0) { if (ttl === 0) {
// ttl of zero is a logical no-op, but redis cannot set expire time of zero // 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) { del(key) {
debug('del', key); debug('del', { key });
return this.queue.enqueue( return this.queue.enqueue(
key, key,
() => () =>

View File

@@ -1215,16 +1215,10 @@ export default class SchemaController {
const promises = []; const promises = [];
for (const fieldName in object) { for (const fieldName in object) {
if (object[fieldName] === undefined) { if (object[fieldName] && getType(object[fieldName]) === 'GeoPoint') {
continue;
}
const expected = getType(object[fieldName]);
if (expected === 'GeoPoint') {
geocount++; geocount++;
} }
if (geocount > 1) { 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( return Promise.reject(
new Parse.Error( new Parse.Error(
Parse.Error.INCORRECT_TYPE, 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) { if (!expected) {
continue; continue;
} }