Skip to content
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

Closed
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion spec/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"range": true,
"jequal": true,
"create": true,
"arrayContains": true
"arrayContains": true,
"delay": true
},
"rules": {
"no-console": [0],
Expand Down
8 changes: 4 additions & 4 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1950,11 +1950,11 @@ describe('afterFind hooks', () => {

it('should validate triggers correctly', () => {
expect(() => {
Parse.Cloud.beforeSave('_Session', () => {});
}).toThrow('Triggers are not supported for _Session class.');
Parse.Cloud.beforeFind('_Session', () => {});
}).toThrow('beforeFind/afterFind is not allowed on _Session class.');
expect(() => {
Parse.Cloud.afterSave('_Session', () => {});
}).toThrow('Triggers are not supported for _Session class.');
Parse.Cloud.afterFind('_Session', () => {});
}).toThrow('beforeFind/afterFind is not allowed on _Session class.');
expect(() => {
Parse.Cloud.beforeSave('_PushStatus', () => {});
}).toThrow('Only afterSave is allowed on _PushStatus');
Expand Down
171 changes: 171 additions & 0 deletions spec/CloudCodeSessionTrigger.spec.js
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) {
Copy link
Contributor

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

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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();
Copy link
Contributor

Choose a reason for hiding this comment

The 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');
});
});
17 changes: 15 additions & 2 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
})
);

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -387,6 +394,11 @@ function mockShortLivedAuth() {
return auth;
}

// delay
function delay(millis) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
Expand All @@ -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));
};
Expand Down
44 changes: 35 additions & 9 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not really understand what is the purpose of this reduce.
Didn't touch it, but RestWrite.prototype.buildUpdatedObject = function(extraData) { could be fixed to have less duplicated code (like inflating twice in the case of session)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert to const

if (key.indexOf('.') > 0) {
Expand All @@ -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;
};

Expand Down
Loading