Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
260 changes: 258 additions & 2 deletions spec/PublicAPI.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
const req = require('../lib/request');
const Config = require('../lib/Config');

const request = function(url, callback) {
const request = function (url, callback) {
return req({
url,
}).then(response => callback(null, response), err => callback(err, err));
}).then(
response => callback(null, response),
err => callback(err, err)
);
};

describe('public API', () => {
Expand Down Expand Up @@ -208,4 +212,256 @@ describe('public API supplied with invalid application id', () => {
}
);
});

fdescribe('resetPassword', () => {
let makeRequest;
const re = new RegExp('^(?=.*[a-z]).{8,}');
let sendEmailOptions;
const emailAdapter = {
sendVerificationEmail: {},
sendPasswordResetEmail: options => {
sendEmailOptions = options;
},
sendMail: () => {},
};

const serverURL = 'http://localhost:8378/1';

const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Installation-Id': 'yolo',
};

beforeEach(() => {
makeRequest = reconfigureServer({
appName: 'coolapp',
publicServerURL: 'http://localhost:1337/1',
emailAdapter: emailAdapter,
passwordPolicy: {
validatorPattern: re,
doNotAllowUsername: true,
maxPasswordHistory: 1,
resetTokenValidityDuration: 0.5, // 0.5 second
},
}).then(() => {
const config = Config.get('test');
const user = new Parse.User();
user.setPassword('asdsweqwasas');
user.setUsername('test');
user.set('email', 'test@parse.com');
return user
.signUp(null)
.then(() => {
// build history
user.setPassword('aaaaaaaaaaaa');
Comment thread
mtrezza marked this conversation as resolved.
Outdated
return user.save();
})
.then(() => Parse.User.requestPasswordReset('test@parse.com'))
.then(() =>
config.database.adapter.find(
'_User',
{ fields: {} },
{ username: 'test' },
{ limit: 1 }
)
);
});
});

it('Password reset failed due to password policy', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'zxcv',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Parse.Error.VALIDATION_ERROR is generic, there should be another error code like Parse.Error.PASSWORD_POLICY_NOT_MEET
expect(err.data.code).not.toBe(undefined);
expect(err.data.code).toBe(Parse.Error.VALIDATION_ERROR);
done();
}
);
});
});

it('Password reset failed due to invalid token', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'] + 'invalid',
new_password: 'zxcv',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Missing Parse.Error code, only string message, there should be an error code like Parse.Error.RESET_PASSWORD_ERROR
expect(err.data.code).not.toBe(undefined);
done();
}
);
});
});

it('Password reset failed due to password is repeated', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'aaaaaaaaaaaa',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Parse.Error.VALIDATION_ERROR is generic, there should be another error code like Parse.Error.PASSWORD_POLICY_REPEAT
expect(err.data.code).not.toBe(undefined);
expect(err.data.code).toBe(Parse.Error.VALIDATION_ERROR);
done();
}
);
});
});

it('Password reset failed due to it contains username', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'asdsweqwasastest',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Parse.Error.VALIDATION_ERROR is generic, there should be another error code like Parse.Error.PASSWORD_POLICY_USERNAME
expect(err.data.code).not.toBe(undefined);
expect(err.data.code).toBe(Parse.Error.VALIDATION_ERROR);
done();
}
);
});
});

it('Password reset username not found', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test1',
token: results[0]['_perishable_token'],
new_password: 'asdsweqwasastest',
}),
}).then(
() => {
fail('Expected to be failed');
done();
},
err => {
// TODO: Missing Parse.Error code, only string message, there should be an error code like Parse.Error.USERNAME_NOT_FOUND
expect(err.data.code).not.toBe(undefined);
done();
}
);
});
});

it('Password reset failed due to link has expired', done => {
makeRequest
.then(results => {
// wait for a bit more than the validity duration set
setTimeout(() => {
expect(sendEmailOptions).not.toBeUndefined();

req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'asdsweqwasas',
}),
})
.then(() => {
fail('Expected to be failed');
done();
})
.catch(error => {
// TODO: Missing Parse.Error code, only string message, there should be an error code like Parse.Error.RESET_LINK_EXPIRED
expect(error.data.code).not.toBe(undefined);
expect(error.data.code).toBe(Parse.Error.RESET_LINK_EXPIRED);
});
done();
}, 1000);
})
.catch(err => {
jfail(err);
done();
});
});

it('Password successfully reset', done => {
makeRequest.then(results => {
req({
url: `${serverURL}/passwordReset`,
method: 'POST',
headers,
body: JSON.stringify({
_method: 'POST',
username: 'test',
token: results[0]['_perishable_token'],
new_password: 'asdsweqwasas',
}),
}).then(
res => {
expect(res.status).toBe(200);
done();
},
() => {
fail('Expected to not fail');
done();
}
);
});
});
});
});
15 changes: 9 additions & 6 deletions src/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export class UserController extends AdaptableController {
)
.then(results => {
if (results.length != 1) {
throw 'Failed to reset password: username / email / token is invalid';
throw new Parse.Error(
Parse.Error.RESET_PASSWORD_ERROR,
'Failed to reset password: username / email / token is invalid'
);
}

if (
Expand All @@ -102,7 +105,10 @@ export class UserController extends AdaptableController {
expiresDate = new Date(expiresDate.iso);
}
if (expiresDate < new Date())
throw 'The password reset link has expired';
throw new Parse.Error(
Parse.Error.RESET_LINK_EXPIRED,
'The password reset link has expired'
);
}

return results[0];
Expand Down Expand Up @@ -246,10 +252,7 @@ export class UserController extends AdaptableController {
return this.checkResetTokenValidity(username, token)
.then(user => updateUserPassword(user.objectId, password, this.config))
.catch(error => {
if (error && error.message) {
// in case of Parse.Error, fail with the error message only
return Promise.reject(error.message);
} else {
if (error) {
return Promise.reject(error);
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/Routers/PublicAPIRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export class PublicAPIRouter extends PromiseRouter {
username: username,
token: token,
id: config.applicationId,
error: result.err,
error: result.err.message,
app: config.appName,
});

Expand Down
37 changes: 37 additions & 0 deletions src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,40 @@ export class UsersRouter extends ClassesRouter {
}
}

handlePasswordReset(req) {
this._throwOnBadEmailConfig(req);

const { username, token, new_password } = req.body;

if (!username) {
throw new Parse.Error(
Parse.Error.USERNAME_MISSING,
'you must provide an username'
);
}
return req.config.database
.find('_User', {
username: username,
})
.then(results => {
if (!results.length || results.length < 1) {
throw new Parse.Error(
Parse.Error.USERNAME_NOT_FOUND,
`No user found with ${username}`
);
}

const userController = req.config.userController;
return userController
.updatePassword(username, token, new_password)
.then(() => {
return {
response: {},
};
});
});
}

handleResetRequest(req) {
this._throwOnBadEmailConfig(req);

Expand Down Expand Up @@ -473,6 +507,9 @@ export class UsersRouter extends ClassesRouter {
this.route('POST', '/requestPasswordReset', req => {
return this.handleResetRequest(req);
});
this.route('POST', '/passwordReset', req => {
return this.handlePasswordReset(req);
});
this.route('POST', '/verificationEmailRequest', req => {
return this.handleVerificationEmailRequest(req);
});
Expand Down