From 49df78fb83facf5ccef19219625d0591163196bd Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Fri, 27 May 2016 15:40:14 -0700 Subject: [PATCH 01/32] Add unique indexing --- spec/ParseAPI.spec.js | 21 ---- spec/Uniqueness.spec.js | 105 ++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 27 +++++ 3 files changed, 132 insertions(+), 21 deletions(-) create mode 100644 spec/Uniqueness.spec.js diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index cd6b40e7ce..9ae9118803 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1119,27 +1119,6 @@ describe('miscellaneous', function() { }); }); - it('fail when create duplicate value in unique field', (done) => { - let obj = new Parse.Object('UniqueField'); - obj.set('unique', 'value'); - obj.save().then(() => { - expect(obj.id).not.toBeUndefined(); - let config = new Config('test'); - return config.database.adapter.adaptiveCollection('UniqueField') - }).then(collection => { - return collection._mongoCollection.createIndex({ 'unique': 1 }, { unique: true }) - }).then(() => { - let obj = new Parse.Object('UniqueField'); - obj.set('unique', 'value'); - return obj.save() - }).then(() => { - return Promise.reject(); - }, error => { - expect(error.code === Parse.Error.DUPLICATE_VALUE); - done(); - }); - }); - it('doesnt convert interior keys of objects that use special names', done => { let obj = new Parse.Object('Obj'); obj.set('val', { createdAt: 'a', updatedAt: 1 }); diff --git a/spec/Uniqueness.spec.js b/spec/Uniqueness.spec.js new file mode 100644 index 0000000000..55bd95310a --- /dev/null +++ b/spec/Uniqueness.spec.js @@ -0,0 +1,105 @@ +'use strict'; + +var DatabaseAdapter = require('../src/DatabaseAdapter'); +var request = require('request'); +const Parse = require("parse/node"); +let Config = require('../src/Config'); + +describe('Uniqueness', function() { + it('fail when create duplicate value in unique field', done => { + let obj = new Parse.Object('UniqueField'); + obj.set('unique', 'value'); + obj.save().then(() => { + expect(obj.id).not.toBeUndefined(); + let config = new Config('test'); + return config.database.adapter.ensureUniqueness('UniqueField', ['unique'], { fields: { unique: { type: 'String' } } }) + }) + .then(() => { + let obj = new Parse.Object('UniqueField'); + obj.set('unique', 'value'); + return obj.save() + }).then(() => { + fail('Saving duplicate field should have failed'); + done(); + }, error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + }); + }); + + it('unique indexing works on pointer fields', done => { + let obj = new Parse.Object('UniquePointer'); + obj.save({ string: 'who cares' }) + .then(() => obj.save({ ptr: obj })) + .then(() => { + let config = new Config('test'); + return config.database.adapter.ensureUniqueness('UniquePointer', ['ptr'], { fields: { unique: { type: 'String' } } }) + }) + .then(() => { + let newObj = new Parse.Object('UniquePointer') + newObj.set('ptr', obj) + return newObj.save() + }) + .then(() => { + fail('save should have failed due to duplicate value'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + }); + }); + + it('fails when attempting to ensure uniqueness of fields that are not currently unique', done => { + let o1 = new Parse.Object('UniqueFail'); + o1.set('key', 'val'); + let o2 = new Parse.Object('UniqueFail'); + o2.set('key', 'val'); + Parse.Object.saveAll([o1, o2]) + .then(() => { + let config = new Config('test'); + return config.database.adapter.ensureUniqueness('UniqueFail', ['key'], { fields: { key: { type: 'String' } } }); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + }); + }); + + notWorking('doesnt let you enforce uniqueness on nullable fields', done => { + // Coming later, once we have a way to specify that a field is non-nullable + }); + + it('can do compound uniqueness', done => { + let config = new Config('test'); + config.database.adapter.ensureUniqueness('CompoundUnique', ['k1', 'k2'], { fields: { k1: { type: 'String' }, k2: { type: 'String' } } }) + .then(() => { + let o1 = new Parse.Object('CompoundUnique'); + o1.set('k1', 'v1'); + o1.set('k2', 'v2'); + return o1.save(); + }) + .then(() => { + let o2 = new Parse.Object('CompoundUnique'); + o2.set('k1', 'v1'); + o2.set('k2', 'not a dupe'); + return o2.save(); + }) + .then(() => { + let o3 = new Parse.Object('CompoundUnique'); + o3.set('k1', 'not a dupe'); + o3.set('k2', 'v2'); + return o3.save(); + }) + .then(() => { + let o4 = new Parse.Object('CompoundUnique'); + o4.set('k1', 'v1'); + o4.set('k2', 'v2'); + return o4.save(); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + }); + }); +}); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 8c95c33879..911e0521e4 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -236,6 +236,33 @@ export class MongoStorageAdapter { .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))); } + // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't + // currently know which fields are nullable and which aren't, we ignore that criteria. + // As such, we shouldn't expose this function to users of parse until we have an out-of-band + // Way of determining if a field is nullable. + ensureUniqueness(className, fieldNames, schema) { + let indexCreationRequest = {}; + fieldNames.map(fieldName => transformKey(className, fieldName, schema)).forEach(transformedName => { + indexCreationRequest[transformedName] = 1; + }); + return this.adaptiveCollection(className) + .then(collection => { + return new Promise((resolve, reject) => { + collection._mongoCollection.ensureIndex(indexCreationRequest, { unique: true, background: true }, (err, indexName) => { + if (err) { + if (err.code === 11000) { + reject(new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Tried to force field uniqueness for a class that already has duplicates.')); + } else { + reject(err); + } + } else { + resolve(); + } + }); + }) + }); + } + // Used in tests _rawFind(className, query) { return this.adaptiveCollection(className).then(collection => collection.find(query)); From 4c84650a42e35fed51b8e13a7c56bc8551d6f2cb Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Fri, 27 May 2016 19:41:09 -0700 Subject: [PATCH 02/32] Add unique indexing for username/email --- spec/ParseAPI.spec.js | 113 ++++++++++++++++++++++++++++++++++++++-- spec/Uniqueness.spec.js | 12 +++-- src/RestWrite.js | 108 +++++++++++++++++++++++++++----------- 3 files changed, 194 insertions(+), 39 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 9ae9118803..e79dc3391e 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -6,6 +6,10 @@ var DatabaseAdapter = require('../src/DatabaseAdapter'); var request = require('request'); const Parse = require("parse/node"); let Config = require('../src/Config'); +let defaultColumns = require('../src/Controllers/SchemaController').defaultColumns; + +const requiredUserFields = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._User) }; + describe('miscellaneous', function() { it('create a GameScore object', function(done) { @@ -45,17 +49,116 @@ describe('miscellaneous', function() { }); }); - it('fail to create a duplicate username', function(done) { - createTestUser(function(data) { - createTestUser(function(data) { - fail('Should not have been able to save duplicate username.'); - }, function(error) { + fit('fail to create a duplicate username', done => { + let numCreated = 0; + let numFailed = 0; + let p1 = createTestUser(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + let p2 = createTestUser(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); + }); + + fit('ensure that if people already have duplicate users, they can still sign up new users', done => { + let config = new Config('test'); + config.database.adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) + .then(() => config.database.adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('u'); + user.signUp() + .catch(error => { expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); done(); }); + }) + .catch(() => { + fail('save should have succeeded'); + done(); }); }); + fit('ensure that email is uniquely indexed', done => { + let numCreated = 0; + let numFailed = 0; + + let user1 = new Parse.User(); + user1.setPassword('asdf'); + user1.setUsername('u1'); + user1.setEmail('dupe@dupe.dupe'); + let p1 = user1.signUp(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); + + let user2 = new Parse.User(); + user2.setPassword('asdf'); + user2.setUsername('u2'); + user2.setEmail('dupe@dupe.dupe'); + let p2 = user2.signUp(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); + }); + + it('ensure that if people already have duplicate emails, they can still sign up new users', done => { + + }); + + it('ensure you get the right error if you have 2 users with the same email', done => { + + }); + + it('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => { + + }); + it('succeed in logging in', function(done) { createTestUser(function(u) { expect(typeof u.id).toEqual('string'); diff --git a/spec/Uniqueness.spec.js b/spec/Uniqueness.spec.js index 55bd95310a..9251ac6910 100644 --- a/spec/Uniqueness.spec.js +++ b/spec/Uniqueness.spec.js @@ -66,10 +66,6 @@ describe('Uniqueness', function() { }); }); - notWorking('doesnt let you enforce uniqueness on nullable fields', done => { - // Coming later, once we have a way to specify that a field is non-nullable - }); - it('can do compound uniqueness', done => { let config = new Config('test'); config.database.adapter.ensureUniqueness('CompoundUnique', ['k1', 'k2'], { fields: { k1: { type: 'String' }, k2: { type: 'String' } } }) @@ -102,4 +98,12 @@ describe('Uniqueness', function() { done(); }); }); + + it('adding a unique index to an existing field works even if it has nulls', done => { + + }); + + it('adding a unique index to an existing field doesnt prevent you from adding new documents with nulls', done => { + + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index fc40a71031..5b9676447a 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -14,6 +14,8 @@ var triggers = require('./triggers'); import RestQuery from './RestQuery'; import _ from 'lodash'; +const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } }; + // query and data are both provided in REST API format. So data // types are encoded by plain old objects. // If query is null, this is a "create" and the data in data should be @@ -356,44 +358,60 @@ RestWrite.prototype.transformUser = function() { } return; } - return this.config.database.find( - this.className, { - username: this.data.username, - objectId: {'$ne': this.objectId()} - }, {limit: 1}).then((results) => { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.USERNAME_TAKEN, - 'Account already exists for this username'); - } - return Promise.resolve(); - }); + // TODO: refactor this so that ensureUniqueness isn't called for every single sign up. + return this.config.database.adapter.ensureUniqueness('_User', ['username'], requiredUserFields) + .catch(error => { + if (error.code === Parse.Error.DUPLICATE_VALUE) { + // If they already have duplicate usernames or emails, the ensureUniqueness will fail, + // and then nobody will be able to sign up D: so just use the old, janky + // method to check for duplicate usernames. + return this.config.database.find( + this.className, + { username: this.data.username, objectId: {'$ne': this.objectId()} }, + { limit: 1 } + ) + .then(results => { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); + } + return; + }); + } + throw error; + }) }).then(() => { if (!this.data.email || this.data.email.__op === 'Delete') { return; } // Validate basic email address format if (!this.data.email.match(/^.+@.+$/)) { - throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, - 'Email address format is invalid.'); + throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.'); } // Check for email uniqueness - return this.config.database.find( - this.className, { - email: this.data.email, - objectId: {'$ne': this.objectId()} - }, {limit: 1}).then((results) => { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.EMAIL_TAKEN, - 'Account already exists for this email ' + - 'address'); - } - return Promise.resolve(); - }).then(() => { - // We updated the email, send a new validation - this.storage['sendVerificationEmail'] = true; - this.config.userController.setEmailVerifyToken(this.data); - return Promise.resolve(); - }) + return this.config.database.adapter.ensureUniqueness('_User', ['email'], requiredUserFields) + .catch(error => { + // Same problem for email as above for username + if (error.code === Parse.Error.DUPLICATE_VALUE) { + return this.config.database.find( + this.className, + { email: this.data.email, objectId: {'$ne': this.objectId()} }, + { limit: 1 } + ) + .then(results => { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); + } + return; + }); + } + throw error; + }) + .then(() => { + // We updated the email, send a new validation + this.storage['sendVerificationEmail'] = true; + this.config.userController.setEmailVerifyToken(this.data); + return; + }) }); }; @@ -762,6 +780,36 @@ RestWrite.prototype.runDatabaseOperation = function() { // Run a create return this.config.database.create(this.className, this.data, this.runOptions) + .catch(error => { + if (this.className !== '_User' || error.code !== Parse.Error.DUPLICATE_VALUE) { + throw error; + } + // If this was a failed user creation due to username or email already taken, we need to + // check whether it was username or email and return the appropriate error. + + // TODO: See if we can later do this without additional queries by using named indexes. + return this.config.database.find( + this.className, + { username: this.data.username, objectId: {'$ne': this.objectId()} }, + { limit: 1 } + ) + .then(results => { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); + } + return this.config.database.find( + this.className, + { email: this.data.email, objectId: {'$ne': this.objectId()} }, + { limit: 1 } + ); + }) + .then(results => { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); + } + throw error; + }); + }) .then(response => { response.objectId = this.data.objectId; response.createdAt = this.data.createdAt; From 6584e62d2768957d2d2f6e01e73a66fee9b44ab6 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 31 May 2016 13:14:57 -0700 Subject: [PATCH 03/32] WIP --- spec/ParseAPI.spec.js | 62 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index e79dc3391e..2392df95b0 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -49,7 +49,7 @@ describe('miscellaneous', function() { }); }); - fit('fail to create a duplicate username', done => { + it('fail to create a duplicate username', done => { let numCreated = 0; let numFailed = 0; let p1 = createTestUser(); @@ -80,7 +80,7 @@ describe('miscellaneous', function() { .catch(done); }); - fit('ensure that if people already have duplicate users, they can still sign up new users', done => { + it('ensure that if people already have duplicate users, they can still sign up new users', done => { let config = new Config('test'); config.database.adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) .then(() => config.database.adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) @@ -106,7 +106,7 @@ describe('miscellaneous', function() { }); }); - fit('ensure that email is uniquely indexed', done => { + it('ensure that email is uniquely indexed', done => { let numCreated = 0; let numFailed = 0; @@ -148,15 +148,57 @@ describe('miscellaneous', function() { }); it('ensure that if people already have duplicate emails, they can still sign up new users', done => { - - }); - - it('ensure you get the right error if you have 2 users with the same email', done => { - + let config = new Config('test'); + config.database.adapter.createObject('_User', { objectId: 'x', email: 'a@b.c' }, requiredUserFields) + .then(() => config.database.adapter.createObject('_User', { objectId: 'y', email: 'a@b.c' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('qqq'); + user.setEmail('unique@unique.unique'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('www'); + user.setEmail('a@b.c'); + user.signUp() + .catch(error => { + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + done(); + }); + }) + .catch(error => { + console.log(error); + fail('save should have succeeded'); + done(); + }); }); - it('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => { - + fit('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => { + let config = new Config('test'); + config.database.adapter.ensureUniqueness('_User', ['randomField'], requiredUserFields) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('1'); + user.setEmail('1@b.c'); + user.set('randomField', 'a'); + return user.signUp() + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('2'); + user.setEmail('2@b.c'); + user.set('randomField', 'a'); + return user.signUp() + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + }); }); it('succeed in logging in', function(done) { From aa1c51b2fcc719d8eab77a0112f7fa67ea2eae8a Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 1 Jun 2016 15:20:30 -0700 Subject: [PATCH 04/32] Finish unique indexes --- spec/ParseAPI.spec.js | 61 +++++++++---------- spec/Uniqueness.spec.js | 19 +++--- src/Adapters/Storage/Mongo/MongoCollection.js | 11 ++++ .../Storage/Mongo/MongoSchemaCollection.js | 14 ++--- .../Storage/Mongo/MongoStorageAdapter.js | 35 +++++------ src/Controllers/DatabaseController.js | 3 +- src/Controllers/UserController.js | 8 +-- src/RestWrite.js | 12 ++-- src/rest.js | 6 +- 9 files changed, 84 insertions(+), 85 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 2392df95b0..32adc81c0a 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -80,32 +80,6 @@ describe('miscellaneous', function() { .catch(done); }); - it('ensure that if people already have duplicate users, they can still sign up new users', done => { - let config = new Config('test'); - config.database.adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) - .then(() => config.database.adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('zxcv'); - return user.signUp(); - }) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('u'); - user.signUp() - .catch(error => { - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - done(); - }); - }) - .catch(() => { - fail('save should have succeeded'); - done(); - }); - }); - it('ensure that email is uniquely indexed', done => { let numCreated = 0; let numFailed = 0; @@ -118,8 +92,7 @@ describe('miscellaneous', function() { p1.then(user => { numCreated++; expect(numCreated).toEqual(1); - }) - .catch(error => { + }, error => { numFailed++; expect(numFailed).toEqual(1); expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); @@ -133,12 +106,12 @@ describe('miscellaneous', function() { p2.then(user => { numCreated++; expect(numCreated).toEqual(1); - }) - .catch(error => { + }, error => { numFailed++; expect(numFailed).toEqual(1); expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); }); + Parse.Promise.all([p1, p2]) .then(() => { fail('one of the users should not have been created'); @@ -147,6 +120,32 @@ describe('miscellaneous', function() { .catch(done); }); + it('ensure that if people already have duplicate users, they can still sign up new users', done => { + let config = new Config('test'); + config.database.adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) + .then(() => config.database.adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('u'); + user.signUp() + .catch(error => { + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + done(); + }); + }) + .catch(() => { + fail('save should have succeeded'); + done(); + }); + }); + it('ensure that if people already have duplicate emails, they can still sign up new users', done => { let config = new Config('test'); config.database.adapter.createObject('_User', { objectId: 'x', email: 'a@b.c' }, requiredUserFields) @@ -176,7 +175,7 @@ describe('miscellaneous', function() { }); }); - fit('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => { + it('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => { let config = new Config('test'); config.database.adapter.ensureUniqueness('_User', ['randomField'], requiredUserFields) .then(() => { diff --git a/spec/Uniqueness.spec.js b/spec/Uniqueness.spec.js index 9251ac6910..80e89901a2 100644 --- a/spec/Uniqueness.spec.js +++ b/spec/Uniqueness.spec.js @@ -12,7 +12,7 @@ describe('Uniqueness', function() { obj.save().then(() => { expect(obj.id).not.toBeUndefined(); let config = new Config('test'); - return config.database.adapter.ensureUniqueness('UniqueField', ['unique'], { fields: { unique: { type: 'String' } } }) + return config.database.adapter.ensureUniqueness('UniqueField', ['unique'], { fields: { unique: { __type: 'String' } } }) }) .then(() => { let obj = new Parse.Object('UniqueField'); @@ -33,7 +33,10 @@ describe('Uniqueness', function() { .then(() => obj.save({ ptr: obj })) .then(() => { let config = new Config('test'); - return config.database.adapter.ensureUniqueness('UniquePointer', ['ptr'], { fields: { unique: { type: 'String' } } }) + return config.database.adapter.ensureUniqueness('UniquePointer', ['ptr'], { fields: { + string: { __type: 'String' }, + ptr: { __type: 'Pointer', targetClass: 'UniquePointer' } + } }); }) .then(() => { let newObj = new Parse.Object('UniquePointer') @@ -58,7 +61,7 @@ describe('Uniqueness', function() { Parse.Object.saveAll([o1, o2]) .then(() => { let config = new Config('test'); - return config.database.adapter.ensureUniqueness('UniqueFail', ['key'], { fields: { key: { type: 'String' } } }); + return config.database.adapter.ensureUniqueness('UniqueFail', ['key'], { fields: { key: { __type: 'String' } } }); }) .catch(error => { expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); @@ -68,7 +71,7 @@ describe('Uniqueness', function() { it('can do compound uniqueness', done => { let config = new Config('test'); - config.database.adapter.ensureUniqueness('CompoundUnique', ['k1', 'k2'], { fields: { k1: { type: 'String' }, k2: { type: 'String' } } }) + config.database.adapter.ensureUniqueness('CompoundUnique', ['k1', 'k2'], { fields: { k1: { __type: 'String' }, k2: { __type: 'String' } } }) .then(() => { let o1 = new Parse.Object('CompoundUnique'); o1.set('k1', 'v1'); @@ -98,12 +101,4 @@ describe('Uniqueness', function() { done(); }); }); - - it('adding a unique index to an existing field works even if it has nulls', done => { - - }); - - it('adding a unique index to an existing field doesnt prevent you from adding new documents with nulls', done => { - - }); }); diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index bf41582b19..e229b420ce 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -83,6 +83,17 @@ export default class MongoCollection { return this._mongoCollection.deleteMany(query); } + _ensureSparseUniqueIndexInBackground(indexRequest) { + return new Promise((resolve, reject) => { + this._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, (error, indexName) => { + if (error) { + reject(error); + } + resolve(); + }); + }); + } + drop() { return this._mongoCollection.drop(); } diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 60b6802f59..8a0c2c12b6 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -148,7 +148,7 @@ class MongoSchemaCollection { if (results.length === 1) { return mongoSchemaToParseSchema(results[0]); } else { - return Promise.reject(); + throw undefined; } }); } @@ -175,9 +175,9 @@ class MongoSchemaCollection { .then(result => mongoSchemaToParseSchema(result.ops[0])) .catch(error => { if (error.code === 11000) { //Mongo's duplicate key error - return Promise.reject(); + throw undefined; } - return Promise.reject(error); + throw error; }); } @@ -207,17 +207,17 @@ class MongoSchemaCollection { if (type.type === 'GeoPoint') { // Make sure there are not other geopoint fields if (Object.keys(schema.fields).some(existingField => schema.fields[existingField].type === 'GeoPoint')) { - return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.')); + throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.'); } } - return Promise.resolve(); + return; }, error => { // If error is undefined, the schema doesn't exist, and we can create the schema with the field. // If some other error, reject with it. if (error === undefined) { - return Promise.resolve(); + return; } - throw Promise.reject(error); + throw error; }) .then(() => { // We use $exists and $set to avoid overwriting the field type if it diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 911e0521e4..d3bec892b6 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -102,9 +102,9 @@ export class MongoStorageAdapter { .catch(error => { // 'ns not found' means collection was already gone. Ignore deletion attempt. if (error.message == 'ns not found') { - return Promise.resolve(); + return; } - return Promise.reject(error); + throw error; }); } @@ -180,7 +180,7 @@ export class MongoStorageAdapter { throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); } - return Promise.reject(error); + throw error; }); } @@ -239,27 +239,22 @@ export class MongoStorageAdapter { // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't // currently know which fields are nullable and which aren't, we ignore that criteria. // As such, we shouldn't expose this function to users of parse until we have an out-of-band - // Way of determining if a field is nullable. + // Way of determining if a field is nullable. Undefined doesn't count against uniqueness, + // which is why we use sparse indexes. ensureUniqueness(className, fieldNames, schema) { let indexCreationRequest = {}; - fieldNames.map(fieldName => transformKey(className, fieldName, schema)).forEach(transformedName => { - indexCreationRequest[transformedName] = 1; + let mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema)); + mongoFieldNames.forEach(fieldName => { + indexCreationRequest[fieldName] = 1; }); return this.adaptiveCollection(className) - .then(collection => { - return new Promise((resolve, reject) => { - collection._mongoCollection.ensureIndex(indexCreationRequest, { unique: true, background: true }, (err, indexName) => { - if (err) { - if (err.code === 11000) { - reject(new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Tried to force field uniqueness for a class that already has duplicates.')); - } else { - reject(err); - } - } else { - resolve(); - } - }); - }) + .then(collection => collection._ensureSparseUniqueIndexInBackground(indexCreationRequest)) + .catch(error => { + if (error.code === 11000) { + throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Tried to ensure field uniqueness for a class that already has duplicates.'); + } else { + throw error; + } }); } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index de9aa5955c..f719750eff 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -87,8 +87,7 @@ DatabaseController.prototype.validateClassName = function(className) { return Promise.resolve(); } if (!SchemaController.classNameIsValid(className)) { - const error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className); - return Promise.reject(error); + return Promise.reject(new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className)); } return Promise.resolve(); }; diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index be6e717592..cf5a9789f7 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -43,7 +43,7 @@ export class UserController extends AdaptableController { if (!this.shouldVerifyEmails) { // Trying to verify email when not enabled // TODO: Better error here. - return Promise.reject(); + throw undefined; } let database = this.config.database.WithoutValidation(); return database.update('_User', { @@ -51,7 +51,7 @@ export class UserController extends AdaptableController { _email_verify_token: token }, {emailVerified: true}).then(document => { if (!document) { - return Promise.reject(); + throw undefined; } return Promise.resolve(document); }); @@ -64,7 +64,7 @@ export class UserController extends AdaptableController { _perishable_token: token }, {limit: 1}).then(results => { if (results.length != 1) { - return Promise.reject(); + throw undefined; } return results[0]; }); @@ -85,7 +85,7 @@ export class UserController extends AdaptableController { var query = new RestQuery(this.config, Auth.master(this.config), '_User', where); return query.execute().then(function(result){ if (result.results.length != 1) { - return Promise.reject(); + throw undefined; } return result.results[0]; }) diff --git a/src/RestWrite.js b/src/RestWrite.js index 5b9676447a..4a662ed2ed 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -107,9 +107,9 @@ RestWrite.prototype.getUserAndRoleACL = function() { return this.auth.getUserRoles().then((roles) => { roles.push(this.auth.user.id); this.runOptions.acl = this.runOptions.acl.concat(roles); - return Promise.resolve(); + return; }); - }else{ + } else { return Promise.resolve(); } }; @@ -121,7 +121,7 @@ RestWrite.prototype.validateClientClassCreation = function() { && sysClass.indexOf(this.className) === -1) { return this.config.database.collectionExists(this.className).then((hasClass) => { if (hasClass === true) { - return Promise.resolve(); + return; } throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, @@ -311,7 +311,7 @@ RestWrite.prototype.handleAuthData = function(authData) { } } } - return Promise.resolve(); + return; }); } @@ -595,7 +595,7 @@ RestWrite.prototype.handleInstallation = function() { 'deviceType may not be changed in this ' + 'operation'); } - return Promise.resolve(); + return; }); }); } @@ -807,7 +807,7 @@ RestWrite.prototype.runDatabaseOperation = function() { if (results.length > 0) { throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); } - throw error; + throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); }); }) .then(response => { diff --git a/src/rest.js b/src/rest.js index f7e21f9e90..45f0d7db74 100644 --- a/src/rest.js +++ b/src/rest.js @@ -69,8 +69,8 @@ function del(config, auth, className, objectId) { }).then(() => { if (!auth.isMaster) { return auth.getUserRoles(); - }else{ - return Promise.resolve(); + } else { + return; } }).then(() => { var options = {}; @@ -87,7 +87,7 @@ function del(config, auth, className, objectId) { }, options); }).then(() => { triggers.maybeRunTrigger(triggers.Types.afterDelete, auth, inflatedObject, null, config); - return Promise.resolve(); + return; }); } From 895eaf1eeaa9866fffcf127693d9c70e01093d9e Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 1 Jun 2016 17:36:35 -0700 Subject: [PATCH 05/32] Notes on how to upgrade to 2.3.0 safely --- 2.3.0.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 2.3.0.md diff --git a/2.3.0.md b/2.3.0.md new file mode 100644 index 0000000000..0a5abe0ccd --- /dev/null +++ b/2.3.0.md @@ -0,0 +1,75 @@ +# Upgrading Parse Server to version 2.3.0 + +Parse Server version 2.3.0 begins using unique indexes to ensure User's username and email are unique. This is not a backwards incompatable change, but it may in some cases cause a significant performance regression until the index finishes building. Building the unique index before upgrading your Parse Server version will eliminate the performance impact, and is a recommended step before upgrading any app to Parse Server 2.3.0. New apps starting with version 2.3.0 do not need to take any steps before beginning their project. + +If you are using MongoDB in Cluster or Replica Set mode, we recommend reading Mongo's [documentation on index building](https://docs.mongodb.com/v3.0/tutorial/build-indexes-on-replica-sets/) first. If you are not using these features, you can execute the following commands from the Mongo shell to build the unique index. You may also want to create a backup first. + +```js +// Select the database that your Parse App uses +use parse; + +// Select the collection your Parse App uses for users. For migrated apps, this probably include a collectionPrefix. +var coll = db['your_prefix:_User']; + +// You can check if the indexes already exists by running coll.getIndexes() +coll.getIndexes(); + +// The indexes you want should look like this. If they already exists, you can skip creating them. +/* +{ + "v" : 1, + "unique" : true, + "key" : { + "username" : 1 + }, + "name" : "username_1", + "ns" : "parse.your_prefix:_User", + "background" : true, + "sparse" : true +} + +{ + "v" : 1, + "unique" : true, + "key" : { + "email" : 1 + }, + "name" : "email_1", + "ns" : "parse.your_prefix:_User", + "background" : true, + "sparse" : true +} +*/ + +// Create the username index. +// "background: true" is mandatory and avoids downtime while the index builds. +// "sparse: true" is also mandatory because Parse Server uses sparse indexes. +coll.ensureIndex({ username: 1 }, { background: true, unique: true, sparse: true }); + +// Create the email index. +// "background: true" is still mandatory. +// "sparse: true" is also mandatory both because Parse Server uses sparse indexes, and because email addresses are not required by the Parse API. +coll.ensureIndex({ email: 1 }, { background: true, unique: true, sparse: true }); +``` + +There are some issues you may run into during this process: + +## Mongo complains that the index already exists, but with different options + +In this case, you will need to create the unique index using a different index name, and/or remove the incorrect index. Avoid downtime or a temporary performance regression in your app by building the new index before removing the old one. + +## There is already non-unique data in the username or email field + +This is possible if you have explicitly set some user's emails to null. These null emails can be removed with this command: + +```js +coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, { multi: true }) +``` + +## There is already non-unique data in the username or email field, and it's not nulls + +This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows. + +## My app depends on emails set to null behaving differently from emails set to undefined + +You will need to update your app before executing the previous steps. From f0353e66981f6bd903c90a55467dfc96b31e19c3 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 6 Jun 2016 13:47:11 -0700 Subject: [PATCH 06/32] index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)" --- spec/ParseAPI.spec.js | 81 ++++++++++--------- spec/helper.js | 8 +- src/Adapters/Storage/Mongo/MongoCollection.js | 3 +- src/DatabaseAdapter.js | 63 +++++++++++++++ src/PromiseRouter.js | 4 +- src/RestWrite.js | 78 +++++++----------- src/middlewares.js | 1 + 7 files changed, 149 insertions(+), 89 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 32adc81c0a..ae7a624abe 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -11,7 +11,7 @@ let defaultColumns = require('../src/Controllers/SchemaController').defaultColum const requiredUserFields = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._User) }; -describe('miscellaneous', function() { +fdescribe('miscellaneous', function() { it('create a GameScore object', function(done) { var obj = new Parse.Object('GameScore'); obj.set('score', 1337); @@ -80,44 +80,53 @@ describe('miscellaneous', function() { .catch(done); }); - it('ensure that email is uniquely indexed', done => { - let numCreated = 0; - let numFailed = 0; - - let user1 = new Parse.User(); - user1.setPassword('asdf'); - user1.setUsername('u1'); - user1.setEmail('dupe@dupe.dupe'); - let p1 = user1.signUp(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }, error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); + fit('ensure that email is uniquely indexed', done => { + DatabaseAdapter._indexBuildsCompleted('test') + .then(() => { + let numCreated = 0; + let numFailed = 0; + + let user1 = new Parse.User(); + user1.setPassword('asdf'); + user1.setUsername('u1'); + user1.setEmail('dupe@dupe.dupe'); + let p1 = user1.signUp(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }, error => { + numFailed++; + console.log(error); + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); - let user2 = new Parse.User(); - user2.setPassword('asdf'); - user2.setUsername('u2'); - user2.setEmail('dupe@dupe.dupe'); - let p2 = user2.signUp(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }, error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); + let user2 = new Parse.User(); + user2.setPassword('asdf'); + user2.setUsername('u2'); + user2.setEmail('dupe@dupe.dupe'); + let p2 = user2.signUp(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }, error => { + numFailed++; + console.log(error); + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); - Parse.Promise.all([p1, p2]) - .then(() => { - fail('one of the users should not have been created'); - done(); + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); }) - .catch(done); + .catch(error => { + fail('index build failed') + done(); + }); }); it('ensure that if people already have duplicate users, they can still sign up new users', done => { diff --git a/spec/helper.js b/spec/helper.js index 43ad0deebd..d0a9fdafed 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -111,7 +111,7 @@ afterEach(function(done) { }) .then(() => Parse.User.logOut()) .then(() => { - return TestUtils.destroyAllDataPermanently(); + //return TestUtils.destroyAllDataPermanently(); }).then(() => { done(); }, (error) => { @@ -243,14 +243,16 @@ function mockFacebook() { facebook.validateAuthData = function(authData) { if (authData.id === '8675309' && authData.access_token === 'jenny') { return Promise.resolve(); + } else { + throw undefined; } - return Promise.reject(); }; facebook.validateAppId = function(appId, authData) { if (authData.access_token === 'jenny') { return Promise.resolve(); + } else { + throw undefined; } - return Promise.reject(); }; return facebook; } diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index e229b420ce..c494869bad 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -88,8 +88,9 @@ export default class MongoCollection { this._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, (error, indexName) => { if (error) { reject(error); + } else { + resolve(); } - resolve(); }); }); } diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index 25ce69b422..39e93c9add 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -17,10 +17,16 @@ import DatabaseController from './Controllers/DatabaseController'; import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; +import log from './logger'; + +var SchemaController = require('./Controllers/SchemaController'); let dbConnections = {}; let appDatabaseURIs = {}; let appDatabaseOptions = {}; +let indexBuildCreationPromises = {}; + +const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } }; function setAppDatabaseURI(appId, uri) { appDatabaseURIs[appId] = uri; @@ -49,6 +55,11 @@ function destroyAllDataPermanently() { throw 'Only supported in test environment'; } +//Super janky. Will be removed in a later PR. +function _indexBuildsCompleted(appId) { + return indexBuildCreationPromises[appId]; +} + function getDatabaseConnection(appId: string, collectionPrefix: string) { if (dbConnections[appId]) { return dbConnections[appId]; @@ -62,6 +73,57 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) { dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions), {appId: appId}); + // Kick off unique index build in the background (or ensure the unique index already exists) + // A bit janky, will be fixed in a later PR. + let p1 = dbConnections[appId].adapter.ensureUniqueness('_User', ['username'], requiredUserFields) + .catch(error => { + log.warn('Unable to ensure uniqueness for usernames: ', error); + return Promise.reject(); + }); + + let p2 = dbConnections[appId].adapter.ensureUniqueness('_User', ['email'], requiredUserFields) + .catch(error => { + log.warn('Unabled to ensure uniqueness for user email addresses: ', error); + return Promise.reject(); + }) + + indexBuildCreationPromises[appId] = p1.then(() => p2) + .then(() => console.log('index build success')) + .then(() => { + let numCreated = 0; + let numFailed = 0; + + let user1 = new Parse.User(); + user1.setPassword('asdf'); + user1.setUsername('u1'); + user1.setEmail('dupe@dupe.dupe'); + let p1 = user1.signUp(); + p1.then(user => { + numCreated++; + console.log(numCreated) + }, error => { + numFailed++; + console.log(error); + console.log(numFailed) + console.log(error.code) + }); + + let user2 = new Parse.User(); + user2.setPassword('asdf'); + user2.setUsername('u2'); + user2.setEmail('dupe@dupe.dupe'); + let p2 = user2.signUp(); + p2.then(user => { + numCreated++; + console.log(numCreated) + }, error => { + numFailed++; + console.log(error); + console.log(numFailed) + console.log(error.code) + }); + }) + return dbConnections[appId]; } @@ -71,4 +133,5 @@ module.exports = { setAppDatabaseURI: setAppDatabaseURI, clearDatabaseSettings: clearDatabaseSettings, destroyAllDataPermanently: destroyAllDataPermanently, + _indexBuildsCompleted, }; diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js index b159ef0768..0f9ca3d342 100644 --- a/src/PromiseRouter.js +++ b/src/PromiseRouter.js @@ -6,8 +6,8 @@ // components that external developers may be modifying. import express from 'express'; -import url from 'url'; -import log from './logger'; +import url from 'url'; +import log from './logger'; export default class PromiseRouter { // Each entry should be an object with: diff --git a/src/RestWrite.js b/src/RestWrite.js index 4a662ed2ed..6b18dd28c8 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -14,8 +14,6 @@ var triggers = require('./triggers'); import RestQuery from './RestQuery'; import _ from 'lodash'; -const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } }; - // query and data are both provided in REST API format. So data // types are encoded by plain old objects. // If query is null, this is a "create" and the data in data should be @@ -358,28 +356,21 @@ RestWrite.prototype.transformUser = function() { } return; } - // TODO: refactor this so that ensureUniqueness isn't called for every single sign up. - return this.config.database.adapter.ensureUniqueness('_User', ['username'], requiredUserFields) - .catch(error => { - if (error.code === Parse.Error.DUPLICATE_VALUE) { - // If they already have duplicate usernames or emails, the ensureUniqueness will fail, - // and then nobody will be able to sign up D: so just use the old, janky - // method to check for duplicate usernames. - return this.config.database.find( - this.className, - { username: this.data.username, objectId: {'$ne': this.objectId()} }, - { limit: 1 } - ) - .then(results => { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); - } - return; - }); + // We need to a find to check for duplicate username in case they are missing the unique index on usernames + // TODO: Check if there is a unique index, and if so, skip this query. + return this.config.database.find( + this.className, + { username: this.data.username, objectId: {'$ne': this.objectId()} }, + { limit: 1 } + ) + .then(results => { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); } - throw error; - }) - }).then(() => { + return; + }); + }) + .then(() => { if (!this.data.email || this.data.email.__op === 'Delete') { return; } @@ -387,32 +378,25 @@ RestWrite.prototype.transformUser = function() { if (!this.data.email.match(/^.+@.+$/)) { throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.'); } - // Check for email uniqueness - return this.config.database.adapter.ensureUniqueness('_User', ['email'], requiredUserFields) - .catch(error => { - // Same problem for email as above for username - if (error.code === Parse.Error.DUPLICATE_VALUE) { - return this.config.database.find( - this.className, - { email: this.data.email, objectId: {'$ne': this.objectId()} }, - { limit: 1 } - ) - .then(results => { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); - } - return; - }); + // Same problem for email as above for username + return this.config.database.find( + this.className, + { email: this.data.email, objectId: {'$ne': this.objectId()} }, + { limit: 1 } + ) + .then(results => { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); } - throw error; - }) - .then(() => { - // We updated the email, send a new validation - this.storage['sendVerificationEmail'] = true; - this.config.userController.setEmailVerifyToken(this.data); return; - }) - }); + }); + }) + .then(() => { + // We updated the email, send a new validation + this.storage['sendVerificationEmail'] = true; + this.config.userController.setEmailVerifyToken(this.data); + return; + }) }; RestWrite.prototype.createSessionTokenIfNeeded = function() { diff --git a/src/middlewares.js b/src/middlewares.js index aea470e380..9ff589af4f 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -235,6 +235,7 @@ var handleParseErrors = function(err, req, res, next) { } res.status(httpStatus); + console.log(err); res.json({code: err.code, error: err.message}); } else if (err.status && err.message) { res.status(err.status); From 44f9ee50a19bddb19ca334375d5dae6abe316383 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 6 Jun 2016 16:28:28 -0700 Subject: [PATCH 07/32] reconfigure username/email tests --- spec/ParseAPI.spec.js | 62 +++++++++++++++++++++--------------------- spec/helper.js | 18 ++++++------ src/DatabaseAdapter.js | 38 ++------------------------ src/middlewares.js | 1 - 4 files changed, 42 insertions(+), 77 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index ae7a624abe..127435609f 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -10,7 +10,6 @@ let defaultColumns = require('../src/Controllers/SchemaController').defaultColum const requiredUserFields = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._User) }; - fdescribe('miscellaneous', function() { it('create a GameScore object', function(done) { var obj = new Parse.Object('GameScore'); @@ -50,37 +49,40 @@ fdescribe('miscellaneous', function() { }); it('fail to create a duplicate username', done => { - let numCreated = 0; - let numFailed = 0; - let p1 = createTestUser(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - let p2 = createTestUser(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - Parse.Promise.all([p1, p2]) + DatabaseAdapter._indexBuildsCompleted('test') .then(() => { - fail('one of the users should not have been created'); - done(); - }) - .catch(done); + let numCreated = 0; + let numFailed = 0; + let p1 = createTestUser(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + let p2 = createTestUser(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); + }); }); - fit('ensure that email is uniquely indexed', done => { + it('ensure that email is uniquely indexed', done => { DatabaseAdapter._indexBuildsCompleted('test') .then(() => { let numCreated = 0; @@ -96,7 +98,6 @@ fdescribe('miscellaneous', function() { expect(numCreated).toEqual(1); }, error => { numFailed++; - console.log(error); expect(numFailed).toEqual(1); expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); }); @@ -111,7 +112,6 @@ fdescribe('miscellaneous', function() { expect(numCreated).toEqual(1); }, error => { numFailed++; - console.log(error); expect(numFailed).toEqual(1); expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); }); diff --git a/spec/helper.js b/spec/helper.js index d0a9fdafed..e343ae825e 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -56,9 +56,6 @@ var currentConfiguration; // Allows testing specific configurations of Parse Server const setServerConfiguration = configuration => { // the configuration hasn't changed - if (configuration === currentConfiguration) { - return; - } DatabaseAdapter.clearDatabaseSettings(); currentConfiguration = configuration; server.close(); @@ -80,11 +77,14 @@ Parse.serverURL = 'http://localhost:' + port + '/1'; Parse.Promise.disableAPlusCompliant(); beforeEach(function(done) { - restoreServerConfiguration(); - Parse.initialize('test', 'test', 'test'); - Parse.serverURL = 'http://localhost:' + port + '/1'; - Parse.User.enableUnsafeCurrentUser(); - return TestUtils.destroyAllDataPermanently().then(done, fail); + TestUtils.destroyAllDataPermanently() + .then(() => { + restoreServerConfiguration(); + Parse.initialize('test', 'test', 'test'); + Parse.serverURL = 'http://localhost:' + port + '/1'; + Parse.User.enableUnsafeCurrentUser(); + done(); + }, fail) }); var mongoAdapter = new MongoStorageAdapter({ @@ -111,7 +111,7 @@ afterEach(function(done) { }) .then(() => Parse.User.logOut()) .then(() => { - //return TestUtils.destroyAllDataPermanently(); + return TestUtils.destroyAllDataPermanently(); }).then(() => { done(); }, (error) => { diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index 39e93c9add..ad8af7e5e2 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -41,6 +41,7 @@ function clearDatabaseSettings() { appDatabaseURIs = {}; dbConnections = {}; appDatabaseOptions = {}; + indexBuildCreationPromises = {}; } //Used by tests @@ -87,42 +88,7 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) { return Promise.reject(); }) - indexBuildCreationPromises[appId] = p1.then(() => p2) - .then(() => console.log('index build success')) - .then(() => { - let numCreated = 0; - let numFailed = 0; - - let user1 = new Parse.User(); - user1.setPassword('asdf'); - user1.setUsername('u1'); - user1.setEmail('dupe@dupe.dupe'); - let p1 = user1.signUp(); - p1.then(user => { - numCreated++; - console.log(numCreated) - }, error => { - numFailed++; - console.log(error); - console.log(numFailed) - console.log(error.code) - }); - - let user2 = new Parse.User(); - user2.setPassword('asdf'); - user2.setUsername('u2'); - user2.setEmail('dupe@dupe.dupe'); - let p2 = user2.signUp(); - p2.then(user => { - numCreated++; - console.log(numCreated) - }, error => { - numFailed++; - console.log(error); - console.log(numFailed) - console.log(error.code) - }); - }) + indexBuildCreationPromises[appId] = Promise.all([p1, p2]) return dbConnections[appId]; } diff --git a/src/middlewares.js b/src/middlewares.js index 9ff589af4f..aea470e380 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -235,7 +235,6 @@ var handleParseErrors = function(err, req, res, next) { } res.status(httpStatus); - console.log(err); res.json({code: err.code, error: err.message}); } else if (err.status && err.message) { res.status(err.status); From 1e557e8eb3e3527dd6f87c231967afcb85c7d568 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 6 Jun 2016 20:52:58 -0700 Subject: [PATCH 08/32] Start dealing with test shittyness --- spec/ParseAPI.spec.js | 93 +++++++++++-------- spec/Uniqueness.spec.js | 1 - spec/helper.js | 9 +- .../Storage/Mongo/MongoStorageAdapter.js | 1 + src/DatabaseAdapter.js | 10 +- 5 files changed, 63 insertions(+), 51 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 127435609f..7d963951a0 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -3,14 +3,16 @@ 'use strict'; var DatabaseAdapter = require('../src/DatabaseAdapter'); +const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); var request = require('request'); const Parse = require("parse/node"); let Config = require('../src/Config'); let defaultColumns = require('../src/Controllers/SchemaController').defaultColumns; +var TestUtils = require('../src/index').TestUtils; const requiredUserFields = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._User) }; -fdescribe('miscellaneous', function() { +describe('miscellaneous', function() { it('create a GameScore object', function(done) { var obj = new Parse.Object('GameScore'); obj.set('score', 1337); @@ -130,57 +132,70 @@ fdescribe('miscellaneous', function() { }); it('ensure that if people already have duplicate users, they can still sign up new users', done => { - let config = new Config('test'); - config.database.adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) - .then(() => config.database.adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('zxcv'); - return user.signUp(); - }) + // Remove existing data to clear out unique index + TestUtils.destroyAllDataPermanently() .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('u'); - user.signUp() + let adapter = new MongoStorageAdapter({ + uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', + collectionPrefix: 'test_', + }); + adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) + .then(() => adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('u'); + user.signUp() + .catch(error => { + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + done(); + }); + }) .catch(error => { - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + fail(JSON.stringify(error)); done(); }); - }) - .catch(() => { - fail('save should have succeeded'); + }, () => { + fail('destroyAllDataPermanently failed') done(); }); }); it('ensure that if people already have duplicate emails, they can still sign up new users', done => { - let config = new Config('test'); - config.database.adapter.createObject('_User', { objectId: 'x', email: 'a@b.c' }, requiredUserFields) - .then(() => config.database.adapter.createObject('_User', { objectId: 'y', email: 'a@b.c' }, requiredUserFields)) + // Wipe out existing database with unique index so we can create a duplicate user + TestUtils.destroyAllDataPermanently() .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('qqq'); - user.setEmail('unique@unique.unique'); - return user.signUp(); - }) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('www'); - user.setEmail('a@b.c'); - user.signUp() + let config = new Config('test'); + config.database.adapter.createObject('_User', { objectId: 'x', email: 'a@b.c' }, requiredUserFields) + .then(() => config.database.adapter.createObject('_User', { objectId: 'y', email: 'a@b.c' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('qqq'); + user.setEmail('unique@unique.unique'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('www'); + user.setEmail('a@b.c'); + user.signUp() + .catch(error => { + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + done(); + }); + }) .catch(error => { - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + fail(JSON.stringify(error)); done(); }); - }) - .catch(error => { - console.log(error); - fail('save should have succeeded'); - done(); }); }); diff --git a/spec/Uniqueness.spec.js b/spec/Uniqueness.spec.js index 80e89901a2..fe7e1b88b5 100644 --- a/spec/Uniqueness.spec.js +++ b/spec/Uniqueness.spec.js @@ -1,6 +1,5 @@ 'use strict'; -var DatabaseAdapter = require('../src/DatabaseAdapter'); var request = require('request'); const Parse = require("parse/node"); let Config = require('../src/Config'); diff --git a/spec/helper.js b/spec/helper.js index e343ae825e..f46713f21a 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,6 +1,6 @@ // Sets up a Parse API server for testing. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; +jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 3000; var cache = require('../src/cache').default; var DatabaseAdapter = require('../src/DatabaseAdapter'); @@ -77,12 +77,12 @@ Parse.serverURL = 'http://localhost:' + port + '/1'; Parse.Promise.disableAPlusCompliant(); beforeEach(function(done) { + Parse.User.enableUnsafeCurrentUser(); TestUtils.destroyAllDataPermanently() .then(() => { restoreServerConfiguration(); Parse.initialize('test', 'test', 'test'); Parse.serverURL = 'http://localhost:' + port + '/1'; - Parse.User.enableUnsafeCurrentUser(); done(); }, fail) }); @@ -112,9 +112,8 @@ afterEach(function(done) { .then(() => Parse.User.logOut()) .then(() => { return TestUtils.destroyAllDataPermanently(); - }).then(() => { - done(); - }, (error) => { + }).then(done, + error => { console.log('error in clearData', error); done(); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index d3bec892b6..c3bb30d7e8 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -65,6 +65,7 @@ export class MongoStorageAdapter { this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(database => { this.database = database; }); + return this.connectionPromise; } diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index ad8af7e5e2..fc8d3b5ec0 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -18,6 +18,7 @@ import DatabaseController from './Controllers/DatabaseController'; import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; import log from './logger'; +import _ from 'lodash'; var SchemaController = require('./Controllers/SchemaController'); @@ -39,7 +40,7 @@ function setAppDatabaseOptions(appId: string, options: Object) { //Used by tests function clearDatabaseSettings() { appDatabaseURIs = {}; - dbConnections = {}; + //dbConnections = {}; appDatabaseOptions = {}; indexBuildCreationPromises = {}; } @@ -47,11 +48,8 @@ function clearDatabaseSettings() { //Used by tests function destroyAllDataPermanently() { if (process.env.TESTING) { - var promises = []; - for (var conn in dbConnections) { - promises.push(dbConnections[conn].deleteEverything()); - } - return Promise.all(promises); + return Promise.all(Object.values(indexBuildCreationPromises)) + .then(() => Promise.all(Object.values(dbConnections).map(conn => conn.deleteEverything()))) } throw 'Only supported in test environment'; } From d346b29f22014bec8fbe8828dc4130f246043979 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 6 Jun 2016 20:58:03 -0700 Subject: [PATCH 09/32] Remove tests for files that we are removing --- spec/DatabaseAdapter.spec.js | 23 ----------------------- spec/DatabaseController.spec.js | 18 ------------------ 2 files changed, 41 deletions(-) delete mode 100644 spec/DatabaseAdapter.spec.js delete mode 100644 spec/DatabaseController.spec.js diff --git a/spec/DatabaseAdapter.spec.js b/spec/DatabaseAdapter.spec.js deleted file mode 100644 index 8071173bc0..0000000000 --- a/spec/DatabaseAdapter.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -let DatabaseAdapter = require('../src/DatabaseAdapter'); - -describe('DatabaseAdapter', () => { - it('options and URI are available to adapter', done => { - DatabaseAdapter.setAppDatabaseURI('optionsTest', 'mongodb://localhost:27017/optionsTest'); - DatabaseAdapter.setAppDatabaseOptions('optionsTest', {foo: "bar"}); - let optionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('optionsTest'); - - expect(optionsTestDatabaseConnection).toEqual(jasmine.any(Object)); - expect(optionsTestDatabaseConnection.adapter._mongoOptions).toEqual(jasmine.any(Object)); - expect(optionsTestDatabaseConnection.adapter._mongoOptions.foo).toBe("bar"); - - DatabaseAdapter.setAppDatabaseURI('noOptionsTest', 'mongodb://localhost:27017/noOptionsTest'); - let noOptionsTestDatabaseConnection = DatabaseAdapter.getDatabaseConnection('noOptionsTest'); - - expect(noOptionsTestDatabaseConnection).toEqual(jasmine.any(Object)); - expect(noOptionsTestDatabaseConnection.adapter._mongoOptions).toEqual(jasmine.any(Object)); - - done(); - }); -}); diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js deleted file mode 100644 index 1b4fd11c41..0000000000 --- a/spec/DatabaseController.spec.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -let DatabaseController = require('../src/Controllers/DatabaseController'); -let MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); - -describe('DatabaseController', () => { - it('can be constructed', done => { - let adapter = new MongoStorageAdapter({ - uri: 'mongodb://localhost:27017/test', - collectionPrefix: 'test_', - }); - let databaseController = new DatabaseController(adapter); - databaseController.connect().then(done, error => { - console.log('error', error.stack); - fail(); - }); - }); -}); From 8fe737fcc066fca7275e9ec8a71783ca67b03ce6 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 00:07:21 -0700 Subject: [PATCH 10/32] most tests passing --- spec/CloudCode.spec.js | 2 + spec/ParseAPI.spec.js | 279 ++++++++++++----------- spec/ParseHooks.spec.js | 5 +- spec/ParseInstallation.spec.js | 3 +- spec/PublicAPI.spec.js | 37 ++- spec/RestCreate.spec.js | 3 +- spec/RestQuery.spec.js | 4 +- spec/ValidationAndPasswordsReset.spec.js | 33 ++- spec/helper.js | 21 +- spec/index.spec.js | 25 +- src/Config.js | 3 +- src/Controllers/DatabaseController.js | 8 - src/Controllers/HooksController.js | 15 +- src/DatabaseAdapter.js | 106 ++------- src/ParseServer.js | 50 +++- src/Routers/HooksRouter.js | 17 +- 16 files changed, 281 insertions(+), 330 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 875a6c762c..61cdb041e0 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -6,6 +6,7 @@ const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter describe('Cloud Code', () => { it('can load absolute cloud code file', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', masterKey: 'test', @@ -19,6 +20,7 @@ describe('Cloud Code', () => { it('can load relative cloud code file', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', masterKey: 'test', diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 7d963951a0..2ab82255ae 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -51,151 +51,166 @@ describe('miscellaneous', function() { }); it('fail to create a duplicate username', done => { - DatabaseAdapter._indexBuildsCompleted('test') - .then(() => { - let numCreated = 0; - let numFailed = 0; - let p1 = createTestUser(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - let p2 = createTestUser(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - Parse.Promise.all([p1, p2]) - .then(() => { - fail('one of the users should not have been created'); - done(); - }) - .catch(done); + setServerConfiguration({ + ...defaultConfiguration, + __indexBuildCompletionCallbackForTests: promise => { + promise.then(() => { + let numCreated = 0; + let numFailed = 0; + let p1 = createTestUser(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + let p2 = createTestUser(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); + }); + }, }); }); it('ensure that email is uniquely indexed', done => { - DatabaseAdapter._indexBuildsCompleted('test') - .then(() => { - let numCreated = 0; - let numFailed = 0; - - let user1 = new Parse.User(); - user1.setPassword('asdf'); - user1.setUsername('u1'); - user1.setEmail('dupe@dupe.dupe'); - let p1 = user1.signUp(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }, error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); + setServerConfiguration({ + ...defaultConfiguration, + __indexBuildCompletionCallbackForTests: promise => { + let numCreated = 0; + let numFailed = 0; + + let user1 = new Parse.User(); + user1.setPassword('asdf'); + user1.setUsername('u1'); + user1.setEmail('dupe@dupe.dupe'); + let p1 = user1.signUp(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }, error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); - let user2 = new Parse.User(); - user2.setPassword('asdf'); - user2.setUsername('u2'); - user2.setEmail('dupe@dupe.dupe'); - let p2 = user2.signUp(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }, error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); + let user2 = new Parse.User(); + user2.setPassword('asdf'); + user2.setUsername('u2'); + user2.setEmail('dupe@dupe.dupe'); + let p2 = user2.signUp(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }, error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); - Parse.Promise.all([p1, p2]) - .then(() => { - fail('one of the users should not have been created'); - done(); - }) - .catch(done); - }) - .catch(error => { - fail('index build failed') - done(); + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); + }, }); }); it('ensure that if people already have duplicate users, they can still sign up new users', done => { - // Remove existing data to clear out unique index - TestUtils.destroyAllDataPermanently() - .then(() => { - let adapter = new MongoStorageAdapter({ - uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', - collectionPrefix: 'test_', - }); - adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) - .then(() => adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('zxcv'); - return user.signUp(); - }) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('u'); - user.signUp() - .catch(error => { - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + setServerConfiguration({ + ...defaultConfiguration, + __indexBuildCompletionCallbackForTests: promise => { + // Remove existing data to clear out unique index + promise.then(TestUtils.destroyAllDataPermanently) + .then(() => { + let adapter = new MongoStorageAdapter({ + uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', + collectionPrefix: 'test_', + }); + adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) + .then(() => adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('u'); + user.signUp() + .catch(error => { + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + done(); + }); + }) + .catch(error => { + fail(JSON.stringify(error)); + done(); + }); + }, () => { + fail('destroyAllDataPermanently failed') done(); }); - }) - .catch(error => { - fail(JSON.stringify(error)); - done(); - }); - }, () => { - fail('destroyAllDataPermanently failed') - done(); - }); + }, + }) }); it('ensure that if people already have duplicate emails, they can still sign up new users', done => { - // Wipe out existing database with unique index so we can create a duplicate user - TestUtils.destroyAllDataPermanently() - .then(() => { - let config = new Config('test'); - config.database.adapter.createObject('_User', { objectId: 'x', email: 'a@b.c' }, requiredUserFields) - .then(() => config.database.adapter.createObject('_User', { objectId: 'y', email: 'a@b.c' }, requiredUserFields)) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('qqq'); - user.setEmail('unique@unique.unique'); - return user.signUp(); - }) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('www'); - user.setEmail('a@b.c'); - user.signUp() - .catch(error => { - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - done(); - }); - }) - .catch(error => { - fail(JSON.stringify(error)); - done(); - }); + setServerConfiguration({ + ...defaultConfiguration, + __indexBuildCompletionCallbackForTests: promise => { + // Wipe out existing database with unique index so we can create a duplicate user + promise.then(TestUtils.destroyAllDataPermanently) + .then(() => { + let adapter = new MongoStorageAdapter({ + uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', + collectionPrefix: 'test_', + }); + adapter.createObject('_User', { objectId: 'x', email: 'a@b.c' }, requiredUserFields) + .then(() => adapter.createObject('_User', { objectId: 'y', email: 'a@b.c' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('qqq'); + user.setEmail('unique@unique.unique'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('www'); + user.setEmail('a@b.c'); + user.signUp() + .catch(error => { + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + done(); + }); + }) + .catch(error => { + fail(JSON.stringify(error)); + done(); + }); + }) + } }); }); @@ -370,8 +385,8 @@ describe('miscellaneous', function() { obj.set('foo', 'bar'); return obj.save(); }).then(() => { - var db = DatabaseAdapter.getDatabaseConnection(appId, 'test_'); - return db.adapter.find('TestObject', {}, { fields: {} }, {}); + let config = new Config(appId); + return config.database.adapter.find('TestObject', {}, { fields: {} }, {}); }).then((results) => { expect(results.length).toEqual(1); expect(results[0]['foo']).toEqual('bar'); diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index 1129a0507f..7f5696cd93 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -1,3 +1,4 @@ +"use strict"; /* global describe, it, expect, fail, Parse */ var request = require('request'); var triggers = require('../src/triggers'); @@ -13,7 +14,7 @@ var hookServerURL = "http://localhost:"+port; var app = express(); app.use(bodyParser.json({ 'type': '*/*' })) app.listen(12345); - +let AppCache = require('../src/cache').AppCache; describe('Hooks', () => { @@ -257,7 +258,7 @@ describe('Hooks', () => { expect(triggers.getTrigger("MyClass"+i, "beforeSave", Parse.applicationId)).toBeUndefined(); expect(triggers.getFunction("AFunction"+i, Parse.applicationId)).toBeUndefined(); } - const hooksController = new HooksController(Parse.applicationId); + const hooksController = new HooksController(Parse.applicationId, AppCache.get('test').databaseController); return hooksController.load() }, (err) => { console.error(err); diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index b316981131..2bc191dacf 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -5,13 +5,12 @@ let auth = require('../src/Auth'); let cache = require('../src/cache'); let Config = require('../src/Config'); -let DatabaseAdapter = require('../src/DatabaseAdapter'); let Parse = require('parse/node').Parse; let rest = require('../src/rest'); let request = require("request"); let config = new Config('test'); -let database = DatabaseAdapter.getDatabaseConnection('test', 'test_'); +let database = config.database; let defaultColumns = require('../src/Controllers/SchemaController').defaultColumns; const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) }; diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 008d544ae4..506ea69f71 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -2,43 +2,39 @@ var request = require('request'); describe("public API", () => { - beforeEach(done => { + it("should get invalid_link.html", (done) => { + request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse, body) => { + expect(httpResponse.statusCode).toBe(200); + done(); + }); + }); + + it("should get choose_password", (done) => { setServerConfiguration({ - serverURL: 'http://localhost:8378/1', + ...defaultConfiguration, appId: 'test', appName: 'unused', javascriptKey: 'test', dotNetKey: 'windows', clientKey: 'client', restAPIKey: 'rest', + publicServerURL: 'http://localhost:8378/1', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', - publicServerURL: 'http://localhost:8378/1' - }); - done(); - }) - it("should get invalid_link.html", (done) => { - request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse, body) => { - expect(httpResponse.statusCode).toBe(200); - done(); - }); - }); - - it("should get choose_password", (done) => { + }) request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => { expect(httpResponse.statusCode).toBe(200); done(); }); }); - + it("should get verify_email_success.html", (done) => { request('http://localhost:8378/1/apps/verify_email_success.html', (err, httpResponse, body) => { expect(httpResponse.statusCode).toBe(200); done(); }); }); - + it("should get password_reset_success.html", (done) => { request('http://localhost:8378/1/apps/password_reset_success.html', (err, httpResponse, body) => { expect(httpResponse.statusCode).toBe(200); @@ -50,7 +46,7 @@ describe("public API", () => { describe("public API without publicServerURL", () => { beforeEach(done => { setServerConfiguration({ - serverURL: 'http://localhost:8378/1', + ...defaultConfiguration, appId: 'test', appName: 'unused', javascriptKey: 'test', @@ -58,7 +54,6 @@ describe("public API without publicServerURL", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', }); done(); @@ -69,14 +64,14 @@ describe("public API without publicServerURL", () => { done(); }); }); - + it("should get 404 choose_password", (done) => { request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => { expect(httpResponse.statusCode).toBe(404); done(); }); }); - + it("should get 404 on request_password_reset", (done) => { request('http://localhost:8378/1/apps/test/request_password_reset', (err, httpResponse, body) => { expect(httpResponse.statusCode).toBe(404); diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index 226121b33c..c187ceeea2 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -3,13 +3,12 @@ var auth = require('../src/Auth'); var cache = require('../src/cache'); var Config = require('../src/Config'); -var DatabaseAdapter = require('../src/DatabaseAdapter'); var Parse = require('parse/node').Parse; var rest = require('../src/rest'); var request = require('request'); var config = new Config('test'); -var database = DatabaseAdapter.getDatabaseConnection('test', 'test_'); +let database = config.database; describe('rest create', () => { it('handles _id', done => { diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 3cff633940..c0df2855d1 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -7,10 +7,8 @@ var rest = require('../src/rest'); var querystring = require('querystring'); var request = require('request'); -var DatabaseAdapter = require('../src/DatabaseAdapter'); -var database = DatabaseAdapter.getDatabaseConnection('test', 'test_'); - var config = new Config('test'); +let database = config.database; var nobody = auth.nobody(config); describe('rest query', () => { diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index cac3c56cfd..64960f7436 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -7,6 +7,7 @@ let Config = require("../src/Config"); describe("Custom Pages Configuration", () => { it("should set the custom pages", (done) => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -15,7 +16,6 @@ describe("Custom Pages Configuration", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', customPages: { invalidLink: "myInvalidLink", @@ -46,6 +46,7 @@ describe("Email Verification", () => { sendMail: () => Promise.resolve() } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -54,7 +55,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -88,6 +88,7 @@ describe("Email Verification", () => { sendMail: () => Promise.resolve() } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -96,7 +97,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -129,6 +129,7 @@ describe("Email Verification", () => { sendMail: () => Promise.resolve() } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -137,7 +138,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -179,6 +179,7 @@ describe("Email Verification", () => { sendMail: () => Promise.resolve() } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -187,7 +188,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -243,6 +243,7 @@ describe("Email Verification", () => { } } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'My Cool App', @@ -251,7 +252,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -286,6 +286,7 @@ describe("Email Verification", () => { it('fails if you include an emailAdapter, set verifyUserEmails to false, dont set a publicServerURL, and try to send a password reset email (regression test for #1649)', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -294,7 +295,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: false, emailAdapter: MockEmailAdapterWithOptions({ @@ -327,6 +327,7 @@ describe("Email Verification", () => { sendMail: () => Promise.resolve() } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -335,7 +336,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: false, emailAdapter: emailAdapter, @@ -371,6 +371,7 @@ describe("Email Verification", () => { sendMail: () => {} } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'emailing app', @@ -379,7 +380,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -422,6 +422,7 @@ describe("Email Verification", () => { sendMail: () => {} } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'emailing app', @@ -430,7 +431,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -444,6 +444,7 @@ describe("Email Verification", () => { it('redirects you to invalid link if you try to verify email incorrecly', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'emailing app', @@ -452,7 +453,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: { @@ -473,6 +473,7 @@ describe("Email Verification", () => { it('redirects you to invalid link if you try to validate a nonexistant users email', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'emailing app', @@ -481,7 +482,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: { @@ -520,6 +520,7 @@ describe("Email Verification", () => { sendMail: () => {} } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'emailing app', @@ -528,7 +529,6 @@ describe("Email Verification", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -571,6 +571,7 @@ describe("Password Reset", () => { sendMail: () => {} } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'emailing app', @@ -579,7 +580,6 @@ describe("Password Reset", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -601,6 +601,7 @@ describe("Password Reset", () => { it('redirects you to invalid link if you try to request password for a nonexistant users email', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'emailing app', @@ -609,7 +610,6 @@ describe("Password Reset", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: { @@ -689,6 +689,7 @@ describe("Password Reset", () => { sendMail: () => {} } setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'emailing app', @@ -697,7 +698,6 @@ describe("Password Reset", () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, @@ -716,5 +716,4 @@ describe("Password Reset", () => { }); }); }); - }) diff --git a/spec/helper.js b/spec/helper.js index f46713f21a..78a6758cf7 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -11,12 +11,16 @@ var path = require('path'); var TestUtils = require('../src/index').TestUtils; var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); -var databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; var port = 8378; +var mongoAdapter = new MongoStorageAdapter({ + uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', + collectionPrefix: 'test_', +}) + // Default server configuration for tests. var defaultConfiguration = { - databaseURI: databaseURI, + databaseAdapter: mongoAdapter, serverURL: 'http://localhost:' + port + '/1', appId: 'test', javascriptKey: 'test', @@ -25,7 +29,6 @@ var defaultConfiguration = { restAPIKey: 'rest', webhookKey: 'hook', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', push: { 'ios': { @@ -52,12 +55,8 @@ var server = app.listen(port); // Prevent reinitializing the server from clobbering Cloud Code delete defaultConfiguration.cloud; -var currentConfiguration; // Allows testing specific configurations of Parse Server const setServerConfiguration = configuration => { - // the configuration hasn't changed - DatabaseAdapter.clearDatabaseSettings(); - currentConfiguration = configuration; server.close(); cache.clear(); app = express(); @@ -66,7 +65,6 @@ const setServerConfiguration = configuration => { server = app.listen(port); }; -var restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration); // Set up a Parse client to talk to our test API server var Parse = require('parse/node'); @@ -80,18 +78,13 @@ beforeEach(function(done) { Parse.User.enableUnsafeCurrentUser(); TestUtils.destroyAllDataPermanently() .then(() => { - restoreServerConfiguration(); + setServerConfiguration(defaultConfiguration); Parse.initialize('test', 'test', 'test'); Parse.serverURL = 'http://localhost:' + port + '/1'; done(); }, fail) }); -var mongoAdapter = new MongoStorageAdapter({ - collectionPrefix: defaultConfiguration.collectionPrefix, - uri: databaseURI, -}) - afterEach(function(done) { Parse.Cloud._removeAllHooks(); mongoAdapter.getAllSchemas() diff --git a/spec/index.spec.js b/spec/index.spec.js index 6ea648423a..4fd59ec8b8 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -1,3 +1,4 @@ +"use strict" var request = require('request'); var parseServerPackage = require('../package.json'); var MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); @@ -39,6 +40,7 @@ describe('server', () => { it('fails if database is unreachable', done => { setServerConfiguration({ + ...defaultConfiguration, databaseURI: 'mongodb://fake:fake@ds043605.mongolab.com:43605/drew3', serverURL: 'http://localhost:8378/1', appId: 'test', @@ -47,7 +49,6 @@ describe('server', () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', }); //Need to use rest api because saving via JS SDK results in fail() not getting called @@ -69,6 +70,7 @@ describe('server', () => { it('can load email adapter via object', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -77,7 +79,6 @@ describe('server', () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: MockEmailAdapterWithOptions({ @@ -92,6 +93,7 @@ describe('server', () => { it('can load email adapter via class', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -100,7 +102,6 @@ describe('server', () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: { @@ -118,6 +119,7 @@ describe('server', () => { it('can load email adapter via module name', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -126,7 +128,6 @@ describe('server', () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: { @@ -144,6 +145,7 @@ describe('server', () => { it('can load email adapter via only module name', done => { expect(() => setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -152,7 +154,6 @@ describe('server', () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: 'parse-server-simple-mailgun-adapter', @@ -163,6 +164,7 @@ describe('server', () => { it('throws if you initialize email adapter incorrecly', done => { expect(() => setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -171,7 +173,6 @@ describe('server', () => { clientKey: 'client', restAPIKey: 'rest', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', verifyUserEmails: true, emailAdapter: { @@ -201,10 +202,10 @@ describe('server', () => { it('can create a parse-server', done => { var parseServer = new ParseServer.default({ + ...defaultConfiguration, appId: "aTestApp", masterKey: "aTestMasterKey", serverURL: "http://localhost:12666/parse", - databaseURI: 'mongodb://localhost:27017/aTestApp' }); expect(Parse.applicationId).toEqual("aTestApp"); @@ -230,10 +231,10 @@ describe('server', () => { it('can create a parse-server', done => { var parseServer = ParseServer.ParseServer({ + ...defaultConfiguration, appId: "anOtherTestApp", masterKey: "anOtherTestMasterKey", serverURL: "http://localhost:12667/parse", - databaseURI: 'mongodb://localhost:27017/anotherTstApp' }); expect(Parse.applicationId).toEqual("anOtherTestApp"); @@ -274,6 +275,7 @@ describe('server', () => { it('properly gives publicServerURL when set', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', masterKey: 'test', @@ -286,6 +288,7 @@ describe('server', () => { it('properly removes trailing slash in mount', done => { setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', masterKey: 'test' @@ -297,6 +300,7 @@ describe('server', () => { it('should throw when getting invalid mount', done => { expect(() => setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', masterKey: 'test', @@ -307,6 +311,7 @@ describe('server', () => { it('fails if the session length is not a number', (done) => { expect(() => setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -319,6 +324,7 @@ describe('server', () => { it('fails if the session length is less than or equal to 0', (done) => { expect(() => setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -328,6 +334,7 @@ describe('server', () => { })).toThrow('Session length must be a value greater than 0.'); expect(() => setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -340,6 +347,7 @@ describe('server', () => { it('ignores the session length when expireInactiveSessions set to false', (done) => { expect(() => setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', @@ -350,6 +358,7 @@ describe('server', () => { })).not.toThrow(); expect(() => setServerConfiguration({ + ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', appName: 'unused', diff --git a/src/Config.js b/src/Config.js index 8da1b70cfd..861f46e266 100644 --- a/src/Config.js +++ b/src/Config.js @@ -16,7 +16,6 @@ function removeTrailingSlash(str) { export class Config { constructor(applicationId: string, mount: string) { - let DatabaseAdapter = require('./DatabaseAdapter'); let cacheInfo = AppCache.get(applicationId); if (!cacheInfo) { return; @@ -32,7 +31,7 @@ export class Config { this.fileKey = cacheInfo.fileKey; this.facebookAppIds = cacheInfo.facebookAppIds; this.allowClientClassCreation = cacheInfo.allowClientClassCreation; - this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix); + this.database = cacheInfo.databaseController; this.serverURL = cacheInfo.serverURL; this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index f719750eff..0da314f5d2 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -61,19 +61,12 @@ function DatabaseController(adapter, { skipValidation } = {}) { // it. Instead, use loadSchema to get a schema. this.schemaPromise = null; this.skipValidation = !!skipValidation; - this.connect(); } DatabaseController.prototype.WithoutValidation = function() { return new DatabaseController(this.adapter, {collectionPrefix: this.collectionPrefix, skipValidation: true}); } -// Connects to the database. Returns a promise that resolves when the -// connection is successful. -DatabaseController.prototype.connect = function() { - return this.adapter.connect(); -}; - DatabaseController.prototype.schemaCollection = function() { return this.adapter.schemaCollection(); }; @@ -416,7 +409,6 @@ DatabaseController.prototype.canAddField = function(schema, className, object, a return Promise.resolve(); } -// Deletes everything in the database matching the current collectionPrefix // Won't delete collections in the system namespace // Returns a promise. DatabaseController.prototype.deleteEverything = function() { diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js index 529b47c578..718336e53c 100644 --- a/src/Controllers/HooksController.js +++ b/src/Controllers/HooksController.js @@ -1,23 +1,20 @@ /** @flow weak */ import * as DatabaseAdapter from "../DatabaseAdapter"; -import * as triggers from "../triggers"; -import * as Parse from "parse/node"; -import * as request from "request"; -import { logger } from '../logger'; +import * as triggers from "../triggers"; +import * as Parse from "parse/node"; +import * as request from "request"; +import { logger } from '../logger'; const DefaultHooksCollectionName = "_Hooks"; export class HooksController { _applicationId:string; - _collectionPrefix:string; - _collection; - constructor(applicationId:string, collectionPrefix:string = '', webhookKey) { + constructor(applicationId:string, databaseController, webhookKey) { this._applicationId = applicationId; - this._collectionPrefix = collectionPrefix; this._webhookKey = webhookKey; - this.database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix).WithoutValidation(); + this.database = databaseController; } load() { diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index fc8d3b5ec0..88fcbe4280 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -1,101 +1,21 @@ -/** @flow weak */ -// Database Adapter -// -// Allows you to change the underlying database. -// -// Adapter classes must implement the following methods: -// * a constructor with signature (connectionString, optionsObject) -// * connect() -// * loadSchema() -// * create(className, object) -// * find(className, query, options) -// * update(className, query, update, options) -// * destroy(className, query, options) -// * This list is incomplete and the database process is not fully modularized. -// -// Default is MongoStorageAdapter. - -import DatabaseController from './Controllers/DatabaseController'; -import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; -import log from './logger'; -import _ from 'lodash'; - -var SchemaController = require('./Controllers/SchemaController'); - -let dbConnections = {}; -let appDatabaseURIs = {}; -let appDatabaseOptions = {}; -let indexBuildCreationPromises = {}; - -const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } }; - -function setAppDatabaseURI(appId, uri) { - appDatabaseURIs[appId] = uri; -} - -function setAppDatabaseOptions(appId: string, options: Object) { - appDatabaseOptions[appId] = options; -} - -//Used by tests -function clearDatabaseSettings() { - appDatabaseURIs = {}; - //dbConnections = {}; - appDatabaseOptions = {}; - indexBuildCreationPromises = {}; -} +import AppCache from './cache'; //Used by tests function destroyAllDataPermanently() { if (process.env.TESTING) { - return Promise.all(Object.values(indexBuildCreationPromises)) - .then(() => Promise.all(Object.values(dbConnections).map(conn => conn.deleteEverything()))) + // This is super janky, but destroyAllDataPermanently is + // a janky interface, so we need to have some jankyness + // to support it + return Promise.all(Object.keys(AppCache.cache).map(appId => { + const app = AppCache.get(appId); + if (app.databaseController) { + return app.databaseController.deleteEverything(); + } else { + return Promise.resolve(); + } + })); } throw 'Only supported in test environment'; } -//Super janky. Will be removed in a later PR. -function _indexBuildsCompleted(appId) { - return indexBuildCreationPromises[appId]; -} - -function getDatabaseConnection(appId: string, collectionPrefix: string) { - if (dbConnections[appId]) { - return dbConnections[appId]; - } - - let mongoAdapterOptions = { - collectionPrefix: collectionPrefix, - mongoOptions: appDatabaseOptions[appId], - uri: appDatabaseURIs[appId], //may be undefined if the user didn't supply a URI, in which case the default will be used - } - - dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions), {appId: appId}); - - // Kick off unique index build in the background (or ensure the unique index already exists) - // A bit janky, will be fixed in a later PR. - let p1 = dbConnections[appId].adapter.ensureUniqueness('_User', ['username'], requiredUserFields) - .catch(error => { - log.warn('Unable to ensure uniqueness for usernames: ', error); - return Promise.reject(); - }); - - let p2 = dbConnections[appId].adapter.ensureUniqueness('_User', ['email'], requiredUserFields) - .catch(error => { - log.warn('Unabled to ensure uniqueness for user email addresses: ', error); - return Promise.reject(); - }) - - indexBuildCreationPromises[appId] = Promise.all([p1, p2]) - - return dbConnections[appId]; -} - -module.exports = { - getDatabaseConnection: getDatabaseConnection, - setAppDatabaseOptions: setAppDatabaseOptions, - setAppDatabaseURI: setAppDatabaseURI, - clearDatabaseSettings: clearDatabaseSettings, - destroyAllDataPermanently: destroyAllDataPermanently, - _indexBuildsCompleted, -}; +module.exports = { destroyAllDataPermanently }; diff --git a/src/ParseServer.js b/src/ParseServer.js index 2876cd840a..690237e12e 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -51,10 +51,17 @@ import { SessionsRouter } from './Routers/SessionsRouter'; import { UserController } from './Controllers/UserController'; import { UsersRouter } from './Routers/UsersRouter'; +import DatabaseController from './Controllers/DatabaseController'; +const SchemaController = require('./Controllers/SchemaController'); import ParsePushAdapter from 'parse-server-push-adapter'; +import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); + +const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } }; + + // ParseServer works like a constructor of an express app. // The args that we understand are: // "filesAdapter": a class like GridStoreAdapter providing create, get, @@ -88,6 +95,7 @@ class ParseServer { masterKey = requiredParameter('You must provide a masterKey!'), appName, filesAdapter, + databaseAdapter, push, loggerAdapter, logsFolder, @@ -122,23 +130,30 @@ class ParseServer { expireInactiveSessions = true, verbose = false, revokeSessionOnPasswordReset = true, + __indexBuildCompletionCallbackForTests = () => {}, }) { // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; + if ((databaseOptions || databaseURI || collectionPrefix !== '') && databaseAdapter) { + throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/connectionPrefix.'; + } else if (!databaseAdapter) { + databaseAdapter = new MongoStorageAdapter({ + uri: databaseURI, + collectionPrefix, + mongoOptions: databaseOptions, + }); + } else { + databaseAdapter = loadAdapter(databaseAdapter) + } + if (logsFolder) { configureLogger({ logsFolder }) } - if (databaseOptions) { - DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions); - } - - DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); - if (cloud) { addParseCloud(); if (typeof cloud === 'function') { @@ -168,10 +183,28 @@ class ParseServer { const filesController = new FilesController(filesControllerAdapter, appId); const pushController = new PushController(pushControllerAdapter, appId); const loggerController = new LoggerController(loggerControllerAdapter, appId); - const hooksController = new HooksController(appId, collectionPrefix, webhookKey); const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails }); const liveQueryController = new LiveQueryController(liveQuery); const cacheController = new CacheController(cacheControllerAdapter, appId); + const databaseController = new DatabaseController(databaseAdapter); + const hooksController = new HooksController(appId, databaseController, webhookKey); + + let usernameUniqueness = databaseController.adapter.ensureUniqueness('_User', ['username'], requiredUserFields) + .catch(error => { + log.warn('Unable to ensure uniqueness for usernames: ', error); + return Promise.reject(); + }); + + let emailUniqueness = databaseController.adapter.ensureUniqueness('_User', ['email'], requiredUserFields) + .catch(error => { + log.warn('Unabled to ensure uniqueness for user email addresses: ', error); + return Promise.reject(); + }) + + if (process.env.TESTING) { + __indexBuildCompletionCallbackForTests(Promise.all([usernameUniqueness, emailUniqueness])); + } + AppCache.put(appId, { masterKey: masterKey, @@ -200,7 +233,8 @@ class ParseServer { liveQueryController: liveQueryController, sessionLength: Number(sessionLength), expireInactiveSessions: expireInactiveSessions, - revokeSessionOnPasswordReset + revokeSessionOnPasswordReset, + databaseController, }); // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability diff --git a/src/Routers/HooksRouter.js b/src/Routers/HooksRouter.js index f214e5a6b9..967057899e 100644 --- a/src/Routers/HooksRouter.js +++ b/src/Routers/HooksRouter.js @@ -1,6 +1,5 @@ -import { Parse } from 'parse/node'; -import PromiseRouter from '../PromiseRouter'; -import { HooksController } from '../Controllers/HooksController'; +import { Parse } from 'parse/node'; +import PromiseRouter from '../PromiseRouter'; import * as middleware from "../middlewares"; export class HooksRouter extends PromiseRouter { @@ -26,7 +25,7 @@ export class HooksRouter extends PromiseRouter { return Promise.resolve({response: foundFunction}); }); } - + return hooksController.getFunctions().then((functions) => { return { response: functions || [] }; }, (err) => { @@ -37,7 +36,7 @@ export class HooksRouter extends PromiseRouter { handleGetTriggers(req) { var hooksController = req.config.hooksController; if (req.params.className && req.params.triggerName) { - + return hooksController.getTrigger(req.params.className, req.params.triggerName).then((foundTrigger) => { if (!foundTrigger) { throw new Parse.Error(143,`class ${req.params.className} does not exist`); @@ -45,7 +44,7 @@ export class HooksRouter extends PromiseRouter { return Promise.resolve({response: foundTrigger}); }); } - + return hooksController.getTriggers().then((triggers) => ({ response: triggers || [] })); } @@ -73,10 +72,10 @@ export class HooksRouter extends PromiseRouter { hook.url = req.body.url } else { throw new Parse.Error(143, "invalid hook declaration"); - } + } return this.updateHook(hook, req.config); } - + handlePut(req) { var body = req.body; if (body.__op == "Delete") { @@ -85,7 +84,7 @@ export class HooksRouter extends PromiseRouter { return this.handleUpdate(req); } } - + mountRoutes() { this.route('GET', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this)); this.route('GET', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this)); From 3da4ce0def0990653ff3480947be741d40312097 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 00:25:10 -0700 Subject: [PATCH 11/32] fix failing test --- spec/index.spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 4fd59ec8b8..e0132a7e3b 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -6,6 +6,8 @@ var ParseServer = require("../src/index"); var Config = require('../src/Config'); var express = require('express'); +const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); + describe('server', () => { it('requires a master key and app id', done => { expect(setServerConfiguration.bind(undefined, { })).toThrow('You must provide an appId!'); @@ -41,7 +43,9 @@ describe('server', () => { it('fails if database is unreachable', done => { setServerConfiguration({ ...defaultConfiguration, - databaseURI: 'mongodb://fake:fake@ds043605.mongolab.com:43605/drew3', + databaseAdapter: new MongoStorageAdapter({ + uri: 'mongodb://fake:fake@localhost:43605/drew3', + }), serverURL: 'http://localhost:8378/1', appId: 'test', javascriptKey: 'test', From 0f2c723263a03503c084a57d29f2d09a6ff91b99 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 13:02:26 -0700 Subject: [PATCH 12/32] Make specific server config for tests async --- package.json | 1 + spec/CloudCode.spec.js | 151 +++--- spec/FileLoggerAdapter.spec.js | 6 +- spec/Parse.Push.spec.js | 29 +- spec/ParseAPI.spec.js | 276 +++++----- spec/ParseHooks.spec.js | 46 +- spec/ParseUser.spec.js | 53 +- spec/PublicAPI.spec.js | 34 +- spec/ValidationAndPasswordsReset.spec.js | 655 ++++++++++------------- spec/helper.js | 29 +- spec/index.spec.js | 246 ++++----- src/Adapters/AdapterLoader.js | 3 +- 12 files changed, 681 insertions(+), 848 deletions(-) diff --git a/package.json b/package.json index d6497c9b01..3def0684b7 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "parse-server-simple-mailgun-adapter": "^1.0.0", "redis": "^2.5.0-1", "request": "^2.65.0", + "request-promise": "^3.0.0", "tv4": "^1.2.7", "winston": "^2.1.1", "winston-daily-rotate-file": "^1.0.1", diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 61cdb041e0..315879678b 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1,35 +1,28 @@ "use strict" const Parse = require("parse/node"); const request = require('request'); +const rp = require('request-promise'); const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter; describe('Cloud Code', () => { it('can load absolute cloud code file', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - cloud: __dirname + '/cloud/cloudCodeRelativeFile.js' - }); - Parse.Cloud.run('cloudCodeInFile', {}, result => { - expect(result).toEqual('It is possible to define cloud code in a file.'); - done(); - }); + reconfigureServer({ cloud: __dirname + '/cloud/cloudCodeRelativeFile.js' }) + .then(() => { + Parse.Cloud.run('cloudCodeInFile', {}, result => { + expect(result).toEqual('It is possible to define cloud code in a file.'); + done(); + }); + }) }); it('can load relative cloud code file', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - cloud: './spec/cloud/cloudCodeAbsoluteFile.js' - }); - Parse.Cloud.run('cloudCodeInFile', {}, result => { - expect(result).toEqual('It is possible to define cloud code in a file.'); - done(); - }); + reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' }) + .then(() => { + Parse.Cloud.run('cloudCodeInFile', {}, result => { + expect(result).toEqual('It is possible to define cloud code in a file.'); + done(); + }); + }) }); it('can create functions', done => { @@ -513,67 +506,75 @@ describe('Cloud Code', () => { }); it('clears out the user cache for all sessions when the user is changed', done => { + let session1; + let session2; + let user; const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 }); - setServerConfiguration(Object.assign({}, defaultConfiguration, { cacheAdapter: cacheAdapter })); - Parse.Cloud.define('checkStaleUser', (request, response) => { - response.success(request.user.get('data')); - }); + reconfigureServer({ cacheAdapter }) + .then(() => { + Parse.Cloud.define('checkStaleUser', (request, response) => { + response.success(request.user.get('data')); + }); - let user = new Parse.User(); - user.set('username', 'test'); - user.set('password', 'moon-y'); - user.set('data', 'first data'); - user.signUp() + user = new Parse.User(); + user.set('username', 'test'); + user.set('password', 'moon-y'); + user.set('data', 'first data'); + return user.signUp(); + }) .then(user => { - let session1 = user.getSessionToken(); - request.get({ - url: 'http://localhost:8378/1/login?username=test&password=moon-y', + session1 = user.getSessionToken(); + return rp({ + uri: 'http://localhost:8378/1/login?username=test&password=moon-y', json: true, headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', }, - }, (error, response, body) => { - let session2 = body.sessionToken; - - //Ensure both session tokens are in the cache - Parse.Cloud.run('checkStaleUser') - .then(() => { - request.post({ - url: 'http://localhost:8378/1/functions/checkStaleUser', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Session-Token': session2, - } - }, (error, response, body) => { - Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)]) - .then(cachedVals => { - expect(cachedVals[0].objectId).toEqual(user.id); - expect(cachedVals[1].objectId).toEqual(user.id); - - //Change with session 1 and then read with session 2. - user.set('data', 'second data'); - user.save() - .then(() => { - request.post({ - url: 'http://localhost:8378/1/functions/checkStaleUser', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Session-Token': session2, - } - }, (error, response, body) => { - expect(body.result).toEqual('second data'); - done(); - }) - }); - }); - }); - }); - }); + }) + }) + .then(body => { + session2 = body.sessionToken; + + //Ensure both session tokens are in the cache + return Parse.Cloud.run('checkStaleUser') + }) + .then(() => rp({ + method: 'POST', + uri: 'http://localhost:8378/1/functions/checkStaleUser', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': session2, + } + })) + .then(() => Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)])) + .then(cachedVals => { + expect(cachedVals[0].objectId).toEqual(user.id); + expect(cachedVals[1].objectId).toEqual(user.id); + + //Change with session 1 and then read with session 2. + user.set('data', 'second data'); + return user.save() + }) + .then(() => rp({ + method: 'POST', + uri: 'http://localhost:8378/1/functions/checkStaleUser', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': session2, + } + })) + .then(body => { + expect(body.result).toEqual('second data'); + done(); + }) + .catch(error => { + fail(JSON.stringify(error)); + done(); }); }); diff --git a/spec/FileLoggerAdapter.spec.js b/spec/FileLoggerAdapter.spec.js index f259422df6..2501a1218d 100644 --- a/spec/FileLoggerAdapter.spec.js +++ b/spec/FileLoggerAdapter.spec.js @@ -52,9 +52,9 @@ describe('error logs', () => { describe('verbose logs', () => { it("mask sensitive information in _User class", (done) => { - let customConfig = Object.assign({}, defaultConfiguration, {verbose: true}); - setServerConfiguration(customConfig); - createTestUser().then(() => { + reconfigureServer({ verbose: true }) + .then(() => createTestUser()) + .then(() => { let fileLoggerAdapter = new FileLoggerAdapter(); return fileLoggerAdapter.query({ from: new Date(Date.now() - 500), diff --git a/spec/Parse.Push.spec.js b/spec/Parse.Push.spec.js index c9b6e8ec08..59adc1d802 100644 --- a/spec/Parse.Push.spec.js +++ b/spec/Parse.Push.spec.js @@ -28,26 +28,27 @@ describe('Parse.Push', () => { } } - setServerConfiguration({ + return reconfigureServer({ appId: Parse.applicationId, masterKey: Parse.masterKey, serverURL: Parse.serverURL, push: { adapter: pushAdapter } + }) + .then(() => { + var installations = []; + while(installations.length != 10) { + var installation = new Parse.Object("_Installation"); + installation.set("installationId", "installation_"+installations.length); + installation.set("deviceToken","device_token_"+installations.length) + installation.set("badge", installations.length); + installation.set("originalBadge", installations.length); + installation.set("deviceType", "ios"); + installations.push(installation); + } + return Parse.Object.saveAll(installations); }); - - var installations = []; - while(installations.length != 10) { - var installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_"+installations.length); - installation.set("deviceToken","device_token_"+installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); - installations.push(installation); - } - return Parse.Object.saveAll(installations); } it('should properly send push', (done) => { @@ -110,7 +111,7 @@ describe('Parse.Push', () => { 'X-Parse-Application-Id': 'test', }, }, (error, response, body) => { - expect(body.results.length).toEqual(0); + expect(body.error).toEqual('unauthorized'); done(); }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 2ab82255ae..ee866085c9 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -51,166 +51,152 @@ describe('miscellaneous', function() { }); it('fail to create a duplicate username', done => { - setServerConfiguration({ - ...defaultConfiguration, - __indexBuildCompletionCallbackForTests: promise => { - promise.then(() => { - let numCreated = 0; - let numFailed = 0; - let p1 = createTestUser(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - let p2 = createTestUser(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - Parse.Promise.all([p1, p2]) - .then(() => { - fail('one of the users should not have been created'); - done(); - }) - .catch(done); - }); - }, + reconfigureServer({}) + .then(() => { + let numCreated = 0; + let numFailed = 0; + let p1 = createTestUser(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + let p2 = createTestUser(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); }); }); it('ensure that email is uniquely indexed', done => { - setServerConfiguration({ - ...defaultConfiguration, - __indexBuildCompletionCallbackForTests: promise => { - let numCreated = 0; - let numFailed = 0; - - let user1 = new Parse.User(); - user1.setPassword('asdf'); - user1.setUsername('u1'); - user1.setEmail('dupe@dupe.dupe'); - let p1 = user1.signUp(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }, error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); + reconfigureServer({}) + .then(() => { + let numCreated = 0; + let numFailed = 0; + + let user1 = new Parse.User(); + user1.setPassword('asdf'); + user1.setUsername('u1'); + user1.setEmail('dupe@dupe.dupe'); + let p1 = user1.signUp(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }, error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); - let user2 = new Parse.User(); - user2.setPassword('asdf'); - user2.setUsername('u2'); - user2.setEmail('dupe@dupe.dupe'); - let p2 = user2.signUp(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }, error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); + let user2 = new Parse.User(); + user2.setPassword('asdf'); + user2.setUsername('u2'); + user2.setEmail('dupe@dupe.dupe'); + let p2 = user2.signUp(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }, error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); - Parse.Promise.all([p1, p2]) - .then(() => { - fail('one of the users should not have been created'); - done(); - }) - .catch(done); - }, + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); }); }); it('ensure that if people already have duplicate users, they can still sign up new users', done => { - setServerConfiguration({ - ...defaultConfiguration, - __indexBuildCompletionCallbackForTests: promise => { - // Remove existing data to clear out unique index - promise.then(TestUtils.destroyAllDataPermanently) - .then(() => { - let adapter = new MongoStorageAdapter({ - uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', - collectionPrefix: 'test_', - }); - adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) - .then(() => adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('zxcv'); - return user.signUp(); - }) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('u'); - user.signUp() - .catch(error => { - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - done(); - }); - }) - .catch(error => { - fail(JSON.stringify(error)); - done(); - }); - }, () => { - fail('destroyAllDataPermanently failed') + reconfigureServer({}) + // Remove existing data to clear out unique index + .then(TestUtils.destroyAllDataPermanently) + .then(() => { + let adapter = new MongoStorageAdapter({ + uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', + collectionPrefix: 'test_', + }); + adapter.createObject('_User', { objectId: 'x', username: 'u' }, requiredUserFields) + .then(() => adapter.createObject('_User', { objectId: 'y', username: 'u' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('u'); + user.signUp() + .catch(error => { + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); done(); }); - }, - }) + }) + .catch(error => { + fail(JSON.stringify(error)); + done(); + }); + }, () => { + fail('destroyAllDataPermanently failed') + done(); + }); }); it('ensure that if people already have duplicate emails, they can still sign up new users', done => { - setServerConfiguration({ - ...defaultConfiguration, - __indexBuildCompletionCallbackForTests: promise => { - // Wipe out existing database with unique index so we can create a duplicate user - promise.then(TestUtils.destroyAllDataPermanently) - .then(() => { - let adapter = new MongoStorageAdapter({ - uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', - collectionPrefix: 'test_', - }); - adapter.createObject('_User', { objectId: 'x', email: 'a@b.c' }, requiredUserFields) - .then(() => adapter.createObject('_User', { objectId: 'y', email: 'a@b.c' }, requiredUserFields)) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('qqq'); - user.setEmail('unique@unique.unique'); - return user.signUp(); - }) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('www'); - user.setEmail('a@b.c'); - user.signUp() - .catch(error => { - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - done(); - }); - }) - .catch(error => { - fail(JSON.stringify(error)); - done(); - }); - }) - } + reconfigureServer({}) + // Wipe out existing database with unique index so we can create a duplicate user + .then(TestUtils.destroyAllDataPermanently) + .then(() => { + let adapter = new MongoStorageAdapter({ + uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', + collectionPrefix: 'test_', + }); + adapter.createObject('_User', { objectId: 'x', email: 'a@b.c' }, requiredUserFields) + .then(() => adapter.createObject('_User', { objectId: 'y', email: 'a@b.c' }, requiredUserFields)) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('qqq'); + user.setEmail('unique@unique.unique'); + return user.signUp(); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('www'); + user.setEmail('a@b.c'); + user.signUp() + .catch(error => { + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + done(); + }); + }) + .catch(error => { + fail(JSON.stringify(error)); + done(); + }); }); }); diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index 7f5696cd93..1a1349dae2 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -348,28 +348,30 @@ describe('Hooks', () => { }); it("should not pass X-Parse-Webhook-Key if not provided", (done) => { - setServerConfiguration(Object.assign({}, defaultConfiguration, { webhookKey: undefined })); - app.post("/ExpectingKeyAlso", function(req, res) { - if (req.get('X-Parse-Webhook-Key') === 'hook') { - res.json({success: "correct key provided"}); - } else { - res.json({error: "incorrect key provided"}); - } - }); - - Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKeyAlso").then(function(){ - return Parse.Cloud.run("SOME_TEST_FUNCTION") - }, (err) => { - console.error(err); - fail("Should not fail creating a function"); - done(); - }).then(function(res){ - fail("Should not succeed calling that function"); - done(); - }, (err) => { - expect(err.code).toBe(141); - expect(err.message).toEqual("incorrect key provided"); - done(); + reconfigureServer({ webhookKey: undefined }) + .then(() => { + app.post("/ExpectingKeyAlso", function(req, res) { + if (req.get('X-Parse-Webhook-Key') === 'hook') { + res.json({success: "correct key provided"}); + } else { + res.json({error: "incorrect key provided"}); + } + }); + + Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL+"/ExpectingKeyAlso").then(function(){ + return Parse.Cloud.run("SOME_TEST_FUNCTION") + }, (err) => { + console.error(err); + fail("Should not fail creating a function"); + done(); + }).then(function(res){ + fail("Should not succeed calling that function"); + done(); + }, (err) => { + expect(err.code).toBe(141); + expect(err.message).toEqual("incorrect key provided"); + done(); + }); }); }); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index f333a714e1..1131e95c0c 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2384,35 +2384,32 @@ describe('Parse.User testing', () => { }); it('should not revoke session tokens if the server is configures to not revoke session tokens', done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - revokeSessionOnPasswordReset: false, - }) - request.post({ - url: 'http://localhost:8378/1/classes/_User', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} - }, (err, res, body) => { - Parse.User.become(body.sessionToken) - .then(user => { - let obj = new Parse.Object('TestObject'); - obj.setACL(new Parse.ACL(user)); - return obj.save() - .then(() => { - // Change password, revoking session - user.set('username', 'no longer anonymous'); - user.set('password', 'password'); - return user.save() + reconfigureServer({ revokeSessionOnPasswordReset: false }) + .then(() => { + request.post({ + url: 'http://localhost:8378/1/classes/_User', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} + }, (err, res, body) => { + Parse.User.become(body.sessionToken) + .then(user => { + let obj = new Parse.Object('TestObject'); + obj.setACL(new Parse.ACL(user)); + return obj.save() + .then(() => { + // Change password, revoking session + user.set('username', 'no longer anonymous'); + user.set('password', 'password'); + return user.save() + }) + .then(() => obj.fetch()) + // fetch should succeed as we still have our session token + .then(done, fail); }) - .then(() => obj.fetch()) - // fetch should succeed as we still have our session token - .then(done, fail); - }) + }); }); }) }); diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 506ea69f71..26b54438bb 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -10,22 +10,16 @@ describe("public API", () => { }); it("should get choose_password", (done) => { - setServerConfiguration({ - ...defaultConfiguration, - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', publicServerURL: 'http://localhost:8378/1', - masterKey: 'test', - fileKey: 'test', }) - request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => { - expect(httpResponse.statusCode).toBe(200); - done(); - }); + .then(() => { + request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse, body) => { + expect(httpResponse.statusCode).toBe(200); + done(); + }); + }) }); it("should get verify_email_success.html", (done) => { @@ -45,18 +39,8 @@ describe("public API", () => { describe("public API without publicServerURL", () => { beforeEach(done => { - setServerConfiguration({ - ...defaultConfiguration, - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', - }); - done(); + reconfigureServer({ appName: 'unused' }) + .then(done, fail); }) it("should get 404 on verify_email", (done) => { request('http://localhost:8378/1/apps/test/verify_email', (err, httpResponse, body) => { diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 64960f7436..782a3921dc 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -6,17 +6,8 @@ let Config = require("../src/Config"); describe("Custom Pages Configuration", () => { it("should set the custom pages", (done) => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', customPages: { invalidLink: "myInvalidLink", verifyEmailSuccess: "myVerifyEmailSuccess", @@ -24,17 +15,17 @@ describe("Custom Pages Configuration", () => { passwordResetSuccess: "myPasswordResetSuccess" }, publicServerURL: "https://my.public.server.com/1" + }) + .then(() => { + var config = new Config("test"); + expect(config.invalidLinkURL).toEqual("myInvalidLink"); + expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess"); + expect(config.choosePasswordURL).toEqual("myChoosePassword"); + expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess"); + expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email"); + expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset"); + done(); }); - - var config = new Config("test"); - - expect(config.invalidLinkURL).toEqual("myInvalidLink"); - expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess"); - expect(config.choosePasswordURL).toEqual("myChoosePassword"); - expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess"); - expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email"); - expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset"); - done(); }); }); @@ -45,39 +36,32 @@ describe("Email Verification", () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => Promise.resolve() } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - spyOn(emailAdapter, 'sendVerificationEmail'); - var user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.setEmail('testIfEnabled@parse.com'); - user.signUp(null, { - success: function(user) { - expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); - user.fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(false); + }) + .then(() => { + spyOn(emailAdapter, 'sendVerificationEmail'); + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.setEmail('testIfEnabled@parse.com'); + user.signUp(null, { + success: function(user) { + expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); + user.fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(false); + done(); + }); + }, + error: function(userAgain, error) { + fail('Failed to save user'); done(); - }); - }, - error: function(userAgain, error) { - fail('Failed to save user'); - done(); - } + } + }); }); }); @@ -87,38 +71,31 @@ describe("Email Verification", () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => Promise.resolve() } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - spyOn(emailAdapter, 'sendVerificationEmail'); - var user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.signUp(null, { - success: function(user) { - expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); - user.fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(undefined); + }) + .then(() => { + spyOn(emailAdapter, 'sendVerificationEmail'); + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.signUp(null, { + success: function(user) { + expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); + user.fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(undefined); + done(); + }); + }, + error: function(userAgain, error) { + fail('Failed to save user'); done(); - }); - }, - error: function(userAgain, error) { - fail('Failed to save user'); - done(); - } + } + }); }); }); @@ -128,47 +105,40 @@ describe("Email Verification", () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => Promise.resolve() } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - spyOn(emailAdapter, 'sendVerificationEmail'); - var user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.signUp(null, { - success: function(user) { - expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); - user.fetch() - .then((user) => { - user.set("email", "testWhenUpdating@parse.com"); - return user.save(); - }).then((user) => { - return user.fetch(); - }).then(() => { - expect(user.get('emailVerified')).toEqual(false); - // Wait as on update email, we need to fetch the username - setTimeout(function(){ - expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); - done(); - }, 200); - }); - }, - error: function(userAgain, error) { - fail('Failed to save user'); - done(); - } + }) + .then(() => { + spyOn(emailAdapter, 'sendVerificationEmail'); + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.signUp(null, { + success: function(user) { + expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); + user.fetch() + .then((user) => { + user.set("email", "testWhenUpdating@parse.com"); + return user.save(); + }).then((user) => { + return user.fetch(); + }).then(() => { + expect(user.get('emailVerified')).toEqual(false); + // Wait as on update email, we need to fetch the username + setTimeout(function(){ + expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); + done(); + }, 200); + }); + }, + error: function(userAgain, error) { + fail('Failed to save user'); + done(); + } + }); }); }); @@ -178,51 +148,44 @@ describe("Email Verification", () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => Promise.resolve() } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => { - expect(options.link).not.toBeNull(); - expect(options.link).not.toMatch(/token=undefined/); - Promise.resolve(); - }); - var user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.signUp(null, { - success: function(user) { - expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); - user.fetch() - .then((user) => { - user.set("email", "testValidLinkWhenUpdating@parse.com"); - return user.save(); - }).then((user) => { - return user.fetch(); - }).then(() => { - expect(user.get('emailVerified')).toEqual(false); - // Wait as on update email, we need to fetch the username - setTimeout(function(){ - expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); - done(); - }, 200); - }); - }, - error: function(userAgain, error) { - fail('Failed to save user'); - done(); - } + }) + .then(() => { + spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => { + expect(options.link).not.toBeNull(); + expect(options.link).not.toMatch(/token=undefined/); + Promise.resolve(); + }); + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.signUp(null, { + success: function(user) { + expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); + user.fetch() + .then((user) => { + user.set("email", "testValidLinkWhenUpdating@parse.com"); + return user.save(); + }).then((user) => { + return user.fetch(); + }).then(() => { + expect(user.get('emailVerified')).toEqual(false); + // Wait as on update email, we need to fetch the username + setTimeout(function(){ + expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); + done(); + }, 200); + }); + }, + error: function(userAgain, error) { + fail('Failed to save user'); + done(); + } + }); }); }); @@ -242,60 +205,44 @@ describe("Email Verification", () => { return Promise.resolve(); } } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'My Cool App', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - var user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testSendSimpleAdapter@parse.com"); - user.signUp(null, { - success: function(user) { - expect(calls).toBe(1); - user.fetch() - .then((user) => { - return user.save(); - }).then((user) => { - return Parse.User.requestPasswordReset("testSendSimpleAdapter@parse.com").catch((err) => { - fail('Should not fail requesting a password'); + }) + .then(() => { + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.set("email", "testSendSimpleAdapter@parse.com"); + user.signUp(null, { + success: function(user) { + expect(calls).toBe(1); + user.fetch() + .then((user) => { + return user.save(); + }).then((user) => { + return Parse.User.requestPasswordReset("testSendSimpleAdapter@parse.com").catch((err) => { + fail('Should not fail requesting a password'); + done(); + }) + }).then(() => { + expect(calls).toBe(2); done(); - }) - }).then(() => { - expect(calls).toBe(2); + }); + }, + error: function(userAgain, error) { + fail('Failed to save user'); done(); - }); - }, - error: function(userAgain, error) { - fail('Failed to save user'); - done(); - } + } + }); }); }); it('fails if you include an emailAdapter, set verifyUserEmails to false, dont set a publicServerURL, and try to send a password reset email (regression test for #1649)', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: false, emailAdapter: MockEmailAdapterWithOptions({ fromAddress: 'parse@example.com', @@ -303,20 +250,21 @@ describe("Email Verification", () => { domain: 'd', }), }) - - let user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testInvalidConfig@parse.com"); - user.signUp(null) - .then(user => Parse.User.requestPasswordReset("testInvalidConfig@parse.com")) - .then(result => { - console.log(result); - fail('sending password reset email should not have succeeded'); - done(); - }, error => { - expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.') - done(); + .then(() => { + let user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.set("email", "testInvalidConfig@parse.com"); + user.signUp(null) + .then(user => Parse.User.requestPasswordReset("testInvalidConfig@parse.com")) + .then(result => { + console.log(result); + fail('sending password reset email should not have succeeded'); + done(); + }, error => { + expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.') + done(); + }); }); }); @@ -326,37 +274,30 @@ describe("Email Verification", () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => Promise.resolve() } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: false, emailAdapter: emailAdapter, - }); - spyOn(emailAdapter, 'sendVerificationEmail'); - var user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.signUp(null, { - success: function(user) { - user.fetch() - .then(() => { - expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0); - expect(user.get('emailVerified')).toEqual(undefined); + }) + .then(() => { + spyOn(emailAdapter, 'sendVerificationEmail'); + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.signUp(null, { + success: function(user) { + user.fetch() + .then(() => { + expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0); + expect(user.get('emailVerified')).toEqual(undefined); + done(); + }); + }, + error: function(userAgain, error) { + fail('Failed to save user'); done(); - }); - }, - error: function(userAgain, error) { - fail('Failed to save user'); - done(); - } + } + }); }); }); @@ -370,31 +311,24 @@ describe("Email Verification", () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => {} } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - var user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set('email', 'user@parse.com'); - user.signUp(null, { - success: () => {}, - error: function(userAgain, error) { - fail('Failed to save user'); - done(); - } + }) + .then(() => { + var user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.set('email', 'user@parse.com'); + user.signUp(null, { + success: () => {}, + error: function(userAgain, error) { + fail('Failed to save user'); + done(); + } + }); }); }) @@ -421,39 +355,23 @@ describe("Email Verification", () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => {} } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" + }) + .then(() => { + user.setPassword("asdf"); + user.setUsername("user"); + user.set('email', 'user@parse.com'); + user.signUp(); }); - user.setPassword("asdf"); - user.setUsername("user"); - user.set('email', 'user@parse.com'); - user.signUp(); }); it('redirects you to invalid link if you try to verify email incorrecly', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { sendVerificationEmail: () => Promise.resolve(), @@ -461,28 +379,21 @@ describe("Email Verification", () => { sendMail: () => {} }, publicServerURL: "http://localhost:8378/1" - }); - request.get('http://localhost:8378/1/apps/test/verify_email', { - followRedirect: false, - }, (error, response, body) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); - done() + }) + .then(() => { + request.get('http://localhost:8378/1/apps/test/verify_email', { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + done() + }); }); }); it('redirects you to invalid link if you try to validate a nonexistant users email', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { sendVerificationEmail: () => Promise.resolve(), @@ -490,13 +401,15 @@ describe("Email Verification", () => { sendMail: () => {} }, publicServerURL: "http://localhost:8378/1" - }); - request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', { - followRedirect: false, - }, (error, response, body) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); - done(); + }) + .then(() => { + request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + done(); + }); }); }); @@ -519,30 +432,23 @@ describe("Email Verification", () => { sendPasswordResetEmail: () => Promise.resolve(), sendMail: () => {} } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set('email', 'user@parse.com'); - user.signUp(null, { - success: () => {}, - error: function(userAgain, error) { - fail('Failed to save user'); - done(); - } + }) + .then(() => { + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.set('email', 'user@parse.com'); + user.signUp(null, { + success: () => {}, + error: function(userAgain, error) { + fail('Failed to save user'); + done(); + } + }); }); }); }); @@ -570,47 +476,31 @@ describe("Password Reset", () => { }, sendMail: () => {} } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - user.setPassword("asdf"); - user.setUsername("zxcv+zxcv"); - user.set('email', 'user@parse.com'); - user.signUp().then(() => { - Parse.User.requestPasswordReset('user@parse.com', { - error: (err) => { - console.error(err); - fail("Should not fail requesting a password"); - done(); - } + }) + .then(() => { + user.setPassword("asdf"); + user.setUsername("zxcv+zxcv"); + user.set('email', 'user@parse.com'); + user.signUp().then(() => { + Parse.User.requestPasswordReset('user@parse.com', { + error: (err) => { + console.error(err); + fail("Should not fail requesting a password"); + done(); + } + }); }); }); }); it('redirects you to invalid link if you try to request password for a nonexistant users email', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { sendVerificationEmail: () => Promise.resolve(), @@ -618,13 +508,15 @@ describe("Password Reset", () => { sendMail: () => {} }, publicServerURL: "http://localhost:8378/1" - }); - request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', { - followRedirect: false, - }, (error, response, body) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); - done(); + }) + .then(() => { + request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', { + followRedirect: false, + }, (error, response, body) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + done(); + }); }); }); @@ -688,31 +580,24 @@ describe("Password Reset", () => { }, sendMail: () => {} } - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: emailAdapter, publicServerURL: "http://localhost:8378/1" - }); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set('email', 'user@parse.com'); - user.signUp().then(() => { - Parse.User.requestPasswordReset('user@parse.com', { - error: (err) => { - console.error(err); - fail("Should not fail"); - done(); - } + }) + .then(() => { + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.set('email', 'user@parse.com'); + user.signUp().then(() => { + Parse.User.requestPasswordReset('user@parse.com', { + error: (err) => { + console.error(err); + fail("Should not fail"); + done(); + } + }); }); }); }); diff --git a/spec/helper.js b/spec/helper.js index 78a6758cf7..dee3c196f8 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,3 +1,4 @@ +"use strict" // Sets up a Parse API server for testing. jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 3000; @@ -56,15 +57,19 @@ var server = app.listen(port); delete defaultConfiguration.cloud; // Allows testing specific configurations of Parse Server -const setServerConfiguration = configuration => { - server.close(); - cache.clear(); - app = express(); - api = new ParseServer(configuration); - app.use('/1', api); - server = app.listen(port); -}; - +const reconfigureServer = changedConfiguration => { + return new Promise((resolve, reject) => { + let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { + __indexBuildCompletionCallbackForTests: promise => promise.then(resolve, reject), + }); + server.close(); + cache.clear(); + app = express(); + api = new ParseServer(newConfiguration); + app.use('/1', api); + server = app.listen(port); + }); +} // Set up a Parse client to talk to our test API server var Parse = require('parse/node'); @@ -74,11 +79,11 @@ Parse.serverURL = 'http://localhost:' + port + '/1'; // TODO: update tests to work in an A+ way Parse.Promise.disableAPlusCompliant(); -beforeEach(function(done) { +beforeEach(done => { Parse.User.enableUnsafeCurrentUser(); TestUtils.destroyAllDataPermanently() + .then(() => reconfigureServer()) .then(() => { - setServerConfiguration(defaultConfiguration); Parse.initialize('test', 'test', 'test'); Parse.serverURL = 'http://localhost:' + port + '/1'; done(); @@ -266,7 +271,7 @@ global.expectError = expectError; global.arrayContains = arrayContains; global.jequal = jequal; global.range = range; -global.setServerConfiguration = setServerConfiguration; +global.reconfigureServer = reconfigureServer; global.defaultConfiguration = defaultConfiguration; // LiveQuery test setting diff --git a/spec/index.spec.js b/spec/index.spec.js index e0132a7e3b..2f549846e9 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -10,9 +10,10 @@ const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageA describe('server', () => { it('requires a master key and app id', done => { - expect(setServerConfiguration.bind(undefined, { })).toThrow('You must provide an appId!'); - expect(setServerConfiguration.bind(undefined, { appId: 'myId' })).toThrow('You must provide a masterKey!'); - expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!'); + fail('TODO: figrue out async'); + /*expect(() => reconfigureServer({})).toThrow('You must provide an appId!'); + expect(() => reconfigureServer({ appId: 'myId' })).toThrow('You must provide a masterKey!'); + expect(() => reconfigureServer({ appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!');*/ done(); }); @@ -41,49 +42,31 @@ describe('server', () => { }); it('fails if database is unreachable', done => { - setServerConfiguration({ - ...defaultConfiguration, - databaseAdapter: new MongoStorageAdapter({ - uri: 'mongodb://fake:fake@localhost:43605/drew3', - }), - serverURL: 'http://localhost:8378/1', - appId: 'test', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', - }); - //Need to use rest api because saving via JS SDK results in fail() not getting called - request.post({ - url: 'http://localhost:8378/1/classes/NewClass', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - }, - body: {}, - json: true, - }, (error, response, body) => { - expect(response.statusCode).toEqual(500); - expect(body.code).toEqual(1); - expect(body.message).toEqual('Internal server error.'); - done(); + reconfigureServer({ + databaseAdapter: new MongoStorageAdapter({ uri: 'mongodb://fake:fake@localhost:43605/drew3' }), + }) + .then(() => { + //Need to use rest api because saving via JS SDK results in fail() not getting called + request.post({ + url: 'http://localhost:8378/1/classes/NewClass', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: {}, + json: true, + }, (error, response, body) => { + expect(response.statusCode).toEqual(500); + expect(body.code).toEqual(1); + expect(body.message).toEqual('Internal server error.'); + done(); + }); }); }); it('can load email adapter via object', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: MockEmailAdapterWithOptions({ fromAddress: 'parse@example.com', @@ -91,22 +74,12 @@ describe('server', () => { domain: 'd', }), publicServerURL: 'http://localhost:8378/1' - }); - done(); + }).then(done, fail); }); it('can load email adapter via class', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { class: MockEmailAdapterWithOptions, @@ -117,22 +90,12 @@ describe('server', () => { } }, publicServerURL: 'http://localhost:8378/1' - }); - done(); + }).then(done, fail); }); it('can load email adapter via module name', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { module: 'parse-server-simple-mailgun-adapter', @@ -143,12 +106,12 @@ describe('server', () => { } }, publicServerURL: 'http://localhost:8378/1' - }); - done(); + }).then(done, fail); }); it('can load email adapter via only module name', done => { - expect(() => setServerConfiguration({ + fail('TODO: figure out async'); + /*expect(() => reconfigureServer({ ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', @@ -162,12 +125,13 @@ describe('server', () => { verifyUserEmails: true, emailAdapter: 'parse-server-simple-mailgun-adapter', publicServerURL: 'http://localhost:8378/1' - })).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.'); + })).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');*/ done(); }); it('throws if you initialize email adapter incorrecly', done => { - expect(() => setServerConfiguration({ + fail('TODO: figure out async'); + /*expect(() => setServerConfiguration({ ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', @@ -186,7 +150,7 @@ describe('server', () => { } }, publicServerURL: 'http://localhost:8378/1' - })).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.'); + })).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');*/ done(); }); @@ -210,27 +174,31 @@ describe('server', () => { appId: "aTestApp", masterKey: "aTestMasterKey", serverURL: "http://localhost:12666/parse", - }); + __indexBuildCompletionCallbackForTests: promise => { + promise + .then(() => { + expect(Parse.applicationId).toEqual("aTestApp"); + var app = express(); + app.use('/parse', parseServer.app); - expect(Parse.applicationId).toEqual("aTestApp"); - var app = express(); - app.use('/parse', parseServer.app); - - var server = app.listen(12666); - var obj = new Parse.Object("AnObject"); - var objId; - obj.save().then((obj) => { - objId = obj.id; - var q = new Parse.Query("AnObject"); - return q.first(); - }).then((obj) => { - expect(obj.id).toEqual(objId); - server.close(); - done(); - }).fail((err) => { - server.close(); - done(); - }) + var server = app.listen(12666); + var obj = new Parse.Object("AnObject"); + var objId; + obj.save().then((obj) => { + objId = obj.id; + var q = new Parse.Query("AnObject"); + return q.first(); + }).then((obj) => { + expect(obj.id).toEqual(objId); + server.close(); + done(); + }).fail((err) => { + server.close(); + done(); + }) + }); + }, + }); }); it('can create a parse-server', done => { @@ -239,27 +207,31 @@ describe('server', () => { appId: "anOtherTestApp", masterKey: "anOtherTestMasterKey", serverURL: "http://localhost:12667/parse", - }); - - expect(Parse.applicationId).toEqual("anOtherTestApp"); - var app = express(); - app.use('/parse', parseServer); + __indexBuildCompletionCallbackForTests: promise => { + promise + .then(() => { + expect(Parse.applicationId).toEqual("anOtherTestApp"); + var app = express(); + app.use('/parse', parseServer); - var server = app.listen(12667); - var obj = new Parse.Object("AnObject"); - var objId; - obj.save().then((obj) => { - objId = obj.id; - var q = new Parse.Query("AnObject"); - return q.first(); - }).then((obj) => { - expect(obj.id).toEqual(objId); - server.close(); - done(); - }).fail((err) => { - server.close(); - done(); - }) + var server = app.listen(12667); + var obj = new Parse.Object("AnObject"); + var objId; + obj.save().then((obj) => { + objId = obj.id; + var q = new Parse.Query("AnObject"); + return q.first(); + }).then((obj) => { + expect(obj.id).toEqual(objId); + server.close(); + done(); + }).fail((err) => { + server.close(); + done(); + }) + }); + }, + }); }); it('has createLiveQueryServer', done => { @@ -278,43 +250,38 @@ describe('server', () => { }); it('properly gives publicServerURL when set', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - publicServerURL: 'https://myserver.com/1' + reconfigureServer({ publicServerURL: 'https://myserver.com/1' }) + .then(() => { + var config = new Config('test', 'http://localhost:8378/1'); + expect(config.mount).toEqual('https://myserver.com/1'); + done(); }); - var config = new Config('test', 'http://localhost:8378/1'); - expect(config.mount).toEqual('https://myserver.com/1'); - done(); }); it('properly removes trailing slash in mount', done => { - setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test' + reconfigureServer({}) + .then(() => { + var config = new Config('test', 'http://localhost:8378/1/'); + expect(config.mount).toEqual('http://localhost:8378/1'); + done(); }); - var config = new Config('test', 'http://localhost:8378/1/'); - expect(config.mount).toEqual('http://localhost:8378/1'); - done(); }); it('should throw when getting invalid mount', done => { - expect(() => setServerConfiguration({ + fail('TODO: figure out async') + /*expect(() => setServerConfiguration({ ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', masterKey: 'test', publicServerURL: 'blabla:/some' - }) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://"); + }) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://");*/ done(); }); it('fails if the session length is not a number', (done) => { - expect(() => setServerConfiguration({ + fail('TODO: figure out async') + /*expect(() => setServerConfiguration({ ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', @@ -322,12 +289,14 @@ describe('server', () => { javascriptKey: 'test', masterKey: 'test', sessionLength: 'test' - })).toThrow('Session length must be a valid number.'); + })).toThrow('Session length must be a valid number.');*/ done(); }); it('fails if the session length is less than or equal to 0', (done) => { - expect(() => setServerConfiguration({ + fail('TODO: figure out async') + + /*expect(() => setServerConfiguration({ ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', @@ -345,12 +314,13 @@ describe('server', () => { javascriptKey: 'test', masterKey: 'test', sessionLength: '0' - })).toThrow('Session length must be a value greater than 0.'); + })).toThrow('Session length must be a value greater than 0.');*/ done(); }); it('ignores the session length when expireInactiveSessions set to false', (done) => { - expect(() => setServerConfiguration({ + fail('TODO: figure out async') + /*expect(() => setServerConfiguration({ ...defaultConfiguration, serverURL: 'http://localhost:8378/1', appId: 'test', @@ -370,12 +340,14 @@ describe('server', () => { masterKey: 'test', sessionLength: '0', expireInactiveSessions: false - })).not.toThrow(); + })).not.toThrow();*/ done(); }) it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => { - expect(() => setServerConfiguration({ revokeSessionOnPasswordReset: 'non-bool' })).toThrow(); + fail('TODO: figure out async') + + /*expect(() => setServerConfiguration({ revokeSessionOnPasswordReset: 'non-bool' })).toThrow();*/ done(); }); }); diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js index 5305b13531..d720fb99da 100644 --- a/src/Adapters/AdapterLoader.js +++ b/src/Adapters/AdapterLoader.js @@ -1,6 +1,5 @@ export function loadAdapter(adapter, defaultAdapter, options) { - if (!adapter) - { + if (!adapter) { if (!defaultAdapter) { return options; } From 74036ad073b5e4e4314b328661871a81bec5a63c Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 13:30:10 -0700 Subject: [PATCH 13/32] Fix more tests --- spec/helper.js | 17 ++--- spec/index.spec.js | 151 ++++++++++++++++----------------------------- 2 files changed, 62 insertions(+), 106 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index dee3c196f8..02d869463a 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -80,7 +80,14 @@ Parse.serverURL = 'http://localhost:' + port + '/1'; Parse.Promise.disableAPlusCompliant(); beforeEach(done => { - Parse.User.enableUnsafeCurrentUser(); + try { + Parse.User.enableUnsafeCurrentUser(); + } catch (error) { + if (error !== 'You need to call Parse.initialize before using Parse.') { + console.log(error); + throw error; + } + } TestUtils.destroyAllDataPermanently() .then(() => reconfigureServer()) .then(() => { @@ -108,13 +115,7 @@ afterEach(function(done) { }); }) .then(() => Parse.User.logOut()) - .then(() => { - return TestUtils.destroyAllDataPermanently(); - }).then(done, - error => { - console.log('error in clearData', error); - done(); - }); + .then(done); }); var TestObject = Parse.Object.extend({ diff --git a/spec/index.spec.js b/spec/index.spec.js index 2f549846e9..aff00dc0f1 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -10,11 +10,19 @@ const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageA describe('server', () => { it('requires a master key and app id', done => { - fail('TODO: figrue out async'); - /*expect(() => reconfigureServer({})).toThrow('You must provide an appId!'); - expect(() => reconfigureServer({ appId: 'myId' })).toThrow('You must provide a masterKey!'); - expect(() => reconfigureServer({ appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!');*/ - done(); + reconfigureServer({ appId: undefined }) + .catch(error => { + expect(error).toEqual('You must provide an appId!'); + return reconfigureServer({ masterKey: undefined }); + }) + .catch(error => { + expect(error).toEqual('You must provide a masterKey!'); + return reconfigureServer({ serverURL: undefined }); + }) + .catch(error => { + expect(error).toEqual('You must provide a serverURL!'); + done(); + }); }); it('support http basic authentication with masterkey', done => { @@ -110,38 +118,21 @@ describe('server', () => { }); it('can load email adapter via only module name', done => { - fail('TODO: figure out async'); - /*expect(() => reconfigureServer({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: 'parse-server-simple-mailgun-adapter', publicServerURL: 'http://localhost:8378/1' - })).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');*/ - done(); + }) + .catch(error => { + expect(error).toEqual('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.'); + done(); + }); }); it('throws if you initialize email adapter incorrecly', done => { - fail('TODO: figure out async'); - /*expect(() => setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { module: 'parse-server-simple-mailgun-adapter', @@ -150,8 +141,11 @@ describe('server', () => { } }, publicServerURL: 'http://localhost:8378/1' - })).toThrow('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.');*/ - done(); + }) + .catch(error => { + expect(error).toEqual('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.'); + done(); + }); }); it('can report the server version', done => { @@ -268,86 +262,47 @@ describe('server', () => { }); it('should throw when getting invalid mount', done => { - fail('TODO: figure out async') - /*expect(() => setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - publicServerURL: 'blabla:/some' - }) ).toThrow("publicServerURL should be a valid HTTPS URL starting with https://");*/ - done(); + reconfigureServer({ publicServerURL: 'blabla:/some' }) + .then(error => { + expect(error).toEqual('publicServerURL should be a valid HTTPS URL starting with https://') + done(); + }); }); - it('fails if the session length is not a number', (done) => { - fail('TODO: figure out async') - /*expect(() => setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - masterKey: 'test', - sessionLength: 'test' - })).toThrow('Session length must be a valid number.');*/ - done(); + it('fails if the session length is not a number', done => { + reconfigureServer({ sessionLength: 'test' }) + .catch(error => { + express(error).toEqual('Session length must be a valid number.'); + done(); + }); }); - it('fails if the session length is less than or equal to 0', (done) => { - fail('TODO: figure out async') - - /*expect(() => setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - masterKey: 'test', - sessionLength: '-33' - })).toThrow('Session length must be a value greater than 0.'); - - expect(() => setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - masterKey: 'test', - sessionLength: '0' - })).toThrow('Session length must be a value greater than 0.');*/ - done(); + it('fails if the session length is less than or equal to 0', done => { + reconfigureServer({ sessionLength: '-33' }) + .catch(error => { + expect(error).toEqual('Session length must be a value greater than 0.'); + return reconfigureServer({ sessionLength: '0' }) + }) + .catch(error => { + expect(error).toEqual('Session length must be a value greater than 0.'); + done(); + }); }); it('ignores the session length when expireInactiveSessions set to false', (done) => { - fail('TODO: figure out async') - /*expect(() => setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - masterKey: 'test', + reconfigureServer({ sessionLength: '-33', expireInactiveSessions: false - })).not.toThrow(); - - expect(() => setServerConfiguration({ - ...defaultConfiguration, - serverURL: 'http://localhost:8378/1', - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - masterKey: 'test', + }) + .then(() => reconfigureServer({ sessionLength: '0', expireInactiveSessions: false - })).not.toThrow();*/ - done(); + })) + .then(done); }) it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => { - fail('TODO: figure out async') - - /*expect(() => setServerConfiguration({ revokeSessionOnPasswordReset: 'non-bool' })).toThrow();*/ - done(); + reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }) + .catch(done); }); }); From 1bc15d258b08cfb65208e2f0c893fb40e8449bbf Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 14:03:41 -0700 Subject: [PATCH 14/32] fix more tests --- spec/helper.js | 19 +++++++++++++++++-- spec/index.spec.js | 42 +++++++++++++++++++++++------------------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 02d869463a..a588ff939a 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -89,12 +89,23 @@ beforeEach(done => { } } TestUtils.destroyAllDataPermanently() + .catch(error => { + // For tests that connect to their own mongo, there won't be any data to delete. + if (error.message === 'ns not found') { + return; + } else { + throw error; + } + }) .then(() => reconfigureServer()) .then(() => { Parse.initialize('test', 'test', 'test'); Parse.serverURL = 'http://localhost:' + port + '/1'; done(); - }, fail) + }, error => { + fail(JSON.stringify(error)); + done(); + }) }); afterEach(function(done) { @@ -115,7 +126,11 @@ afterEach(function(done) { }); }) .then(() => Parse.User.logOut()) - .then(done); + .then(done) + .catch(error => { + fail(JSON.stringify(error)); + done(); + }); }); var TestObject = Parse.Object.extend({ diff --git a/spec/index.spec.js b/spec/index.spec.js index aff00dc0f1..c73928f7c0 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -162,7 +162,7 @@ describe('server', () => { }) }); - it('can create a parse-server', done => { + it('can create a parse-server v1', done => { var parseServer = new ParseServer.default({ ...defaultConfiguration, appId: "aTestApp", @@ -195,8 +195,10 @@ describe('server', () => { }); }); - it('can create a parse-server', done => { - var parseServer = ParseServer.ParseServer({ + it('can create a parse-server v2', done => { + let objId; + let server + let parseServer = ParseServer.ParseServer({ ...defaultConfiguration, appId: "anOtherTestApp", masterKey: "anOtherTestMasterKey", @@ -205,24 +207,26 @@ describe('server', () => { promise .then(() => { expect(Parse.applicationId).toEqual("anOtherTestApp"); - var app = express(); + let app = express(); app.use('/parse', parseServer); - var server = app.listen(12667); - var obj = new Parse.Object("AnObject"); - var objId; - obj.save().then((obj) => { - objId = obj.id; - var q = new Parse.Query("AnObject"); - return q.first(); - }).then((obj) => { - expect(obj.id).toEqual(objId); - server.close(); - done(); - }).fail((err) => { - server.close(); - done(); - }) + server = app.listen(12667); + let obj = new Parse.Object("AnObject"); + return obj.save() + }) + .then(obj => { + objId = obj.id; + let q = new Parse.Query("AnObject"); + return q.first(); + }) + .then(obj => { + expect(obj.id).toEqual(objId); + server.close(); + done(); + }) + .catch(error => { + fail(JSON.stringify(error)) + done(); }); }, }); From 2f2f0107a42f708379714a7c7448b91ae14fbdf9 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 14:09:44 -0700 Subject: [PATCH 15/32] Fix another test --- spec/index.spec.js | 6 ++---- src/ParseServer.js | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index c73928f7c0..b0f1415c41 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -50,10 +50,8 @@ describe('server', () => { }); it('fails if database is unreachable', done => { - reconfigureServer({ - databaseAdapter: new MongoStorageAdapter({ uri: 'mongodb://fake:fake@localhost:43605/drew3' }), - }) - .then(() => { + reconfigureServer({ databaseAdapter: new MongoStorageAdapter({ uri: 'mongodb://fake:fake@localhost:43605/drew3' }) }) + .catch(() => { //Need to use rest api because saving via JS SDK results in fail() not getting called request.post({ url: 'http://localhost:8378/1/classes/NewClass', diff --git a/src/ParseServer.js b/src/ParseServer.js index 690237e12e..16be9796cc 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -191,13 +191,13 @@ class ParseServer { let usernameUniqueness = databaseController.adapter.ensureUniqueness('_User', ['username'], requiredUserFields) .catch(error => { - log.warn('Unable to ensure uniqueness for usernames: ', error); + logger.warn('Unable to ensure uniqueness for usernames: ', error); return Promise.reject(); }); let emailUniqueness = databaseController.adapter.ensureUniqueness('_User', ['email'], requiredUserFields) .catch(error => { - log.warn('Unabled to ensure uniqueness for user email addresses: ', error); + logger.warn('Unabled to ensure uniqueness for user email addresses: ', error); return Promise.reject(); }) From 8a3e52ebf6169ad6757d433826098df3d8e0cf25 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 16:23:31 -0700 Subject: [PATCH 16/32] fix more tests --- spec/helper.js | 18 ++++++++++++------ spec/index.spec.js | 6 +++--- src/Config.js | 23 +++++++++++++++-------- src/ParseServer.js | 10 +++++----- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index a588ff939a..e837b0ab3e 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -60,12 +60,16 @@ delete defaultConfiguration.cloud; const reconfigureServer = changedConfiguration => { return new Promise((resolve, reject) => { let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { - __indexBuildCompletionCallbackForTests: promise => promise.then(resolve, reject), + __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject) }); server.close(); cache.clear(); app = express(); - api = new ParseServer(newConfiguration); + try { + api = new ParseServer(newConfiguration); + } catch(error) { + reject(error); + } app.use('/1', api); server = app.listen(port); }); @@ -84,17 +88,17 @@ beforeEach(done => { Parse.User.enableUnsafeCurrentUser(); } catch (error) { if (error !== 'You need to call Parse.initialize before using Parse.') { - console.log(error); throw error; } } TestUtils.destroyAllDataPermanently() .catch(error => { // For tests that connect to their own mongo, there won't be any data to delete. - if (error.message === 'ns not found') { + if (error.message === 'ns not found' || error.message.startsWith('connect ECONNREFUSED')) { return; } else { - throw error; + fail(error); + return; } }) .then(() => reconfigureServer()) @@ -126,7 +130,9 @@ afterEach(function(done) { }); }) .then(() => Parse.User.logOut()) - .then(done) + .then(() => { + done(); + }) .catch(error => { fail(JSON.stringify(error)); done(); diff --git a/spec/index.spec.js b/spec/index.spec.js index b0f1415c41..1bd5ebf1d3 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -265,16 +265,16 @@ describe('server', () => { it('should throw when getting invalid mount', done => { reconfigureServer({ publicServerURL: 'blabla:/some' }) - .then(error => { + .catch(error => { expect(error).toEqual('publicServerURL should be a valid HTTPS URL starting with https://') done(); - }); + }) }); it('fails if the session length is not a number', done => { reconfigureServer({ sessionLength: 'test' }) .catch(error => { - express(error).toEqual('Session length must be a valid number.'); + expect(error).toEqual('Session length must be a valid number.'); done(); }); }); diff --git a/src/Config.js b/src/Config.js index 861f46e266..ffdf078cc2 100644 --- a/src/Config.js +++ b/src/Config.js @@ -54,24 +54,31 @@ export class Config { this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset; } - static validate(options) { + static validate({ + verifyUserEmails, + appName, + publicServerURL, + revokeSessionOnPasswordReset, + expireInactiveSessions, + sessionLength, + }) { this.validateEmailConfiguration({ - verifyUserEmails: options.verifyUserEmails, - appName: options.appName, - publicServerURL: options.publicServerURL + verifyUserEmails: verifyUserEmails, + appName: appName, + publicServerURL: publicServerURL }) - if (typeof options.revokeSessionOnPasswordReset !== 'boolean') { + if (typeof revokeSessionOnPasswordReset !== 'boolean') { throw 'revokeSessionOnPasswordReset must be a boolean value'; } - if (options.publicServerURL) { - if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) { + if (publicServerURL) { + if (!publicServerURL.startsWith("http://") && !publicServerURL.startsWith("https://")) { throw "publicServerURL should be a valid HTTPS URL starting with https://" } } - this.validateSessionConfiguration(options.sessionLength, options.expireInactiveSessions); + this.validateSessionConfiguration(sessionLength, expireInactiveSessions); } static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) { diff --git a/src/ParseServer.js b/src/ParseServer.js index 16be9796cc..be9b708a92 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -201,11 +201,6 @@ class ParseServer { return Promise.reject(); }) - if (process.env.TESTING) { - __indexBuildCompletionCallbackForTests(Promise.all([usernameUniqueness, emailUniqueness])); - } - - AppCache.put(appId, { masterKey: masterKey, serverURL: serverURL, @@ -245,6 +240,11 @@ class ParseServer { Config.validate(AppCache.get(appId)); this.config = AppCache.get(appId); hooksController.load(); + + // Note: Tests will start to fail if any validation happens after this is called. + if (process.env.TESTING) { + __indexBuildCompletionCallbackForTests(Promise.all([usernameUniqueness, emailUniqueness])); + } } get app() { From c2c85f7d9570377ff2a1bc22dc105d29a6a2809c Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 16:36:11 -0700 Subject: [PATCH 17/32] Fix email validation --- src/RestWrite.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 6b18dd28c8..78478025ab 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -388,15 +388,11 @@ RestWrite.prototype.transformUser = function() { if (results.length > 0) { throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); } - return; + // We updated the email, send a new validation + this.storage['sendVerificationEmail'] = true; + this.config.userController.setEmailVerifyToken(this.data); }); }) - .then(() => { - // We updated the email, send a new validation - this.storage['sendVerificationEmail'] = true; - this.config.userController.setEmailVerifyToken(this.data); - return; - }) }; RestWrite.prototype.createSessionTokenIfNeeded = function() { From 4c35ef2a07cff80a403ebda23294f4f2042995fc Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 21:39:56 -0700 Subject: [PATCH 18/32] move some stuff around --- spec/helper.js | 28 +++++++++++++++------------- spec/index.spec.js | 9 +++------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index e837b0ab3e..c850232ba1 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -36,7 +36,7 @@ var defaultConfiguration = { cert: 'prodCert.pem', key: 'prodKey.pem', production: true, - bundleId: 'bundleId' + bundleId: 'bundleId', } }, oauth: { // Override the facebook provider @@ -59,19 +59,21 @@ delete defaultConfiguration.cloud; // Allows testing specific configurations of Parse Server const reconfigureServer = changedConfiguration => { return new Promise((resolve, reject) => { - let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { - __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject) + server.close(() => { + try { + let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { + __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject) + }); + cache.clear(); + app = express(); + api = new ParseServer(newConfiguration); + app.use('/1', api); + var newServer = app.listen(port); + server = newServer; + } catch(error) { + reject(error); + } }); - server.close(); - cache.clear(); - app = express(); - try { - api = new ParseServer(newConfiguration); - } catch(error) { - reject(error); - } - app.use('/1', api); - server = app.listen(port); }); } diff --git a/spec/index.spec.js b/spec/index.spec.js index 1bd5ebf1d3..14db9ccd96 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -182,11 +182,9 @@ describe('server', () => { return q.first(); }).then((obj) => { expect(obj.id).toEqual(objId); - server.close(); - done(); + server.close(done); }).fail((err) => { - server.close(); - done(); + server.close(done); }) }); }, @@ -219,8 +217,7 @@ describe('server', () => { }) .then(obj => { expect(obj.id).toEqual(objId); - server.close(); - done(); + server.close(done); }) .catch(error => { fail(JSON.stringify(error)) From b16d22c4a06435db89faf2c99864a65b231fc9cb Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 21:48:03 -0700 Subject: [PATCH 19/32] Destroy server to ensure all connections are gone --- package.json | 1 + spec/helper.js | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 3def0684b7..7198a513f4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "redis": "^2.5.0-1", "request": "^2.65.0", "request-promise": "^3.0.0", + "server-destroy": "^1.0.1", "tv4": "^1.2.7", "winston": "^2.1.1", "winston-daily-rotate-file": "^1.0.1", diff --git a/spec/helper.js b/spec/helper.js index c850232ba1..702001f469 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -11,6 +11,7 @@ var ParseServer = require('../src/index').ParseServer; var path = require('path'); var TestUtils = require('../src/index').TestUtils; var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); +const enableDestroy = require('server-destroy'); var port = 8378; @@ -52,6 +53,7 @@ var api = new ParseServer(defaultConfiguration); var app = express(); app.use('/1', api); var server = app.listen(port); +enableDestroy(server); // Prevent reinitializing the server from clobbering Cloud Code delete defaultConfiguration.cloud; @@ -59,7 +61,7 @@ delete defaultConfiguration.cloud; // Allows testing specific configurations of Parse Server const reconfigureServer = changedConfiguration => { return new Promise((resolve, reject) => { - server.close(() => { + server.destroy(() => { try { let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject) @@ -68,8 +70,8 @@ const reconfigureServer = changedConfiguration => { app = express(); api = new ParseServer(newConfiguration); app.use('/1', api); - var newServer = app.listen(port); - server = newServer; + server = app.listen(port); + enableDestroy(server); } catch(error) { reject(error); } From f827c7364ae7ecd1e5ed1b4e8b4837c97108e829 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 22:16:28 -0700 Subject: [PATCH 20/32] Fix broken cloud code --- package.json | 1 - spec/ParseRelation.spec.js | 1 + spec/helper.js | 5 +---- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7198a513f4..3def0684b7 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "redis": "^2.5.0-1", "request": "^2.65.0", "request-promise": "^3.0.0", - "server-destroy": "^1.0.1", "tv4": "^1.2.7", "winston": "^2.1.1", "winston-daily-rotate-file": "^1.0.1", diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index 6b79743bb3..8c1996cd05 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -696,6 +696,7 @@ describe('Parse.Relation testing', () => { admins.first({ useMasterKey: true }) .then(user => { if (user) { + response.success(user); done(); } else { fail('Should have found admin user, found nothing instead'); diff --git a/spec/helper.js b/spec/helper.js index 702001f469..ca8086bff9 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -11,7 +11,6 @@ var ParseServer = require('../src/index').ParseServer; var path = require('path'); var TestUtils = require('../src/index').TestUtils; var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); -const enableDestroy = require('server-destroy'); var port = 8378; @@ -53,7 +52,6 @@ var api = new ParseServer(defaultConfiguration); var app = express(); app.use('/1', api); var server = app.listen(port); -enableDestroy(server); // Prevent reinitializing the server from clobbering Cloud Code delete defaultConfiguration.cloud; @@ -61,7 +59,7 @@ delete defaultConfiguration.cloud; // Allows testing specific configurations of Parse Server const reconfigureServer = changedConfiguration => { return new Promise((resolve, reject) => { - server.destroy(() => { + server.close(() => { try { let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject) @@ -71,7 +69,6 @@ const reconfigureServer = changedConfiguration => { api = new ParseServer(newConfiguration); app.use('/1', api); server = app.listen(port); - enableDestroy(server); } catch(error) { reject(error); } From c290c35acb0de65375db5aaaa3fb961fd973ca4e Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 22:56:37 -0700 Subject: [PATCH 21/32] Save callback to variable --- spec/helper.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index ca8086bff9..b280b0d000 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -59,7 +59,7 @@ delete defaultConfiguration.cloud; // Allows testing specific configurations of Parse Server const reconfigureServer = changedConfiguration => { return new Promise((resolve, reject) => { - server.close(() => { + const startNewServer = () => { try { let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject) @@ -72,7 +72,8 @@ const reconfigureServer = changedConfiguration => { } catch(error) { reject(error); } - }); + } + server.close(startNewServer); }); } From dccc1dbd16d622d544b6924741af6ea16d67bb47 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 22:58:53 -0700 Subject: [PATCH 22/32] no need to delete non existant cloud --- spec/helper.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index b280b0d000..24a20132eb 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -53,9 +53,6 @@ var app = express(); app.use('/1', api); var server = app.listen(port); -// Prevent reinitializing the server from clobbering Cloud Code -delete defaultConfiguration.cloud; - // Allows testing specific configurations of Parse Server const reconfigureServer = changedConfiguration => { return new Promise((resolve, reject) => { From 05e5495342b0ae95b223a2f1b1d406cae8d69377 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 7 Jun 2016 23:19:55 -0700 Subject: [PATCH 23/32] undo --- spec/helper.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 24a20132eb..a149881d99 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -56,7 +56,7 @@ var server = app.listen(port); // Allows testing specific configurations of Parse Server const reconfigureServer = changedConfiguration => { return new Promise((resolve, reject) => { - const startNewServer = () => { + server.close(() => { try { let newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject) @@ -69,8 +69,7 @@ const reconfigureServer = changedConfiguration => { } catch(error) { reject(error); } - } - server.close(startNewServer); + }); }); } @@ -100,7 +99,7 @@ beforeEach(done => { return; } }) - .then(() => reconfigureServer()) + .then(reconfigureServer) .then(() => { Parse.initialize('test', 'test', 'test'); Parse.serverURL = 'http://localhost:' + port + '/1'; From 3e7a03e4548d3876c30a4e816da3bf229acf8568 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 8 Jun 2016 00:09:02 -0700 Subject: [PATCH 24/32] Fix all tests where connections are left open after server closes. --- spec/ParseACL.spec.js | 920 +++++++++++++++++--------------- spec/ParseAPI.spec.js | 130 +++-- spec/PointerPermissions.spec.js | 31 +- spec/helper.js | 17 + 4 files changed, 573 insertions(+), 525 deletions(-) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index ac25793b8f..5d4f1cef2b 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -58,18 +58,20 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); // Start making requests by the public, which should all fail. - Parse.User.logOut(); - // Get - var query = new Parse.Query(TestObject); - query.get(object.id, { - success: function(model) { - fail('Should not have retrieved the object.'); - done(); - }, - error: function(model, error) { - equal(error.code, Parse.Error.OBJECT_NOT_FOUND); - done(); - } + Parse.User.logOut() + .then(() => { + // Get + var query = new Parse.Query(TestObject); + query.get(object.id, { + success: function(model) { + fail('Should not have retrieved the object.'); + done(); + }, + error: function(model, error) { + equal(error.code, Parse.Error.OBJECT_NOT_FOUND); + done(); + } + }); }); } }); @@ -96,16 +98,18 @@ describe('Parse.ACL', () => { ok(object.get("ACL")); // Start making requests by the public, which should all fail. - Parse.User.logOut(); - - // Find - var query = new Parse.Query(TestObject); - query.find({ - success: function(results) { - equal(results.length, 0); - done(); - } + Parse.User.logOut() + .then(() => { + // Find + var query = new Parse.Query(TestObject); + query.find({ + success: function(results) { + equal(results.length, 0); + done(); + } + }); }); + } }); } @@ -131,18 +135,19 @@ describe('Parse.ACL', () => { ok(object.get("ACL")); // Start making requests by the public, which should all fail. - Parse.User.logOut(); - - // Update - object.set("foo", "bar"); - object.save(null, { - success: function() { - fail('Should not have been able to update the object.'); - done(); - }, error: function(model, err) { - equal(err.code, Parse.Error.OBJECT_NOT_FOUND); - done(); - } + Parse.User.logOut() + .then(() => { + // Update + object.set("foo", "bar"); + object.save(null, { + success: function() { + fail('Should not have been able to update the object.'); + done(); + }, error: function(model, err) { + equal(err.code, Parse.Error.OBJECT_NOT_FOUND); + done(); + } + }); }); } }); @@ -202,24 +207,26 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); - Parse.User.logOut(); - Parse.User.logIn("alice", "wonderland", { - success: function() { - // Get - var query = new Parse.Query(TestObject); - query.get(object.id, { - success: function(result) { - ok(result); - equal(result.id, object.id); - equal(result.getACL().getReadAccess(user), true); - equal(result.getACL().getWriteAccess(user), true); - equal(result.getACL().getPublicReadAccess(), false); - equal(result.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); - done(); - } - }); - } + Parse.User.logOut() + .then(() => { + Parse.User.logIn("alice", "wonderland", { + success: function() { + // Get + var query = new Parse.Query(TestObject); + query.get(object.id, { + success: function(result) { + ok(result); + equal(result.id, object.id); + equal(result.getACL().getReadAccess(user), true); + equal(result.getACL().getWriteAccess(user), true); + equal(result.getACL().getPublicReadAccess(), false); + equal(result.getACL().getPublicWriteAccess(), false); + ok(object.get("ACL")); + done(); + } + }); + } + }); }); } }); @@ -245,29 +252,31 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); - Parse.User.logOut(); - Parse.User.logIn("alice", "wonderland", { - success: function() { - // Find - var query = new Parse.Query(TestObject); - query.find({ - success: function(results) { - equal(results.length, 1); - var result = results[0]; - ok(result); - if (!result) { - return fail(); + Parse.User.logOut() + .then(() => { + Parse.User.logIn("alice", "wonderland", { + success: function() { + // Find + var query = new Parse.Query(TestObject); + query.find({ + success: function(results) { + equal(results.length, 1); + var result = results[0]; + ok(result); + if (!result) { + return fail(); + } + equal(result.id, object.id); + equal(result.getACL().getReadAccess(user), true); + equal(result.getACL().getWriteAccess(user), true); + equal(result.getACL().getPublicReadAccess(), false); + equal(result.getACL().getPublicWriteAccess(), false); + ok(object.get("ACL")); + done(); } - equal(result.id, object.id); - equal(result.getACL().getReadAccess(user), true); - equal(result.getACL().getWriteAccess(user), true); - equal(result.getACL().getPublicReadAccess(), false); - equal(result.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); - done(); - } - }); - } + }); + } + }); }); } }); @@ -293,17 +302,19 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); - Parse.User.logOut(); - Parse.User.logIn("alice", "wonderland", { - success: function() { - // Update - object.set("foo", "bar"); - object.save(null, { - success: function() { - done(); - } - }); - } + Parse.User.logOut() + .then(() => { + Parse.User.logIn("alice", "wonderland", { + success: function() { + // Update + object.set("foo", "bar"); + object.save(null, { + success: function() { + done(); + } + }); + } + }); }); } }); @@ -329,16 +340,18 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); - Parse.User.logOut(); - Parse.User.logIn("alice", "wonderland", { - success: function() { - // Delete - object.destroy({ - success: function() { - done(); - } - }); - } + Parse.User.logOut() + .then(() => { + Parse.User.logIn("alice", "wonderland", { + success: function() { + // Delete + object.destroy({ + success: function() { + done(); + } + }); + } + }); }); } }); @@ -374,16 +387,17 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); - Parse.User.logOut(); - - // Get - var query = new Parse.Query(TestObject); - query.get(object.id, { - success: function(result) { - ok(result); - equal(result.id, object.id); - done(); - } + Parse.User.logOut() + .then(() => { + // Get + var query = new Parse.Query(TestObject); + query.get(object.id, { + success: function(result) { + ok(result); + equal(result.id, object.id); + done(); + } + }); }); } }); @@ -421,18 +435,19 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); - Parse.User.logOut(); - - // Find - var query = new Parse.Query(TestObject); - query.find({ - success: function(results) { - equal(results.length, 1); - var result = results[0]; - ok(result); - equal(result.id, object.id); - done(); - } + Parse.User.logOut() + .then(() => { + // Find + var query = new Parse.Query(TestObject); + query.find({ + success: function(results) { + equal(results.length, 1); + var result = results[0]; + ok(result); + equal(result.id, object.id); + done(); + } + }); }); } }); @@ -470,15 +485,16 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); - Parse.User.logOut(); - - // Update - object.set("foo", "bar"); - object.save().then(() => { - fail('the save should fail'); - }, error => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); + Parse.User.logOut() + .then(() => { + // Update + object.set("foo", "bar"); + object.save().then(() => { + fail('the save should fail'); + }, error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); }); } }); @@ -516,10 +532,9 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); ok(object.get("ACL")); - Parse.User.logOut(); - - // Delete - object.destroy().then(() => { + Parse.User.logOut() + .then(() => object.destroy()) + .then(() => { fail('expected failure'); }, error => { expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); @@ -561,15 +576,16 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), true); ok(object.get("ACL")); - Parse.User.logOut(); - - // Get - var query = new Parse.Query(TestObject); - query.get(object.id, { - error: function(model, error) { - equal(error.code, Parse.Error.OBJECT_NOT_FOUND); - done(); - } + Parse.User.logOut() + .then(() => { + // Get + var query = new Parse.Query(TestObject); + query.get(object.id, { + error: function(model, error) { + equal(error.code, Parse.Error.OBJECT_NOT_FOUND); + done(); + } + }); }); } }); @@ -607,15 +623,16 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), true); ok(object.get("ACL")); - Parse.User.logOut(); - - // Find - var query = new Parse.Query(TestObject); - query.find({ - success: function(results) { - equal(results.length, 0); - done(); - } + Parse.User.logOut() + .then(() => { + // Find + var query = new Parse.Query(TestObject); + query.find({ + success: function(results) { + equal(results.length, 0); + done(); + } + }); }); } }); @@ -653,14 +670,15 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), true); ok(object.get("ACL")); - Parse.User.logOut(); - - // Update - object.set("foo", "bar"); - object.save(null, { - success: function() { - done(); - } + Parse.User.logOut() + .then(() => { + // Update + object.set("foo", "bar"); + object.save(null, { + success: function() { + done(); + } + }); }); } }); @@ -698,13 +716,14 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), true); ok(object.get("ACL")); - Parse.User.logOut(); - - // Delete - object.destroy({ - success: function() { - done(); - } + Parse.User.logOut() + .then(() => { + // Delete + object.destroy({ + success: function() { + done(); + } + }); }); } }); @@ -718,41 +737,43 @@ describe('Parse.ACL', () => { // Sign in as Bob. Parse.User.signUp("bob", "pass", null, { success: function(bob) { - Parse.User.logOut(); - // Sign in as Alice. - Parse.User.signUp("alice", "wonderland", null, { - success: function(alice) { - // Create an object shared by Bob and Alice. - var object = new TestObject(); - var acl = new Parse.ACL(alice); - acl.setWriteAccess(bob, true); - acl.setReadAccess(bob, true); - object.setACL(acl); - object.save(null, { - success: function() { - equal(object.getACL().getReadAccess(alice), true); - equal(object.getACL().getWriteAccess(alice), true); - equal(object.getACL().getReadAccess(bob), true); - equal(object.getACL().getWriteAccess(bob), true); - equal(object.getACL().getPublicReadAccess(), false); - equal(object.getACL().getPublicWriteAccess(), false); - - // Sign in as Bob again. - Parse.User.logIn("bob", "pass", { - success: function() { - var query = new Parse.Query(TestObject); - query.get(object.id, { - success: function(result) { - ok(result); - equal(result.id, object.id); - done(); - } - }); - } - }); - } - }); - } + Parse.User.logOut() + .then(() => { + // Sign in as Alice. + Parse.User.signUp("alice", "wonderland", null, { + success: function(alice) { + // Create an object shared by Bob and Alice. + var object = new TestObject(); + var acl = new Parse.ACL(alice); + acl.setWriteAccess(bob, true); + acl.setReadAccess(bob, true); + object.setACL(acl); + object.save(null, { + success: function() { + equal(object.getACL().getReadAccess(alice), true); + equal(object.getACL().getWriteAccess(alice), true); + equal(object.getACL().getReadAccess(bob), true); + equal(object.getACL().getWriteAccess(bob), true); + equal(object.getACL().getPublicReadAccess(), false); + equal(object.getACL().getPublicWriteAccess(), false); + + // Sign in as Bob again. + Parse.User.logIn("bob", "pass", { + success: function() { + var query = new Parse.Query(TestObject); + query.get(object.id, { + success: function(result) { + ok(result); + equal(result.id, object.id); + done(); + } + }); + } + }); + } + }); + } + }); }); } }); @@ -762,47 +783,49 @@ describe('Parse.ACL', () => { // Sign in as Bob. Parse.User.signUp("bob", "pass", null, { success: function(bob) { - Parse.User.logOut(); - // Sign in as Alice. - Parse.User.signUp("alice", "wonderland", null, { - success: function(alice) { - // Create an object shared by Bob and Alice. - var object = new TestObject(); - var acl = new Parse.ACL(alice); - acl.setWriteAccess(bob, true); - acl.setReadAccess(bob, true); - object.setACL(acl); - object.save(null, { - success: function() { - equal(object.getACL().getReadAccess(alice), true); - equal(object.getACL().getWriteAccess(alice), true); - equal(object.getACL().getReadAccess(bob), true); - equal(object.getACL().getWriteAccess(bob), true); - equal(object.getACL().getPublicReadAccess(), false); - equal(object.getACL().getPublicWriteAccess(), false); - - // Sign in as Bob again. - Parse.User.logIn("bob", "pass", { - success: function() { - var query = new Parse.Query(TestObject); - query.find({ - success: function(results) { - equal(results.length, 1); - var result = results[0]; - ok(result); - if (!result) { - fail("should have result"); - } else { - equal(result.id, object.id); + Parse.User.logOut() + .then(() => { + // Sign in as Alice. + Parse.User.signUp("alice", "wonderland", null, { + success: function(alice) { + // Create an object shared by Bob and Alice. + var object = new TestObject(); + var acl = new Parse.ACL(alice); + acl.setWriteAccess(bob, true); + acl.setReadAccess(bob, true); + object.setACL(acl); + object.save(null, { + success: function() { + equal(object.getACL().getReadAccess(alice), true); + equal(object.getACL().getWriteAccess(alice), true); + equal(object.getACL().getReadAccess(bob), true); + equal(object.getACL().getWriteAccess(bob), true); + equal(object.getACL().getPublicReadAccess(), false); + equal(object.getACL().getPublicWriteAccess(), false); + + // Sign in as Bob again. + Parse.User.logIn("bob", "pass", { + success: function() { + var query = new Parse.Query(TestObject); + query.find({ + success: function(results) { + equal(results.length, 1); + var result = results[0]; + ok(result); + if (!result) { + fail("should have result"); + } else { + equal(result.id, object.id); + } + done(); } - done(); - } - }); - } - }); - } - }); - } + }); + } + }); + } + }); + } + }); }); } }); @@ -812,39 +835,41 @@ describe('Parse.ACL', () => { // Sign in as Bob. Parse.User.signUp("bob", "pass", null, { success: function(bob) { - Parse.User.logOut(); - // Sign in as Alice. - Parse.User.signUp("alice", "wonderland", null, { - success: function(alice) { - // Create an object shared by Bob and Alice. - var object = new TestObject(); - var acl = new Parse.ACL(alice); - acl.setWriteAccess(bob, true); - acl.setReadAccess(bob, true); - object.setACL(acl); - object.save(null, { - success: function() { - equal(object.getACL().getReadAccess(alice), true); - equal(object.getACL().getWriteAccess(alice), true); - equal(object.getACL().getReadAccess(bob), true); - equal(object.getACL().getWriteAccess(bob), true); - equal(object.getACL().getPublicReadAccess(), false); - equal(object.getACL().getPublicWriteAccess(), false); - - // Sign in as Bob again. - Parse.User.logIn("bob", "pass", { - success: function() { - object.set("foo", "bar"); - object.save(null, { - success: function() { - done(); - } - }); - } - }); - } - }); - } + Parse.User.logOut() + .then(() => { + // Sign in as Alice. + Parse.User.signUp("alice", "wonderland", null, { + success: function(alice) { + // Create an object shared by Bob and Alice. + var object = new TestObject(); + var acl = new Parse.ACL(alice); + acl.setWriteAccess(bob, true); + acl.setReadAccess(bob, true); + object.setACL(acl); + object.save(null, { + success: function() { + equal(object.getACL().getReadAccess(alice), true); + equal(object.getACL().getWriteAccess(alice), true); + equal(object.getACL().getReadAccess(bob), true); + equal(object.getACL().getWriteAccess(bob), true); + equal(object.getACL().getPublicReadAccess(), false); + equal(object.getACL().getPublicWriteAccess(), false); + + // Sign in as Bob again. + Parse.User.logIn("bob", "pass", { + success: function() { + object.set("foo", "bar"); + object.save(null, { + success: function() { + done(); + } + }); + } + }); + } + }); + } + }); }); } }); @@ -854,39 +879,41 @@ describe('Parse.ACL', () => { // Sign in as Bob. Parse.User.signUp("bob", "pass", null, { success: function(bob) { - Parse.User.logOut(); - // Sign in as Alice. - Parse.User.signUp("alice", "wonderland", null, { - success: function(alice) { - // Create an object shared by Bob and Alice. - var object = new TestObject(); - var acl = new Parse.ACL(alice); - acl.setWriteAccess(bob, true); - acl.setReadAccess(bob, true); - object.setACL(acl); - object.save(null, { - success: function() { - equal(object.getACL().getReadAccess(alice), true); - equal(object.getACL().getWriteAccess(alice), true); - equal(object.getACL().getReadAccess(bob), true); - equal(object.getACL().getWriteAccess(bob), true); - equal(object.getACL().getPublicReadAccess(), false); - equal(object.getACL().getPublicWriteAccess(), false); - - // Sign in as Bob again. - Parse.User.logIn("bob", "pass", { - success: function() { - object.set("foo", "bar"); - object.destroy({ - success: function() { - done(); - } - }); - } - }); - } - }); - } + Parse.User.logOut() + .then(() => { + // Sign in as Alice. + Parse.User.signUp("alice", "wonderland", null, { + success: function(alice) { + // Create an object shared by Bob and Alice. + var object = new TestObject(); + var acl = new Parse.ACL(alice); + acl.setWriteAccess(bob, true); + acl.setReadAccess(bob, true); + object.setACL(acl); + object.save(null, { + success: function() { + equal(object.getACL().getReadAccess(alice), true); + equal(object.getACL().getWriteAccess(alice), true); + equal(object.getACL().getReadAccess(bob), true); + equal(object.getACL().getWriteAccess(bob), true); + equal(object.getACL().getPublicReadAccess(), false); + equal(object.getACL().getPublicWriteAccess(), false); + + // Sign in as Bob again. + Parse.User.logIn("bob", "pass", { + success: function() { + object.set("foo", "bar"); + object.destroy({ + success: function() { + done(); + } + }); + } + }); + } + }); + } + }); }); } }); @@ -896,38 +923,41 @@ describe('Parse.ACL', () => { // Sign in as Bob. Parse.User.signUp("bob", "pass", null, { success: function(bob) { - Parse.User.logOut(); - // Sign in as Alice. - Parse.User.signUp("alice", "wonderland", null, { - success: function(alice) { - // Create an object shared by Bob and Alice. - var object = new TestObject(); - var acl = new Parse.ACL(alice); - acl.setWriteAccess(bob, true); - acl.setReadAccess(bob, true); - object.setACL(acl); - object.save(null, { - success: function() { - equal(object.getACL().getReadAccess(alice), true); - equal(object.getACL().getWriteAccess(alice), true); - equal(object.getACL().getReadAccess(bob), true); - equal(object.getACL().getWriteAccess(bob), true); - equal(object.getACL().getPublicReadAccess(), false); - equal(object.getACL().getPublicWriteAccess(), false); - - // Start making requests by the public. - Parse.User.logOut(); - - var query = new Parse.Query(TestObject); - query.get(object.id).then((result) => { - fail(result); - }, (error) => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); - } - }); - } + Parse.User.logOut() + .then(() => { + // Sign in as Alice. + Parse.User.signUp("alice", "wonderland", null, { + success: function(alice) { + // Create an object shared by Bob and Alice. + var object = new TestObject(); + var acl = new Parse.ACL(alice); + acl.setWriteAccess(bob, true); + acl.setReadAccess(bob, true); + object.setACL(acl); + object.save(null, { + success: function() { + equal(object.getACL().getReadAccess(alice), true); + equal(object.getACL().getWriteAccess(alice), true); + equal(object.getACL().getReadAccess(bob), true); + equal(object.getACL().getWriteAccess(bob), true); + equal(object.getACL().getPublicReadAccess(), false); + equal(object.getACL().getPublicWriteAccess(), false); + + // Start making requests by the public. + Parse.User.logOut() + .then(() => { + var query = new Parse.Query(TestObject); + query.get(object.id).then((result) => { + fail(result); + }, (error) => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); + }); + } + }); + } + }); }); } }); @@ -937,38 +967,41 @@ describe('Parse.ACL', () => { // Sign in as Bob. Parse.User.signUp("bob", "pass", null, { success: function(bob) { - Parse.User.logOut(); - // Sign in as Alice. - Parse.User.signUp("alice", "wonderland", null, { - success: function(alice) { - // Create an object shared by Bob and Alice. - var object = new TestObject(); - var acl = new Parse.ACL(alice); - acl.setWriteAccess(bob, true); - acl.setReadAccess(bob, true); - object.setACL(acl); - object.save(null, { - success: function() { - equal(object.getACL().getReadAccess(alice), true); - equal(object.getACL().getWriteAccess(alice), true); - equal(object.getACL().getReadAccess(bob), true); - equal(object.getACL().getWriteAccess(bob), true); - equal(object.getACL().getPublicReadAccess(), false); - equal(object.getACL().getPublicWriteAccess(), false); - - // Start making requests by the public. - Parse.User.logOut(); - - var query = new Parse.Query(TestObject); - query.find({ - success: function(results) { - equal(results.length, 0); - done(); - } - }); - } - }); - } + Parse.User.logOut() + .then(() => { + // Sign in as Alice. + Parse.User.signUp("alice", "wonderland", null, { + success: function(alice) { + // Create an object shared by Bob and Alice. + var object = new TestObject(); + var acl = new Parse.ACL(alice); + acl.setWriteAccess(bob, true); + acl.setReadAccess(bob, true); + object.setACL(acl); + object.save(null, { + success: function() { + equal(object.getACL().getReadAccess(alice), true); + equal(object.getACL().getWriteAccess(alice), true); + equal(object.getACL().getReadAccess(bob), true); + equal(object.getACL().getWriteAccess(bob), true); + equal(object.getACL().getPublicReadAccess(), false); + equal(object.getACL().getPublicWriteAccess(), false); + + // Start making requests by the public. + Parse.User.logOut() + .then(() => { + var query = new Parse.Query(TestObject); + query.find({ + success: function(results) { + equal(results.length, 0); + done(); + } + }); + }); + } + }); + } + }); }); } }); @@ -978,38 +1011,41 @@ describe('Parse.ACL', () => { // Sign in as Bob. Parse.User.signUp("bob", "pass", null, { success: function(bob) { - Parse.User.logOut(); - // Sign in as Alice. - Parse.User.signUp("alice", "wonderland", null, { - success: function(alice) { - // Create an object shared by Bob and Alice. - var object = new TestObject(); - var acl = new Parse.ACL(alice); - acl.setWriteAccess(bob, true); - acl.setReadAccess(bob, true); - object.setACL(acl); - object.save(null, { - success: function() { - equal(object.getACL().getReadAccess(alice), true); - equal(object.getACL().getWriteAccess(alice), true); - equal(object.getACL().getReadAccess(bob), true); - equal(object.getACL().getWriteAccess(bob), true); - equal(object.getACL().getPublicReadAccess(), false); - equal(object.getACL().getPublicWriteAccess(), false); - - // Start making requests by the public. - Parse.User.logOut(); - - object.set("foo", "bar"); - object.save().then(() => { - fail('expected failure'); - }, (error) => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); - } - }); - } + Parse.User.logOut() + .then(() => { + // Sign in as Alice. + Parse.User.signUp("alice", "wonderland", null, { + success: function(alice) { + // Create an object shared by Bob and Alice. + var object = new TestObject(); + var acl = new Parse.ACL(alice); + acl.setWriteAccess(bob, true); + acl.setReadAccess(bob, true); + object.setACL(acl); + object.save(null, { + success: function() { + equal(object.getACL().getReadAccess(alice), true); + equal(object.getACL().getWriteAccess(alice), true); + equal(object.getACL().getReadAccess(bob), true); + equal(object.getACL().getWriteAccess(bob), true); + equal(object.getACL().getPublicReadAccess(), false); + equal(object.getACL().getPublicWriteAccess(), false); + + // Start making requests by the public. + Parse.User.logOut() + .then(() => { + object.set("foo", "bar"); + object.save().then(() => { + fail('expected failure'); + }, (error) => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); + }); + } + }); + } + }); }); } }); @@ -1019,37 +1055,39 @@ describe('Parse.ACL', () => { // Sign in as Bob. Parse.User.signUp("bob", "pass", null, { success: function(bob) { - Parse.User.logOut(); - // Sign in as Alice. - Parse.User.signUp("alice", "wonderland", null, { - success: function(alice) { - // Create an object shared by Bob and Alice. - var object = new TestObject(); - var acl = new Parse.ACL(alice); - acl.setWriteAccess(bob, true); - acl.setReadAccess(bob, true); - object.setACL(acl); - object.save(null, { - success: function() { - equal(object.getACL().getReadAccess(alice), true); - equal(object.getACL().getWriteAccess(alice), true); - equal(object.getACL().getReadAccess(bob), true); - equal(object.getACL().getWriteAccess(bob), true); - equal(object.getACL().getPublicReadAccess(), false); - equal(object.getACL().getPublicWriteAccess(), false); - - // Start making requests by the public. - Parse.User.logOut(); - - object.destroy().then(() => { - fail('expected failure'); - }, (error) => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); - } - }); - } + Parse.User.logOut() + .then(() => { + // Sign in as Alice. + Parse.User.signUp("alice", "wonderland", null, { + success: function(alice) { + // Create an object shared by Bob and Alice. + var object = new TestObject(); + var acl = new Parse.ACL(alice); + acl.setWriteAccess(bob, true); + acl.setReadAccess(bob, true); + object.setACL(acl); + object.save(null, { + success: function() { + equal(object.getACL().getReadAccess(alice), true); + equal(object.getACL().getWriteAccess(alice), true); + equal(object.getACL().getReadAccess(bob), true); + equal(object.getACL().getWriteAccess(bob), true); + equal(object.getACL().getPublicReadAccess(), false); + equal(object.getACL().getPublicWriteAccess(), false); + + // Start making requests by the public. + Parse.User.logOut() + .then(() => object.destroy()) + .then(() => { + fail('expected failure'); + }, (error) => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); + } + }); + } + }); }); } }); @@ -1102,16 +1140,18 @@ describe('Parse.ACL', () => { foo: "bar" }, { success: function(user) { - Parse.User.logOut(); - Parse.User.logIn("tdurden", "mayhem", { - success: function(user) { - equal(user.get("foo"), "bar"); - done(); - }, - error: function(user, error) { - ok(null, "Error " + error.id + ": " + error.message); - done(); - } + Parse.User.logOut() + .then(() => { + Parse.User.logIn("tdurden", "mayhem", { + success: function(user) { + equal(user.get("foo"), "bar"); + done(); + }, + error: function(user, error) { + ok(null, "Error " + error.id + ": " + error.message); + done(); + } + }); }); }, error: function(user, error) { diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index ee866085c9..d611aca596 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -51,80 +51,74 @@ describe('miscellaneous', function() { }); it('fail to create a duplicate username', done => { - reconfigureServer({}) - .then(() => { - let numCreated = 0; - let numFailed = 0; - let p1 = createTestUser(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - let p2 = createTestUser(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - Parse.Promise.all([p1, p2]) - .then(() => { - fail('one of the users should not have been created'); - done(); - }) - .catch(done); + let numCreated = 0; + let numFailed = 0; + let p1 = createTestUser(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); }); + let p2 = createTestUser(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + }); + Parse.Promise.when([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); }); it('ensure that email is uniquely indexed', done => { - reconfigureServer({}) - .then(() => { - let numCreated = 0; - let numFailed = 0; - - let user1 = new Parse.User(); - user1.setPassword('asdf'); - user1.setUsername('u1'); - user1.setEmail('dupe@dupe.dupe'); - let p1 = user1.signUp(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }, error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); - - let user2 = new Parse.User(); - user2.setPassword('asdf'); - user2.setUsername('u2'); - user2.setEmail('dupe@dupe.dupe'); - let p2 = user2.signUp(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }, error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); + let numCreated = 0; + let numFailed = 0; + + let user1 = new Parse.User(); + user1.setPassword('asdf'); + user1.setUsername('u1'); + user1.setEmail('dupe@dupe.dupe'); + let p1 = user1.signUp(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }, error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); - Parse.Promise.all([p1, p2]) - .then(() => { - fail('one of the users should not have been created'); - done(); - }) - .catch(done); + let user2 = new Parse.User(); + user2.setPassword('asdf'); + user2.setUsername('u2'); + user2.setEmail('dupe@dupe.dupe'); + let p2 = user2.signUp(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }, error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); }); + + Parse.Promise.when([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); }); it('ensure that if people already have duplicate users, they can still sign up new users', done => { diff --git a/spec/PointerPermissions.spec.js b/spec/PointerPermissions.spec.js index de51a3314b..d78b5b6edf 100644 --- a/spec/PointerPermissions.spec.js +++ b/spec/PointerPermissions.spec.js @@ -194,7 +194,7 @@ describe('Pointer Permissions', () => { }) }); - it('should handle multiple writeUserFields', (done) => { + it('should handle multiple writeUserFields', done => { let config = new Config(Parse.applicationId); let user = new Parse.User(); let user2 = new Parse.User(); @@ -207,27 +207,24 @@ describe('Pointer Permissions', () => { password: 'password' }); let obj = new Parse.Object('AnObject'); - Parse.Object.saveAll([user, user2]).then(() => { + Parse.Object.saveAll([user, user2]) + .then(() => { obj.set('owner', user); obj.set('otherOwner', user2); return obj.save(); - }).then(() => { - return config.database.loadSchema().then((schema) => { - return schema.updateClass('AnObject', {}, {find: {"*": true},writeUserFields: ['owner', 'otherOwner']}); - }); - }).then(() => { - return Parse.User.logIn('user1', 'password'); - }).then(() => { - return obj.save({hello: 'fromUser1'}); - }).then(() => { - return Parse.User.logIn('user2', 'password'); - }).then(() => { - return obj.save({hello: 'fromUser2'}); - }).then(() => { - Parse.User.logOut(); + }) + .then(() => config.database.loadSchema()) + .then(schema => schema.updateClass('AnObject', {}, {find: {"*": true},writeUserFields: ['owner', 'otherOwner']})) + .then(() => Parse.User.logIn('user1', 'password')) + .then(() => obj.save({hello: 'fromUser1'})) + .then(() => Parse.User.logIn('user2', 'password')) + .then(() => obj.save({hello: 'fromUser2'})) + .then(() => Parse.User.logOut()) + .then(() => { let q = new Parse.Query('AnObject'); return q.first(); - }).then((result) => { + }) + .then(result => { expect(result.get('hello')).toBe('fromUser2'); done(); }).catch(err => { diff --git a/spec/helper.js b/spec/helper.js index a149881d99..2cf381298b 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -47,11 +47,19 @@ var defaultConfiguration = { }, }; +let openConnections = {}; + // Set up a default API server for testing with default configuration. var api = new ParseServer(defaultConfiguration); var app = express(); app.use('/1', api); + var server = app.listen(port); +server.on('connection', connection => { + let key = `${connection.remoteAddress}:${connection.remotePort}`; + openConnections[key] = connection; + connection.on('close', () => { delete openConnections[key] }); +}); // Allows testing specific configurations of Parse Server const reconfigureServer = changedConfiguration => { @@ -65,7 +73,13 @@ const reconfigureServer = changedConfiguration => { app = express(); api = new ParseServer(newConfiguration); app.use('/1', api); + server = app.listen(port); + server.on('connection', connection => { + let key = `${connection.remoteAddress}:${connection.remotePort}`; + openConnections[key] = connection; + connection.on('close', () => { delete openConnections[key] }); + }); } catch(error) { reject(error); } @@ -129,6 +143,9 @@ afterEach(function(done) { }) .then(() => Parse.User.logOut()) .then(() => { + if (Object.keys(openConnections).length > 0) { + fail('There were open connections to the server left after the test finished'); + } done(); }) .catch(error => { From 7e1349ce31c1cfa6f9df322b9cf08438db9a42c5 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 8 Jun 2016 00:28:39 -0700 Subject: [PATCH 25/32] Fix issues caused by missing gridstore adapter --- spec/helper.js | 10 ++++++++-- src/ParseServer.js | 4 ++++ src/Routers/FilesRouter.js | 15 +++++++-------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index 2cf381298b..7160cfd958 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -11,17 +11,23 @@ var ParseServer = require('../src/index').ParseServer; var path = require('path'); var TestUtils = require('../src/index').TestUtils; var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); +const GridStoreAdapter = require('../src/Adapters/Files/GridStoreAdapter').GridStoreAdapter; + var port = 8378; -var mongoAdapter = new MongoStorageAdapter({ - uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', +let mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; +let mongoAdapter = new MongoStorageAdapter({ + uri: mongoURI, collectionPrefix: 'test_', }) +let gridStoreAdapter = new GridStoreAdapter(mongoURI); + // Default server configuration for tests. var defaultConfiguration = { databaseAdapter: mongoAdapter, + filesAdapter: gridStoreAdapter, serverURL: 'http://localhost:' + port + '/1', appId: 'test', javascriptKey: 'test', diff --git a/src/ParseServer.js b/src/ParseServer.js index be9b708a92..b823b00615 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -148,6 +148,10 @@ class ParseServer { databaseAdapter = loadAdapter(databaseAdapter) } + if (!filesAdapter && !databaseURI) { + throw 'When using an explicit database adapter, you must also use and explicit filesAdapter.'; + } + if (logsFolder) { configureLogger({ logsFolder diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index a3a3c81172..160574e1b7 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -1,9 +1,9 @@ -import express from 'express'; -import BodyParser from 'body-parser'; -import * as Middlewares from '../middlewares'; +import express from 'express'; +import BodyParser from 'body-parser'; +import * as Middlewares from '../middlewares'; import { randomHexString } from '../cryptoUtils'; -import Config from '../Config'; -import mime from 'mime'; +import Config from '../Config'; +import mime from 'mime'; export class FilesRouter { @@ -77,8 +77,7 @@ export class FilesRouter { res.set('Location', result.url); res.json(result); }).catch((err) => { - next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, - 'Could not store file.')); + next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.')); }); } @@ -93,4 +92,4 @@ export class FilesRouter { 'Could not delete file.')); }); } -} \ No newline at end of file +} From 0dc918ad096200368078586ee65fe0c06f54b136 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 8 Jun 2016 00:42:34 -0700 Subject: [PATCH 26/32] Update guide for 2.3.0 and fix final tests --- 2.3.0.md | 12 +++--------- spec/ParseAPI.spec.js | 8 ++++---- src/testing-routes.js | 1 + 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/2.3.0.md b/2.3.0.md index 0a5abe0ccd..a46590f033 100644 --- a/2.3.0.md +++ b/2.3.0.md @@ -8,14 +8,13 @@ If you are using MongoDB in Cluster or Replica Set mode, we recommend reading Mo // Select the database that your Parse App uses use parse; -// Select the collection your Parse App uses for users. For migrated apps, this probably include a collectionPrefix. +// Select the collection your Parse App uses for users. For migrated apps, this probably includes a collectionPrefix. var coll = db['your_prefix:_User']; // You can check if the indexes already exists by running coll.getIndexes() coll.getIndexes(); // The indexes you want should look like this. If they already exists, you can skip creating them. -/* { "v" : 1, "unique" : true, @@ -39,7 +38,6 @@ coll.getIndexes(); "background" : true, "sparse" : true } -*/ // Create the username index. // "background: true" is mandatory and avoids downtime while the index builds. @@ -56,11 +54,11 @@ There are some issues you may run into during this process: ## Mongo complains that the index already exists, but with different options -In this case, you will need to create the unique index using a different index name, and/or remove the incorrect index. Avoid downtime or a temporary performance regression in your app by building the new index before removing the old one. +In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as a different options. Then, drop the conflicting index, and create the unique index. ## There is already non-unique data in the username or email field -This is possible if you have explicitly set some user's emails to null. These null emails can be removed with this command: +This is possible if you have explicitly set some user's emails to null. If this is bogus data, and those null fields shoud be unset, you can unset the null emails with this command. If your app relies on the difference between null and unset emails, you will need to upgrade your app to treat null and unset emails the same before building the index and upgrading to Parse Server 2.3.0. ```js coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, { multi: true }) @@ -69,7 +67,3 @@ coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, ## There is already non-unique data in the username or email field, and it's not nulls This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows. - -## My app depends on emails set to null behaving differently from emails set to undefined - -You will need to update your app before executing the previous steps. diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index d611aca596..6696568110 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -252,8 +252,8 @@ describe('miscellaneous', function() { return Parse.User.logIn('test', 'moon-y'); }).then((user) => { expect(user.get('foo')).toEqual(2); - Parse.User.logOut(); - done(); + Parse.User.logOut() + .then(done); }, (error) => { fail(error); done(); @@ -371,8 +371,8 @@ describe('miscellaneous', function() { expect(results.length).toEqual(1); expect(results[0]['foo']).toEqual('bar'); done(); - }).fail(err => { - fail(err); + }).fail(error => { + fail(JSON.stringify(error)); done(); }) }); diff --git a/src/testing-routes.js b/src/testing-routes.js index eee022d9f9..bcd05a9db6 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -14,6 +14,7 @@ function createApp(req, res) { var appId = cryptoUtils.randomHexString(32); ParseServer({ + databaseURI: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', appId: appId, masterKey: 'master', serverURL: Parse.serverURL, From a3f023b2098c42f8706e7e25f463a316823d971d Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 8 Jun 2016 01:20:20 -0700 Subject: [PATCH 27/32] use strict --- spec/RestQuery.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index c0df2855d1..12bfa6a178 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -1,3 +1,4 @@ +'use strict' // These tests check the "find" functionality of the REST API. var auth = require('../src/Auth'); var cache = require('../src/cache'); From cbbc59070f3c784fa6d55c6a01d5b14493456d87 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 8 Jun 2016 01:22:10 -0700 Subject: [PATCH 28/32] don't use features that won't work in node 4 --- spec/index.spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 14db9ccd96..5e34bd89da 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -161,8 +161,8 @@ describe('server', () => { }); it('can create a parse-server v1', done => { - var parseServer = new ParseServer.default({ - ...defaultConfiguration, + var parseServer = new ParseServer.default(Object.assign({}, + defaultConfiguration, { appId: "aTestApp", masterKey: "aTestMasterKey", serverURL: "http://localhost:12666/parse", @@ -187,15 +187,15 @@ describe('server', () => { server.close(done); }) }); - }, - }); + }}) + ); }); it('can create a parse-server v2', done => { let objId; let server - let parseServer = ParseServer.ParseServer({ - ...defaultConfiguration, + let parseServer = ParseServer.ParseServer(Object.assign({}, + defaultConfiguration, { appId: "anOtherTestApp", masterKey: "anOtherTestMasterKey", serverURL: "http://localhost:12667/parse", @@ -223,8 +223,8 @@ describe('server', () => { fail(JSON.stringify(error)) done(); }); - }, - }); + }} + ); }); it('has createLiveQueryServer', done => { From 1a0859d4478c6c28f808f21f207008efae42a7ab Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 8 Jun 2016 10:05:23 -0700 Subject: [PATCH 29/32] Fix syntax error --- spec/index.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/index.spec.js b/spec/index.spec.js index 5e34bd89da..b368dccb3e 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -224,7 +224,7 @@ describe('server', () => { done(); }); }} - ); + )); }); it('has createLiveQueryServer', done => { From 0b0518f1a1a480d2225c0273b912fd039b965315 Mon Sep 17 00:00:00 2001 From: Drew Date: Thu, 9 Jun 2016 15:07:49 -0700 Subject: [PATCH 30/32] Fix typos --- 2.3.0.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/2.3.0.md b/2.3.0.md index a46590f033..976b0e72a7 100644 --- a/2.3.0.md +++ b/2.3.0.md @@ -14,7 +14,7 @@ var coll = db['your_prefix:_User']; // You can check if the indexes already exists by running coll.getIndexes() coll.getIndexes(); -// The indexes you want should look like this. If they already exists, you can skip creating them. +// The indexes you want should look like this. If they already exist, you can skip creating them. { "v" : 1, "unique" : true, @@ -54,7 +54,7 @@ There are some issues you may run into during this process: ## Mongo complains that the index already exists, but with different options -In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as a different options. Then, drop the conflicting index, and create the unique index. +In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as different options. Then, drop the conflicting index, and create the unique index. ## There is already non-unique data in the username or email field From b88a0b29ae62f7ff9773330db40baa456509a5e9 Mon Sep 17 00:00:00 2001 From: Drew Date: Thu, 9 Jun 2016 15:42:32 -0700 Subject: [PATCH 31/32] Add duplicate finding command --- 2.3.0.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/2.3.0.md b/2.3.0.md index 976b0e72a7..54ce08a6ab 100644 --- a/2.3.0.md +++ b/2.3.0.md @@ -67,3 +67,16 @@ coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, ## There is already non-unique data in the username or email field, and it's not nulls This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows. + +This command, can be used to find the duplicate data: + +```js +coll.aggregate([ + {$match: {"username": {"$ne": null}}}, + {$group: {_id: "$username", uniqueIds: {$addToSet: "$_id"}, count: {$sum: 1}}}, + {$match: {count: {"$gt": 1}}}, + {$project: {id: "$uniqueIds", username: "$_id", _id : 0} }, + {$unwind: "$id" }, + {$out: 'duplicates'} +], {allowDiskUse:true}) +``` From 1b8aaaf79ab592ef2c75edd8142c7551853ad9f1 Mon Sep 17 00:00:00 2001 From: Drew Date: Thu, 9 Jun 2016 15:48:05 -0700 Subject: [PATCH 32/32] Update 2.3.0.md --- 2.3.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2.3.0.md b/2.3.0.md index 54ce08a6ab..2528c290b6 100644 --- a/2.3.0.md +++ b/2.3.0.md @@ -77,6 +77,6 @@ coll.aggregate([ {$match: {count: {"$gt": 1}}}, {$project: {id: "$uniqueIds", username: "$_id", _id : 0} }, {$unwind: "$id" }, - {$out: 'duplicates'} + {$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates collection. Remove this line to just output the list. ], {allowDiskUse:true}) ```