-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Readonly trigger for _Session hook #5155
Changes from 18 commits
a78aad8
be84afa
7e88128
ce60635
49016ee
8327882
cd582d1
e821e4c
8b0a342
a1d8861
7dd5151
cdb82b4
defcde6
892bc17
104f612
8b88268
7a651e6
56dc2e4
2716adb
d52a577
9a9dbc8
999ac90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
'use strict'; | ||
const Parse = require('parse/node'); | ||
|
||
describe('CloudCode _Session Trigger tests', () => { | ||
it('beforeSave should be okay', async () => { | ||
Parse.Cloud.beforeSave('_Session', async req => { | ||
const sessionObject = req.object; | ||
expect(sessionObject).toBeDefined(); | ||
expect(sessionObject.get('sessionToken')).toBeDefined(); | ||
expect(sessionObject.get('createdWith')).toBeDefined(); | ||
expect(sessionObject.get('user')).toBeDefined(); | ||
expect(sessionObject.get('user')).toEqual(jasmine.any(Parse.User)); | ||
}); | ||
try { | ||
// signup a user (internally creates a session) | ||
const user = new Parse.User(); | ||
user.setUsername('some-user-name'); | ||
user.setPassword('password'); | ||
await user.signUp(); | ||
} catch (error) { | ||
throw error; | ||
} | ||
}); | ||
it('beforeSave should discard any changes', async () => { | ||
georgesjamous marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Parse.Cloud.beforeSave('_Session', function(req) { | ||
// perform some changes | ||
req.object.set('KeyA', 'EDITED_VALUE'); | ||
req.object.set('KeyB', 'EDITED_VALUE'); | ||
}); | ||
// signup a user (internally creates a session) | ||
const user = new Parse.User(); | ||
user.setUsername('some-user-name'); | ||
user.setPassword('password'); | ||
await user.signUp(); | ||
// get the session | ||
const query = new Parse.Query('_Session'); | ||
query.equalTo('user', user); | ||
const sessionObject = await query.first({ | ||
useMasterKey: true, | ||
}); | ||
// expect un-edited object | ||
expect(sessionObject.get('KeyA')).toBeUndefined(); | ||
expect(sessionObject.get('KeyB')).toBeUndefined(); | ||
expect(sessionObject.get('user')).toBeDefined(); | ||
expect(sessionObject.get('user').id).toBe(user.id); | ||
expect(sessionObject.get('sessionToken')).toBeDefined(); | ||
}); | ||
it('beforeSave rejection should not affect user creation flow during signup', async () => { | ||
let user; | ||
Parse.Cloud.beforeSave('_Session', async () => { | ||
// reject the session | ||
throw new Parse.Error(12345678, 'Sorry, more steps are required'); | ||
}); | ||
Parse.Cloud.beforeSave('_User', async req => { | ||
// make sure this runs correctly | ||
req.object.set('firstName', 'abcd'); | ||
}); | ||
Parse.Cloud.afterSave('_User', async req => { | ||
if (req.object.has('lastName')) { | ||
return; | ||
} | ||
// make sure this runs correctly | ||
req.object.set('lastName', '1234'); | ||
await req.object.save({}, { useMasterKey: true }); | ||
}); | ||
try { | ||
user = new Parse.User(); | ||
user.setUsername('user-name'); | ||
user.setPassword('user-password'); | ||
await user.signUp(); | ||
} catch (error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unnecessary |
||
throw error; | ||
} | ||
expect(user.getSessionToken()).toBeUndefined(); | ||
await delay(200); // just so that afterSave has time to run | ||
await user.fetch({ useMasterKey: true }); | ||
expect(user.get('username')).toBe('user-name'); | ||
expect(user.get('firstName')).toBe('abcd'); | ||
expect(user.get('lastName')).toBe('1234'); | ||
// get the session | ||
const query2 = new Parse.Query('_Session'); | ||
query2.equalTo('user', user); | ||
const sessionObject = await query2.first({ | ||
useMasterKey: true, | ||
}); | ||
expect(sessionObject).toBeUndefined(); | ||
}); | ||
it('beforeSave should fail and prevent login on throw', async () => { | ||
Parse.Cloud.beforeSave('_Session', async req => { | ||
const sessionObject = req.object; | ||
if (sessionObject.get('createdWith').action === 'login') { | ||
throw new Parse.Error(12345678, 'Sorry, you cant login :('); | ||
} | ||
}); | ||
const user = new Parse.User(); | ||
user.setUsername('some-username'); | ||
user.setPassword('password'); | ||
await user.signUp(); | ||
await Parse.User.logOut(); | ||
try { | ||
await Parse.User.logIn('some-username', 'password'); | ||
} catch (error) { | ||
expect(error.code).toBe(12345678); | ||
expect(error.message).toBe('Sorry, you cant login :('); | ||
georgesjamous marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
// make sure no session was created | ||
const query = new Parse.Query('_Session'); | ||
query.equalTo('user', user); | ||
const sessionObject = await query.first({ | ||
useMasterKey: true, | ||
}); | ||
expect(sessionObject).toBeUndefined(); | ||
}); | ||
it('beforeDelete should ignore thrown errors', async () => { | ||
Parse.Cloud.beforeDelete('_Session', async () => { | ||
throw new Parse.Error(12345678, 'Nop'); | ||
}); | ||
const user = new Parse.User(); | ||
user.setUsername('some-user-name'); | ||
user.setPassword('password'); | ||
try { | ||
await user.signUp(); | ||
await user.destroy({ useMasterKey: true }); | ||
} catch (error) { | ||
georgesjamous marked this conversation as resolved.
Show resolved
Hide resolved
|
||
throw error; | ||
} | ||
try { | ||
await user.fetch({ useMasterKey: true }); | ||
throw 'User should have been deleted.'; | ||
} catch (error) { | ||
expect(error.code).toBe(Parse.Error.OBJECT_NOT_FOUND); | ||
} | ||
}); | ||
it('afterDelete should work normally', async () => { | ||
Parse.Cloud.afterDelete('_Session', function() { | ||
const someObject = new Parse.Object('Test'); | ||
someObject.set('key', 'value'); | ||
someObject.save(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can also use a simple spy or counter to ensure the function has been called. This makes the test suite faster :) |
||
}); | ||
const user = new Parse.User(); | ||
user.setUsername('some-user-name'); | ||
user.setPassword('password'); | ||
await user.signUp(); | ||
await Parse.User.logOut(); | ||
await delay(200); | ||
const query = new Parse.Query('Test'); | ||
const object = await query.first({ | ||
useMasterKey: true, | ||
}); | ||
expect(object).toBeDefined(); | ||
expect(object.get('key')).toBe('value'); | ||
}); | ||
it('afterSave should work normally', async () => { | ||
Parse.Cloud.afterSave('_Session', function() { | ||
const someObject = new Parse.Object('Test'); | ||
someObject.set('key', 'value'); | ||
someObject.save(); | ||
}); | ||
const user = new Parse.User(); | ||
user.setUsername('some-user-name'); | ||
user.setPassword('password'); | ||
await user.signUp(); | ||
await delay(200); | ||
const query = new Parse.Query('Test'); | ||
const object = await query.first({ | ||
useMasterKey: true, | ||
}); | ||
expect(object).toBeDefined(); | ||
expect(object.get('key')).toBe('value'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,8 +8,12 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = | |
jasmine.getEnv().clearReporters(); | ||
jasmine.getEnv().addReporter( | ||
new SpecReporter({ | ||
colors: { enabled: supportsColor.stdout }, | ||
spec: { displayDuration: true }, | ||
colors: { | ||
enabled: supportsColor.stdout, | ||
}, | ||
spec: { | ||
displayDuration: true, | ||
}, | ||
}) | ||
); | ||
|
||
|
@@ -299,12 +303,15 @@ function createTestUser() { | |
function ok(bool, message) { | ||
expect(bool).toBeTruthy(message); | ||
} | ||
|
||
function equal(a, b, message) { | ||
expect(a).toEqual(b, message); | ||
} | ||
|
||
function strictEqual(a, b, message) { | ||
expect(a).toBe(b, message); | ||
} | ||
|
||
function notEqual(a, b, message) { | ||
expect(a).not.toEqual(b, message); | ||
} | ||
|
@@ -387,6 +394,11 @@ function mockShortLivedAuth() { | |
return auth; | ||
} | ||
|
||
// delay | ||
function delay(millis) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this function is declared at many places already, do you plan to reuse it everywhere? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will check if i can consolidate them all here. |
||
return new Promise(r => setTimeout(r, millis)); | ||
} | ||
|
||
// This is polluting, but, it makes it way easier to directly port old tests. | ||
global.Parse = Parse; | ||
global.TestObject = TestObject; | ||
|
@@ -404,6 +416,7 @@ global.range = range; | |
global.reconfigureServer = reconfigureServer; | ||
global.defaultConfiguration = defaultConfiguration; | ||
global.mockFacebookAuthenticator = mockFacebookAuthenticator; | ||
global.delay = delay; | ||
global.jfail = function(err) { | ||
fail(JSON.stringify(err)); | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -741,11 +741,23 @@ RestWrite.prototype.createSessionToken = function() { | |
installationId: this.auth.installationId, | ||
}); | ||
|
||
if (this.response && this.response.response) { | ||
this.response.response.sessionToken = sessionData.sessionToken; | ||
} | ||
|
||
return createSession(); | ||
return createSession() | ||
.then(() => { | ||
// session was created, populate the sessionToken | ||
if (this.response && this.response.response) { | ||
this.response.response.sessionToken = sessionData.sessionToken; | ||
} | ||
}) | ||
.catch(e => { | ||
// we need to swallow the error during a Signup | ||
// this is to make sure the .execute() chain continues normally for the user. | ||
// we dont need to kwow about any errors when signing a user up. | ||
if (!this.storage['authProvider']) { | ||
return Promise.resolve(); | ||
} | ||
// on login proppagate errors | ||
throw e; | ||
}); | ||
}; | ||
|
||
// Delete email reset tokens if user is changing password or email. | ||
|
@@ -1482,19 +1494,34 @@ RestWrite.prototype.objectId = function() { | |
}; | ||
|
||
// Returns a copy of the data and delete bad keys (_auth_data, _hashed_password...) | ||
RestWrite.prototype.sanitizedData = function() { | ||
RestWrite.prototype.sanitizedData = function(decodeData = false) { | ||
const data = Object.keys(this.data).reduce((data, key) => { | ||
// Regexp comes from Parse.Object.prototype.validate | ||
if (!/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) { | ||
delete data[key]; | ||
} | ||
return data; | ||
}, deepcopy(this.data)); | ||
return Parse._decode(undefined, data); | ||
if (decodeData) { | ||
// decoded data might contain instances of ParseObject, ParseRelation, ParseACl... | ||
return Parse._decode(undefined, data); | ||
} | ||
// data is kept in json format. Not decoded | ||
return data; | ||
}; | ||
|
||
// Returns an updated copy of the object | ||
RestWrite.prototype.buildUpdatedObject = function(extraData) { | ||
if (this.className === '_Session') { | ||
// on Session, 'updatedObject' will be an instance of 'ParseSession'. | ||
// 'ParseSession' prevents setting readOnlyKeys and in turn '.set' fails. | ||
// So, 'beforeSave' on session will show the full object in req.object with | ||
// req.object.dirtyKeys being empty []. | ||
// This is okay, since sessions are mostly never updated, only created or destroyed. | ||
// Additionally sanitizedData should be kept in json, not decoded. | ||
// because '.inflate' internally uses '.fromJSON' so it expects data to be JSON to work properly. | ||
return triggers.inflate(extraData, this.sanitizedData(false)); | ||
} | ||
const updatedObject = triggers.inflate(extraData, this.originalData); | ||
Object.keys(this.data).reduce(function(data, key) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did not really understand what is the purpose of this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please revert to const |
||
if (key.indexOf('.') > 0) { | ||
|
@@ -1511,8 +1538,7 @@ RestWrite.prototype.buildUpdatedObject = function(extraData) { | |
} | ||
return data; | ||
}, deepcopy(this.data)); | ||
|
||
updatedObject.set(this.sanitizedData()); | ||
updatedObject.set(this.sanitizedData(true)); | ||
return updatedObject; | ||
}; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This try / catch is not necessary