From 7e868b2dcc98a9010ee582d0dfc1ccbd85a450f8 Mon Sep 17 00:00:00 2001 From: Drew Date: Fri, 10 Jun 2016 20:27:21 -0700 Subject: [PATCH] Unique indexes (#1971) * Add unique indexing * Add unique indexing for username/email * WIP * Finish unique indexes * Notes on how to upgrade to 2.3.0 safely * index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)" * reconfigure username/email tests * Start dealing with test shittyness * Remove tests for files that we are removing * most tests passing * fix failing test * Make specific server config for tests async * Fix more tests * fix more tests * Fix another test * fix more tests * Fix email validation * move some stuff around * Destroy server to ensure all connections are gone * Fix broken cloud code * Save callback to variable * no need to delete non existant cloud * undo * Fix all tests where connections are left open after server closes. * Fix issues caused by missing gridstore adapter * Update guide for 2.3.0 and fix final tests * use strict * don't use features that won't work in node 4 * Fix syntax error * Fix typos * Add duplicate finding command * Update 2.3.0.md --- 2.3.0.md | 82 ++ package.json | 1 + spec/CloudCode.spec.js | 149 +-- spec/DatabaseAdapter.spec.js | 23 - spec/DatabaseController.spec.js | 18 - spec/FileLoggerAdapter.spec.js | 6 +- spec/Parse.Push.spec.js | 29 +- spec/ParseACL.spec.js | 920 +++++++++--------- spec/ParseAPI.spec.js | 208 +++- spec/ParseHooks.spec.js | 51 +- spec/ParseInstallation.spec.js | 3 +- spec/ParseRelation.spec.js | 1 + spec/ParseUser.spec.js | 53 +- spec/PointerPermissions.spec.js | 31 +- spec/PublicAPI.spec.js | 55 +- spec/RestCreate.spec.js | 3 +- spec/RestQuery.spec.js | 5 +- spec/Uniqueness.spec.js | 103 ++ spec/ValidationAndPasswordsReset.spec.js | 656 +++++-------- spec/helper.js | 126 ++- spec/index.spec.js | 349 +++---- src/Adapters/AdapterLoader.js | 3 +- src/Adapters/Storage/Mongo/MongoCollection.js | 12 + .../Storage/Mongo/MongoSchemaCollection.js | 14 +- .../Storage/Mongo/MongoStorageAdapter.js | 29 +- src/Config.js | 26 +- src/Controllers/DatabaseController.js | 11 +- src/Controllers/HooksController.js | 15 +- src/Controllers/UserController.js | 8 +- src/DatabaseAdapter.js | 79 +- src/ParseServer.js | 54 +- src/PromiseRouter.js | 4 +- src/RestWrite.js | 100 +- src/Routers/FilesRouter.js | 15 +- src/Routers/HooksRouter.js | 17 +- src/rest.js | 6 +- src/testing-routes.js | 1 + 37 files changed, 1738 insertions(+), 1528 deletions(-) create mode 100644 2.3.0.md delete mode 100644 spec/DatabaseAdapter.spec.js delete mode 100644 spec/DatabaseController.spec.js create mode 100644 spec/Uniqueness.spec.js diff --git a/2.3.0.md b/2.3.0.md new file mode 100644 index 0000000000..2528c290b6 --- /dev/null +++ b/2.3.0.md @@ -0,0 +1,82 @@ +# 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 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 exist, 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 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 + +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 }) +``` + +## 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'} // Save the list of duplicates to a new, "_duplicates collection. Remove this line to just output the list. +], {allowDiskUse:true}) +``` diff --git a/package.json b/package.json index 44f6abeab7..9954abee13 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 88ab24b88d..06aef43d36 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1,33 +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({ - 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({ - 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 => { @@ -568,67 +563,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/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(); - }); - }); -}); 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/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 cd6b40e7ce..6696568110 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -3,9 +3,14 @@ '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) }; describe('miscellaneous', function() { it('create a GameScore object', function(done) { @@ -45,14 +50,172 @@ 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) { - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + 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.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 => { + 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); + }); + + 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 => { + 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 => { + 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(); + }); + }); + }); + + 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(() => { + 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(); }); }); @@ -89,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(); @@ -202,14 +365,14 @@ 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'); done(); - }).fail(err => { - fail(err); + }).fail(error => { + fail(JSON.stringify(error)); done(); }) }); @@ -1119,27 +1282,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/ParseHooks.spec.js b/spec/ParseHooks.spec.js index 1129a0507f..1a1349dae2 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); @@ -347,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/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/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/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/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/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 008d544ae4..26b54438bb 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -2,43 +2,33 @@ var request = require('request'); describe("public API", () => { - beforeEach(done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - 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(); - }); + reconfigureServer({ + appName: 'unused', + publicServerURL: 'http://localhost:8378/1', + }) + .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) => { 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); @@ -49,19 +39,8 @@ describe("public API", () => { describe("public API without publicServerURL", () => { beforeEach(done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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) => { @@ -69,14 +48,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..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'); @@ -7,10 +8,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/Uniqueness.spec.js b/spec/Uniqueness.spec.js new file mode 100644 index 0000000000..fe7e1b88b5 --- /dev/null +++ b/spec/Uniqueness.spec.js @@ -0,0 +1,103 @@ +'use strict'; + +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: { + string: { __type: 'String' }, + ptr: { __type: 'Pointer', targetClass: 'UniquePointer' } + } }); + }) + .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(); + }); + }); + + 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/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index cac3c56cfd..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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'My Cool App', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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,33 +580,25 @@ describe("Password Reset", () => { }, sendMail: () => {} } - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'emailing app', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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 df2df591c7..b5d000ef00 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,6 +1,7 @@ +"use strict" // 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'); @@ -10,13 +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 databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; var port = 8378; +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 = { - databaseURI: databaseURI, + databaseAdapter: mongoAdapter, + filesAdapter: gridStoreAdapter, serverURL: 'http://localhost:' + port + '/1', appId: 'test', javascriptKey: 'test', @@ -25,14 +36,13 @@ var defaultConfiguration = { restAPIKey: 'rest', webhookKey: 'hook', masterKey: 'test', - collectionPrefix: 'test_', fileKey: 'test', push: { 'ios': { cert: 'prodCert.pem', key: 'prodKey.pem', production: true, - bundleId: 'bundleId' + bundleId: 'bundleId', } }, oauth: { // Override the facebook provider @@ -43,33 +53,45 @@ 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); -// Prevent reinitializing the server from clobbering Cloud Code -delete defaultConfiguration.cloud; +var server = app.listen(port); +server.on('connection', connection => { + let key = `${connection.remoteAddress}:${connection.remotePort}`; + openConnections[key] = connection; + connection.on('close', () => { delete openConnections[key] }); +}); -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(); - cache.clear(); - app = express(); - api = new ParseServer(configuration); - app.use('/1', api); - server = app.listen(port); -}; +const reconfigureServer = changedConfiguration => { + return new Promise((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 restoreServerConfiguration = () => setServerConfiguration(defaultConfiguration); + 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); + } + }); + }); +} // Set up a Parse client to talk to our test API server var Parse = require('parse/node'); @@ -79,19 +101,35 @@ Parse.serverURL = 'http://localhost:' + port + '/1'; // TODO: update tests to work in an A+ way 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); +beforeEach(done => { + try { + Parse.User.enableUnsafeCurrentUser(); + } catch (error) { + if (error !== 'You need to call Parse.initialize before using Parse.') { + 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' || error.message.startsWith('connect ECONNREFUSED')) { + return; + } else { + fail(error); + return; + } + }) + .then(reconfigureServer) + .then(() => { + Parse.initialize('test', 'test', 'test'); + Parse.serverURL = 'http://localhost:' + port + '/1'; + done(); + }, error => { + fail(JSON.stringify(error)); + done(); + }) }); -var mongoAdapter = new MongoStorageAdapter({ - collectionPrefix: defaultConfiguration.collectionPrefix, - uri: databaseURI, -}) - afterEach(function(done) { Parse.Cloud._removeAllHooks(); mongoAdapter.getAllSchemas() @@ -111,11 +149,13 @@ afterEach(function(done) { }) .then(() => Parse.User.logOut()) .then(() => { - return TestUtils.destroyAllDataPermanently(); - }).then(() => { + if (Object.keys(openConnections).length > 0) { + fail('There were open connections to the server left after the test finished'); + } done(); - }, (error) => { - console.log('error in clearData', error); + }) + .catch(error => { + fail(JSON.stringify(error)); done(); }); }); @@ -243,14 +283,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; } @@ -272,7 +314,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 6ea648423a..b368dccb3e 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'); @@ -5,12 +6,23 @@ 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!'); - 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!'); - 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 => { @@ -38,47 +50,29 @@ describe('server', () => { }); it('fails if database is unreachable', done => { - setServerConfiguration({ - databaseURI: 'mongodb://fake:fake@ds043605.mongolab.com:43605/drew3', - serverURL: 'http://localhost:8378/1', - appId: 'test', - javascriptKey: 'test', - dotNetKey: 'windows', - 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 - 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' }) }) + .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', + 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({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: 'test_', - fileKey: 'test', verifyUserEmails: true, emailAdapter: MockEmailAdapterWithOptions({ fromAddress: 'parse@example.com', @@ -86,22 +80,12 @@ describe('server', () => { domain: 'd', }), publicServerURL: 'http://localhost:8378/1' - }); - done(); + }).then(done, fail); }); it('can load email adapter via class', done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: 'test_', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { class: MockEmailAdapterWithOptions, @@ -112,22 +96,12 @@ describe('server', () => { } }, publicServerURL: 'http://localhost:8378/1' - }); - done(); + }).then(done, fail); }); it('can load email adapter via module name', done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: 'test_', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { module: 'parse-server-simple-mailgun-adapter', @@ -138,41 +112,25 @@ describe('server', () => { } }, publicServerURL: 'http://localhost:8378/1' - }); - done(); + }).then(done, fail); }); it('can load email adapter via only module name', done => { - expect(() => setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: '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 => { - expect(() => setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', + reconfigureServer({ appName: 'unused', - javascriptKey: 'test', - dotNetKey: 'windows', - clientKey: 'client', - restAPIKey: 'rest', - masterKey: 'test', - collectionPrefix: 'test_', - fileKey: 'test', verifyUserEmails: true, emailAdapter: { module: 'parse-server-simple-mailgun-adapter', @@ -181,8 +139,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 => { @@ -199,62 +160,71 @@ describe('server', () => { }) }); - it('can create a parse-server', done => { - var parseServer = new ParseServer.default({ + it('can create a parse-server v1', done => { + var parseServer = new ParseServer.default(Object.assign({}, + defaultConfiguration, { appId: "aTestApp", masterKey: "aTestMasterKey", serverURL: "http://localhost:12666/parse", - databaseURI: 'mongodb://localhost:27017/aTestApp' - }); + __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 => { - var parseServer = ParseServer.ParseServer({ + it('can create a parse-server v2', done => { + let objId; + let server + let parseServer = ParseServer.ParseServer(Object.assign({}, + defaultConfiguration, { appId: "anOtherTestApp", masterKey: "anOtherTestMasterKey", serverURL: "http://localhost:12667/parse", - databaseURI: 'mongodb://localhost:27017/anotherTstApp' - }); + __indexBuildCompletionCallbackForTests: promise => { + promise + .then(() => { + expect(Parse.applicationId).toEqual("anOtherTestApp"); + let app = express(); + app.use('/parse', parseServer); - 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(); - }) + 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(); + }); + }} + )); }); it('has createLiveQueryServer', done => { @@ -273,96 +243,65 @@ describe('server', () => { }); it('properly gives publicServerURL when set', done => { - setServerConfiguration({ - 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({ - 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({ - 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' }) + .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) => { - expect(() => setServerConfiguration({ - 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 => { + expect(error).toEqual('Session length must be a valid number.'); + done(); + }); }); - it('fails if the session length is less than or equal to 0', (done) => { - expect(() => setServerConfiguration({ - 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({ - 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) => { - expect(() => setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', - appName: 'unused', - javascriptKey: 'test', - masterKey: 'test', + reconfigureServer({ sessionLength: '-33', expireInactiveSessions: false - })).not.toThrow(); - - expect(() => setServerConfiguration({ - 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 => { - expect(() => setServerConfiguration({ revokeSessionOnPasswordReset: 'non-bool' })).toThrow(); - done(); + reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }) + .catch(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; } diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index bf41582b19..c494869bad 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -83,6 +83,18 @@ 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); + } else { + 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 8c95c33879..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; } @@ -102,9 +103,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 +181,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; }); } @@ -236,6 +237,28 @@ 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. Undefined doesn't count against uniqueness, + // which is why we use sparse indexes. + ensureUniqueness(className, fieldNames, schema) { + let indexCreationRequest = {}; + let mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema)); + mongoFieldNames.forEach(fieldName => { + indexCreationRequest[fieldName] = 1; + }); + return this.adaptiveCollection(className) + .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; + } + }); + } + // Used in tests _rawFind(className, query) { return this.adaptiveCollection(className).then(collection => collection.find(query)); diff --git a/src/Config.js b/src/Config.js index 8da1b70cfd..ffdf078cc2 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); @@ -55,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/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index de9aa5955c..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(); }; @@ -87,8 +80,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(); }; @@ -417,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/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/DatabaseAdapter.js b/src/DatabaseAdapter.js index 25ce69b422..88fcbe4280 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -1,74 +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'; - -let dbConnections = {}; -let appDatabaseURIs = {}; -let appDatabaseOptions = {}; - -function setAppDatabaseURI(appId, uri) { - appDatabaseURIs[appId] = uri; -} - -function setAppDatabaseOptions(appId: string, options: Object) { - appDatabaseOptions[appId] = options; -} - -//Used by tests -function clearDatabaseSettings() { - appDatabaseURIs = {}; - dbConnections = {}; - appDatabaseOptions = {}; -} +import AppCache from './cache'; //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); + // 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'; } -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}); - - return dbConnections[appId]; -} - -module.exports = { - getDatabaseConnection: getDatabaseConnection, - setAppDatabaseOptions: setAppDatabaseOptions, - setAppDatabaseURI: setAppDatabaseURI, - clearDatabaseSettings: clearDatabaseSettings, - destroyAllDataPermanently: destroyAllDataPermanently, -}; +module.exports = { destroyAllDataPermanently }; diff --git a/src/ParseServer.js b/src/ParseServer.js index 2876cd840a..b823b00615 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,34 @@ 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 (!filesAdapter && !databaseURI) { + throw 'When using an explicit database adapter, you must also use and explicit filesAdapter.'; + } + if (logsFolder) { configureLogger({ logsFolder }) } - if (databaseOptions) { - DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions); - } - - DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); - if (cloud) { addParseCloud(); if (typeof cloud === 'function') { @@ -168,10 +187,23 @@ 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 => { + logger.warn('Unable to ensure uniqueness for usernames: ', error); + return Promise.reject(); + }); + + let emailUniqueness = databaseController.adapter.ensureUniqueness('_User', ['email'], requiredUserFields) + .catch(error => { + logger.warn('Unabled to ensure uniqueness for user email addresses: ', error); + return Promise.reject(); + }) AppCache.put(appId, { masterKey: masterKey, @@ -200,7 +232,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 @@ -211,6 +244,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() { 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 fc40a71031..78478025ab 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -105,9 +105,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(); } }; @@ -119,7 +119,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, @@ -309,7 +309,7 @@ RestWrite.prototype.handleAuthData = function(authData) { } } } - return Promise.resolve(); + return; }); } @@ -356,45 +356,43 @@ RestWrite.prototype.transformUser = function() { } 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'); - } - return Promise.resolve(); - }); - }).then(() => { + 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; + }); + }) + .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 + // 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'); - } - 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(); - }) - }); + 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.'); + } + // We updated the email, send a new validation + this.storage['sendVerificationEmail'] = true; + this.config.userController.setEmailVerifyToken(this.data); + }); + }) }; RestWrite.prototype.createSessionTokenIfNeeded = function() { @@ -577,7 +575,7 @@ RestWrite.prototype.handleInstallation = function() { 'deviceType may not be changed in this ' + 'operation'); } - return Promise.resolve(); + return; }); }); } @@ -762,6 +760,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 new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + }); + }) .then(response => { response.objectId = this.data.objectId; response.createdAt = this.data.createdAt; 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 +} 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)); 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; }); } 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,