InstallationId didn't get passed correctly. Resulting in _Session without installationId https://github.com/parse-community/parse-server/blob/master/src/Routers/UsersRouter.js#L263 * Fixed error with POST /login and req.query is undefined
697 lines
21 KiB
JavaScript
697 lines
21 KiB
JavaScript
const ParseServerRESTController = require('../lib/ParseServerRESTController')
|
|
.ParseServerRESTController;
|
|
const ParseServer = require('../lib/ParseServer').default;
|
|
const Parse = require('parse/node').Parse;
|
|
const TestUtils = require('../lib/TestUtils');
|
|
const semver = require('semver');
|
|
|
|
let RESTController;
|
|
|
|
describe('ParseServerRESTController', () => {
|
|
beforeEach(() => {
|
|
RESTController = ParseServerRESTController(
|
|
Parse.applicationId,
|
|
ParseServer.promiseRouter({ appId: Parse.applicationId })
|
|
);
|
|
});
|
|
|
|
it('should handle a get request', done => {
|
|
RESTController.request('GET', '/classes/MyObject').then(
|
|
res => {
|
|
expect(res.results.length).toBe(0);
|
|
done();
|
|
},
|
|
err => {
|
|
console.log(err);
|
|
jfail(err);
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('should handle a get request with full serverURL mount path', done => {
|
|
RESTController.request('GET', '/1/classes/MyObject').then(
|
|
res => {
|
|
expect(res.results.length).toBe(0);
|
|
done();
|
|
},
|
|
err => {
|
|
jfail(err);
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('should handle a POST batch without transaction', done => {
|
|
RESTController.request('POST', 'batch', {
|
|
requests: [
|
|
{
|
|
method: 'GET',
|
|
path: '/classes/MyObject',
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/classes/MyObject',
|
|
body: { key: 'value' },
|
|
},
|
|
{
|
|
method: 'GET',
|
|
path: '/classes/MyObject',
|
|
},
|
|
],
|
|
}).then(
|
|
res => {
|
|
expect(res.length).toBe(3);
|
|
done();
|
|
},
|
|
err => {
|
|
jfail(err);
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('should handle a POST batch with transaction=false', done => {
|
|
RESTController.request('POST', 'batch', {
|
|
requests: [
|
|
{
|
|
method: 'GET',
|
|
path: '/classes/MyObject',
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/classes/MyObject',
|
|
body: { key: 'value' },
|
|
},
|
|
{
|
|
method: 'GET',
|
|
path: '/classes/MyObject',
|
|
},
|
|
],
|
|
transaction: false,
|
|
}).then(
|
|
res => {
|
|
expect(res.length).toBe(3);
|
|
done();
|
|
},
|
|
err => {
|
|
jfail(err);
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
if (
|
|
(semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
|
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
|
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger') ||
|
|
process.env.PARSE_SERVER_TEST_DB === 'postgres'
|
|
) {
|
|
describe('transactions', () => {
|
|
beforeAll(async () => {
|
|
if (
|
|
semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') &&
|
|
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
|
|
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
|
|
) {
|
|
await reconfigureServer({
|
|
databaseAdapter: undefined,
|
|
databaseURI:
|
|
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
|
|
});
|
|
}
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await TestUtils.destroyAllDataPermanently(true);
|
|
});
|
|
|
|
it('should handle a batch request with transaction = true', done => {
|
|
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
|
|
myObject
|
|
.save()
|
|
.then(() => {
|
|
return myObject.destroy();
|
|
})
|
|
.then(() => {
|
|
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
|
|
|
RESTController.request('POST', 'batch', {
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value2' },
|
|
},
|
|
],
|
|
transaction: true,
|
|
}).then(response => {
|
|
expect(response.length).toEqual(2);
|
|
expect(response[0].success.objectId).toBeDefined();
|
|
expect(response[0].success.createdAt).toBeDefined();
|
|
expect(response[1].success.objectId).toBeDefined();
|
|
expect(response[1].success.createdAt).toBeDefined();
|
|
const query = new Parse.Query('MyObject');
|
|
query.find().then(results => {
|
|
expect(databaseAdapter.createObject.calls.count()).toBe(2);
|
|
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toBe(
|
|
databaseAdapter.createObject.calls.argsFor(1)[3]
|
|
);
|
|
expect(
|
|
results.map(result => result.get('key')).sort()
|
|
).toEqual(['value1', 'value2']);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should not save anything when one operation fails in a transaction', done => {
|
|
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
|
|
myObject
|
|
.save()
|
|
.then(() => {
|
|
return myObject.destroy();
|
|
})
|
|
.then(() => {
|
|
RESTController.request('POST', 'batch', {
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 10 },
|
|
},
|
|
],
|
|
transaction: true,
|
|
}).catch(error => {
|
|
expect(error).toBeDefined();
|
|
const query = new Parse.Query('MyObject');
|
|
query.find().then(results => {
|
|
expect(results.length).toBe(0);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should generate separate session for each call', async () => {
|
|
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
|
|
await myObject.save();
|
|
await myObject.destroy();
|
|
|
|
const myObject2 = new Parse.Object('MyObject2'); // This is important because transaction only works on pre-existing collections
|
|
await myObject2.save();
|
|
await myObject2.destroy();
|
|
|
|
spyOn(databaseAdapter, 'createObject').and.callThrough();
|
|
|
|
let myObjectCalls = 0;
|
|
Parse.Cloud.beforeSave('MyObject', async () => {
|
|
myObjectCalls++;
|
|
if (myObjectCalls === 2) {
|
|
try {
|
|
await RESTController.request('POST', 'batch', {
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject2',
|
|
body: { key: 10 },
|
|
},
|
|
],
|
|
transaction: true,
|
|
});
|
|
fail('should fail');
|
|
} catch (e) {
|
|
expect(e).toBeDefined();
|
|
}
|
|
}
|
|
});
|
|
|
|
const response = await RESTController.request('POST', 'batch', {
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value2' },
|
|
},
|
|
],
|
|
transaction: true,
|
|
});
|
|
|
|
expect(response.length).toEqual(2);
|
|
expect(response[0].success.objectId).toBeDefined();
|
|
expect(response[0].success.createdAt).toBeDefined();
|
|
expect(response[1].success.objectId).toBeDefined();
|
|
expect(response[1].success.createdAt).toBeDefined();
|
|
|
|
await RESTController.request('POST', 'batch', {
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject3',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject3',
|
|
body: { key: 'value2' },
|
|
},
|
|
],
|
|
});
|
|
|
|
const query = new Parse.Query('MyObject');
|
|
const results = await query.find();
|
|
expect(results.map(result => result.get('key')).sort()).toEqual([
|
|
'value1',
|
|
'value2',
|
|
]);
|
|
|
|
const query2 = new Parse.Query('MyObject2');
|
|
const results2 = await query2.find();
|
|
expect(results2.length).toEqual(0);
|
|
|
|
const query3 = new Parse.Query('MyObject3');
|
|
const results3 = await query3.find();
|
|
expect(results3.map(result => result.get('key')).sort()).toEqual([
|
|
'value1',
|
|
'value2',
|
|
]);
|
|
|
|
expect(databaseAdapter.createObject.calls.count()).toBe(13);
|
|
let transactionalSession;
|
|
let transactionalSession2;
|
|
let myObjectDBCalls = 0;
|
|
let myObject2DBCalls = 0;
|
|
let myObject3DBCalls = 0;
|
|
for (let i = 0; i < 13; i++) {
|
|
const args = databaseAdapter.createObject.calls.argsFor(i);
|
|
switch (args[0]) {
|
|
case 'MyObject':
|
|
myObjectDBCalls++;
|
|
if (!transactionalSession) {
|
|
transactionalSession = args[3];
|
|
} else {
|
|
expect(transactionalSession).toBe(args[3]);
|
|
}
|
|
if (transactionalSession2) {
|
|
expect(transactionalSession2).not.toBe(args[3]);
|
|
}
|
|
break;
|
|
case 'MyObject2':
|
|
myObject2DBCalls++;
|
|
if (!transactionalSession2) {
|
|
transactionalSession2 = args[3];
|
|
} else {
|
|
expect(transactionalSession2).toBe(args[3]);
|
|
}
|
|
if (transactionalSession) {
|
|
expect(transactionalSession).not.toBe(args[3]);
|
|
}
|
|
break;
|
|
case 'MyObject3':
|
|
myObject3DBCalls++;
|
|
expect(args[3]).toEqual(null);
|
|
break;
|
|
}
|
|
}
|
|
expect(myObjectDBCalls).toEqual(2);
|
|
expect(myObject2DBCalls).toEqual(9);
|
|
expect(myObject3DBCalls).toEqual(2);
|
|
});
|
|
});
|
|
}
|
|
|
|
it('should handle a POST request', done => {
|
|
RESTController.request('POST', '/classes/MyObject', { key: 'value' })
|
|
.then(() => {
|
|
return RESTController.request('GET', '/classes/MyObject');
|
|
})
|
|
.then(res => {
|
|
expect(res.results.length).toBe(1);
|
|
expect(res.results[0].key).toEqual('value');
|
|
done();
|
|
})
|
|
.catch(err => {
|
|
console.log(err);
|
|
jfail(err);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should handle a POST request with context', async () => {
|
|
Parse.Cloud.beforeSave('MyObject', req => {
|
|
expect(req.context.a).toEqual('a');
|
|
});
|
|
Parse.Cloud.afterSave('MyObject', req => {
|
|
expect(req.context.a).toEqual('a');
|
|
});
|
|
|
|
await RESTController.request(
|
|
'POST',
|
|
'/classes/MyObject',
|
|
{ key: 'value' },
|
|
{ context: { a: 'a' } }
|
|
);
|
|
});
|
|
|
|
it('ensures sessionTokens are properly handled', done => {
|
|
let userId;
|
|
Parse.User.signUp('user', 'pass')
|
|
.then(user => {
|
|
userId = user.id;
|
|
const sessionToken = user.getSessionToken();
|
|
return RESTController.request('GET', '/users/me', undefined, {
|
|
sessionToken,
|
|
});
|
|
})
|
|
.then(res => {
|
|
// Result is in JSON format
|
|
expect(res.objectId).toEqual(userId);
|
|
done();
|
|
})
|
|
.catch(err => {
|
|
console.log(err);
|
|
jfail(err);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('ensures masterKey is properly handled', done => {
|
|
let userId;
|
|
Parse.User.signUp('user', 'pass')
|
|
.then(user => {
|
|
userId = user.id;
|
|
return Parse.User.logOut().then(() => {
|
|
return RESTController.request('GET', '/classes/_User', undefined, {
|
|
useMasterKey: true,
|
|
});
|
|
});
|
|
})
|
|
.then(
|
|
res => {
|
|
expect(res.results.length).toBe(1);
|
|
expect(res.results[0].objectId).toEqual(userId);
|
|
done();
|
|
},
|
|
err => {
|
|
jfail(err);
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('ensures no user is created when passing an empty username', done => {
|
|
RESTController.request('POST', '/classes/_User', {
|
|
username: '',
|
|
password: 'world',
|
|
}).then(
|
|
() => {
|
|
jfail(
|
|
new Error(
|
|
'Success callback should not be called when passing an empty username.'
|
|
)
|
|
);
|
|
done();
|
|
},
|
|
err => {
|
|
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
|
|
expect(err.message).toBe('bad or missing username');
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('ensures no user is created when passing an empty password', done => {
|
|
RESTController.request('POST', '/classes/_User', {
|
|
username: 'hello',
|
|
password: '',
|
|
}).then(
|
|
() => {
|
|
jfail(
|
|
new Error(
|
|
'Success callback should not be called when passing an empty password.'
|
|
)
|
|
);
|
|
done();
|
|
},
|
|
err => {
|
|
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
|
|
expect(err.message).toBe('password is required');
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('ensures no session token is created on creating users', done => {
|
|
RESTController.request('POST', '/classes/_User', {
|
|
username: 'hello',
|
|
password: 'world',
|
|
})
|
|
.then(user => {
|
|
expect(user.sessionToken).toBeUndefined();
|
|
const query = new Parse.Query('_Session');
|
|
return query.find({ useMasterKey: true });
|
|
})
|
|
.then(sessions => {
|
|
expect(sessions.length).toBe(0);
|
|
done();
|
|
}, done.fail);
|
|
});
|
|
|
|
it('ensures a session token is created when passing installationId != cloud', done => {
|
|
RESTController.request(
|
|
'POST',
|
|
'/classes/_User',
|
|
{ username: 'hello', password: 'world' },
|
|
{ installationId: 'my-installation' }
|
|
)
|
|
.then(user => {
|
|
expect(user.sessionToken).not.toBeUndefined();
|
|
const query = new Parse.Query('_Session');
|
|
return query.find({ useMasterKey: true });
|
|
})
|
|
.then(
|
|
sessions => {
|
|
expect(sessions.length).toBe(1);
|
|
expect(sessions[0].get('installationId')).toBe('my-installation');
|
|
done();
|
|
},
|
|
err => {
|
|
jfail(err);
|
|
done();
|
|
}
|
|
);
|
|
});
|
|
|
|
it('ensures logIn is saved with installationId', async () => {
|
|
const installationId = 'installation123';
|
|
const user = await RESTController.request(
|
|
'POST',
|
|
'/classes/_User',
|
|
{ username: 'hello', password: 'world' },
|
|
{ installationId }
|
|
);
|
|
expect(user.sessionToken).not.toBeUndefined();
|
|
const query = new Parse.Query('_Session');
|
|
let sessions = await query.find({ useMasterKey: true });
|
|
|
|
expect(sessions.length).toBe(1);
|
|
expect(sessions[0].get('installationId')).toBe(installationId);
|
|
expect(sessions[0].get('sessionToken')).toBe(user.sessionToken);
|
|
|
|
const loggedUser = await RESTController.request(
|
|
'POST',
|
|
'/login',
|
|
{ username: 'hello', password: 'world' },
|
|
{ installationId }
|
|
);
|
|
expect(loggedUser.sessionToken).not.toBeUndefined();
|
|
sessions = await query.find({ useMasterKey: true });
|
|
|
|
// Should clean up old sessions with this installationId
|
|
expect(sessions.length).toBe(1);
|
|
expect(sessions[0].get('installationId')).toBe(installationId);
|
|
expect(sessions[0].get('sessionToken')).toBe(loggedUser.sessionToken);
|
|
});
|
|
});
|