Files
kami-parse-server/spec/MongoStorageAdapter.spec.js
Arthur Cinader fd0b535159 Case insensitive signup (#5634)
* Always delete data after each, even for mongo.

* Add failing simple case test

* run all tests

* 1. when validating username be case insensitive

2. add _auth_data_anonymous to specialQueryKeys...whatever that is!

* More case sensitivity

1. also make email validation case insensitive
2. update comments to reflect what this change does

* wordsmithery and grammar

* first pass at a preformant case insensitive query.  mongo only so far.

* change name of parameter from insensitive to
caseInsensitive

* Postgres support

* properly handle auth data null

* wip

* use 'caseInsensitive' instead of 'insensitive' in all places.

* update commenet to reclect current plan

* skip the mystery test for now

* create case insensitive indecies for
mongo to support case insensitive
checks for email and username

* remove unneeded specialKey

* pull collation out to a function.

* not sure what i planned
to do with this test.
removing.

* remove typo

* remove another unused flag

* maintain order

* maintain order of params

* boil the ocean on param sequence
i like having explain last cause it seems
like something you would
change/remove after getting what you want
from the explain?

* add test to verify creation
and use of caseInsensitive index

* add no op func to prostgress

* get collation object from mongocollection
make flow lint happy by declaring things Object.

* fix typo

* add changelog

* kick travis

* properly reference static method

* add a test to confirm that anonymous users with
unique username that do collide when compared
insensitively can still be created.

* minot doc nits

* add a few tests to make sure our spy is working as expected
wordsmith the changelog

Co-authored-by: Diamond Lewis <findlewis@gmail.com>
2020-02-14 09:44:51 -08:00

578 lines
18 KiB
JavaScript

'use strict';
const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter')
.default;
const { MongoClient } = require('mongodb');
const databaseURI =
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
const request = require('../lib/request');
const Config = require('../lib/Config');
const TestUtils = require('../lib/TestUtils');
const fakeClient = {
s: { options: { dbName: null } },
db: () => null,
};
// These tests are specific to the mongo storage adapter + mongo storage format
// and will eventually be moved into their own repo
describe_only_db('mongo')('MongoStorageAdapter', () => {
beforeEach(done => {
new MongoStorageAdapter({ uri: databaseURI })
.deleteAllClasses()
.then(done, fail);
});
it('auto-escapes symbols in auth information', () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(fakeClient));
new MongoStorageAdapter({
uri:
'mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse',
}).connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
jasmine.any(Object)
);
});
it("doesn't double escape already URI-encoded information", () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(fakeClient));
new MongoStorageAdapter({
uri:
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
}).connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse',
jasmine.any(Object)
);
});
// https://github.com/parse-community/parse-server/pull/148#issuecomment-180407057
it('preserves replica sets', () => {
spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(fakeClient));
new MongoStorageAdapter({
uri:
'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415',
}).connect();
expect(MongoClient.connect).toHaveBeenCalledWith(
'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415',
jasmine.any(Object)
);
});
it('stores objectId in _id', done => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
adapter
.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
.then(() => adapter._rawFind('Foo', {}))
.then(results => {
expect(results.length).toEqual(1);
const obj = results[0];
expect(obj._id).toEqual('abcde');
expect(obj.objectId).toBeUndefined();
done();
});
});
it('find succeeds when query is within maxTimeMS', done => {
const maxTimeMS = 250;
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { maxTimeMS },
});
adapter
.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
.then(() =>
adapter._rawFind('Foo', { $where: `sleep(${maxTimeMS / 2})` })
)
.then(
() => done(),
err => {
done.fail(`maxTimeMS should not affect fast queries ${err}`);
}
);
});
it('find fails when query exceeds maxTimeMS', done => {
const maxTimeMS = 250;
const adapter = new MongoStorageAdapter({
uri: databaseURI,
mongoOptions: { maxTimeMS },
});
adapter
.createObject('Foo', { fields: {} }, { objectId: 'abcde' })
.then(() =>
adapter._rawFind('Foo', { $where: `sleep(${maxTimeMS * 2})` })
)
.then(
() => {
done.fail('Find succeeded despite taking too long!');
},
err => {
expect(err.name).toEqual('MongoError');
expect(err.code).toEqual(50);
expect(err.message).toMatch('operation exceeded time limit');
done();
}
);
});
it('stores pointers with a _p_ prefix', done => {
const obj = {
objectId: 'bar',
aPointer: {
__type: 'Pointer',
className: 'JustThePointer',
objectId: 'qwerty',
},
};
const adapter = new MongoStorageAdapter({ uri: databaseURI });
adapter
.createObject(
'APointerDarkly',
{
fields: {
objectId: { type: 'String' },
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
},
},
obj
)
.then(() => adapter._rawFind('APointerDarkly', {}))
.then(results => {
expect(results.length).toEqual(1);
const output = results[0];
expect(typeof output._id).toEqual('string');
expect(typeof output._p_aPointer).toEqual('string');
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
expect(output.aPointer).toBeUndefined();
done();
});
});
it('handles object and subdocument', done => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
const schema = { fields: { subdoc: { type: 'Object' } } };
const obj = { subdoc: { foo: 'bar', wu: 'tan' } };
adapter
.createObject('MyClass', schema, obj)
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('tan');
const obj = { 'subdoc.wu': 'clan' };
return adapter.findOneAndUpdate('MyClass', schema, {}, obj);
})
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('clan');
done();
});
});
it('handles creating an array, object, date', done => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
const obj = {
array: [1, 2, 3],
object: { foo: 'bar' },
date: {
__type: 'Date',
iso: '2016-05-26T20:55:01.154Z',
},
};
const schema = {
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
};
adapter
.createObject('MyClass', schema, obj)
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date instanceof Date).toBe(true);
return adapter.find('MyClass', schema, {}, {});
})
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date.__type).toBe('Date');
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
done();
})
.catch(error => {
console.log(error);
fail();
done();
});
});
it('handles updating a single object with array, object date', done => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
const schema = {
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
};
adapter
.createObject('MyClass', schema, {})
.then(() => adapter._rawFind('MyClass', {}))
.then(results => {
expect(results.length).toEqual(1);
const update = {
array: [1, 2, 3],
object: { foo: 'bar' },
date: {
__type: 'Date',
iso: '2016-05-26T20:55:01.154Z',
},
};
const query = {};
return adapter.findOneAndUpdate('MyClass', schema, query, update);
})
.then(results => {
const mob = results;
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date.__type).toBe('Date');
expect(mob.date.iso).toBe('2016-05-26T20:55:01.154Z');
return adapter._rawFind('MyClass', {});
})
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date instanceof Date).toBe(true);
done();
})
.catch(error => {
console.log(error);
fail();
done();
});
});
it('handleShutdown, close connection', async () => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
const schema = {
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
};
await adapter.createObject('MyClass', schema, {});
const status = await adapter.database.admin().serverStatus();
expect(status.connections.current > 0).toEqual(true);
await adapter.handleShutdown();
try {
await adapter.database.admin().serverStatus();
expect(false).toBe(true);
} catch (e) {
expect(e.message).toEqual('topology was destroyed');
}
});
it('getClass if exists', async () => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
const schema = {
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
};
await adapter.createClass('MyClass', schema);
const myClassSchema = await adapter.getClass('MyClass');
expect(myClassSchema).toBeDefined();
});
it('getClass if not exists', async () => {
const adapter = new MongoStorageAdapter({ uri: databaseURI });
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(
undefined
);
});
it('should use index for caseInsensitive query', async () => {
const user = new Parse.User();
user.set('username', 'Bugs');
user.set('password', 'Bunny');
await user.signUp();
const database = Config.get(Parse.applicationId).database;
const preIndexPlan = await database.find(
'_User',
{ username: 'bugs' },
{ caseInsensitive: true, explain: true }
);
const schema = await new Parse.Schema('_User').get();
await database.adapter.ensureIndex(
'_User',
schema,
['username'],
'case_insensitive_username',
true
);
const postIndexPlan = await database.find(
'_User',
{ username: 'bugs' },
{ caseInsensitive: true, explain: true }
);
expect(preIndexPlan.executionStats.executionStages.stage).toBe('COLLSCAN');
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
});
if (
process.env.MONGODB_VERSION === '4.0.4' &&
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
) {
describe('transactions', () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
beforeAll(async () => {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI:
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
});
});
beforeEach(async () => {
await TestUtils.destroyAllDataPermanently(true);
});
it('should use transaction in a batch with transaction = true', async () => {
const myObject = new Parse.Object('MyObject');
await myObject.save();
const databaseAdapter = Config.get(Parse.applicationId).database
.adapter;
spyOn(
databaseAdapter.database.serverConfig,
'command'
).and.callThrough();
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/batch',
body: JSON.stringify({
requests: [
{
method: 'PUT',
path: '/1/classes/MyObject/' + myObject.id,
body: { myAttribute: 'myValue' },
},
],
transaction: true,
}),
});
let found = false;
databaseAdapter.database.serverConfig.command.calls
.all()
.forEach(call => {
found = true;
expect(call.args[2].session.transaction.state).not.toBe(
'NO_TRANSACTION'
);
});
expect(found).toBe(true);
});
it('should not use transaction in a batch with transaction = false', async () => {
const myObject = new Parse.Object('MyObject');
await myObject.save();
const databaseAdapter = Config.get(Parse.applicationId).database
.adapter;
spyOn(
databaseAdapter.database.serverConfig,
'command'
).and.callThrough();
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/batch',
body: JSON.stringify({
requests: [
{
method: 'PUT',
path: '/1/classes/MyObject/' + myObject.id,
body: { myAttribute: 'myValue' },
},
],
transaction: false,
}),
});
let found = false;
databaseAdapter.database.serverConfig.command.calls
.all()
.forEach(call => {
found = true;
expect(call.args[2].session).toBe(undefined);
});
expect(found).toBe(true);
});
it('should not use transaction in a batch with no transaction option sent', async () => {
const myObject = new Parse.Object('MyObject');
await myObject.save();
const databaseAdapter = Config.get(Parse.applicationId).database
.adapter;
spyOn(
databaseAdapter.database.serverConfig,
'command'
).and.callThrough();
await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1/batch',
body: JSON.stringify({
requests: [
{
method: 'PUT',
path: '/1/classes/MyObject/' + myObject.id,
body: { myAttribute: 'myValue' },
},
],
}),
});
let found = false;
databaseAdapter.database.serverConfig.command.calls
.all()
.forEach(call => {
found = true;
expect(call.args[2].session).toBe(undefined);
});
expect(found).toBe(true);
});
it('should not use transaction in a put request', async () => {
const myObject = new Parse.Object('MyObject');
await myObject.save();
const databaseAdapter = Config.get(Parse.applicationId).database
.adapter;
spyOn(
databaseAdapter.database.serverConfig,
'command'
).and.callThrough();
await request({
method: 'PUT',
headers: headers,
url: 'http://localhost:8378/1/classes/MyObject/' + myObject.id,
body: { myAttribute: 'myValue' },
});
let found = false;
databaseAdapter.database.serverConfig.command.calls
.all()
.forEach(call => {
found = true;
expect(call.args[2].session).toBe(undefined);
});
expect(found).toBe(true);
});
it('should not use transactions when using SDK insert', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database
.adapter;
spyOn(
databaseAdapter.database.serverConfig,
'insert'
).and.callThrough();
const myObject = new Parse.Object('MyObject');
await myObject.save();
const calls = databaseAdapter.database.serverConfig.insert.calls.all();
expect(calls.length).toBeGreaterThan(0);
calls.forEach(call => {
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
});
});
it('should not use transactions when using SDK update', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database
.adapter;
spyOn(
databaseAdapter.database.serverConfig,
'update'
).and.callThrough();
const myObject = new Parse.Object('MyObject');
await myObject.save();
myObject.set('myAttribute', 'myValue');
await myObject.save();
const calls = databaseAdapter.database.serverConfig.update.calls.all();
expect(calls.length).toBeGreaterThan(0);
calls.forEach(call => {
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
});
});
it('should not use transactions when using SDK delete', async () => {
const databaseAdapter = Config.get(Parse.applicationId).database
.adapter;
spyOn(
databaseAdapter.database.serverConfig,
'remove'
).and.callThrough();
const myObject = new Parse.Object('MyObject');
await myObject.save();
await myObject.destroy();
const calls = databaseAdapter.database.serverConfig.remove.calls.all();
expect(calls.length).toBeGreaterThan(0);
calls.forEach(call => {
expect(call.args[2].session.transaction.state).toBe('NO_TRANSACTION');
});
});
});
}
});