fix: Remove username from email verification and password reset process (#8488)
BREAKING CHANGE: This removes the username from the email verification and password reset process to prevent storing personally identifiable information (PII) in server and infrastructure logs. Customized HTML pages or emails related to email verification and password reset may need to be adapted accordingly. See the new templates that come bundled with Parse Server and the [migration guide](https://github.com/parse-community/parse-server/blob/alpha/8.0.0.md) for more details.
This commit is contained in:
@@ -419,7 +419,7 @@ describe('lockout with password reset option', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
body: `new_password=${newPassword}&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -454,7 +454,7 @@ describe('lockout with password reset option', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: `${config.publicServerURL}/apps/test/request_password_reset`,
|
||||
body: `new_password=${newPassword}&token=${token}&username=${username}`,
|
||||
body: `new_password=${newPassword}&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
|
||||
@@ -39,8 +39,10 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const url = new URL(sendEmailOptions.link);
|
||||
const token = url.searchParams.get('token');
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test'
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -135,7 +137,7 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html'
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -292,6 +294,64 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can resend email using an expired token', async () => {
|
||||
const user = new Parse.User();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
user.setUsername('test');
|
||||
user.setPassword('password');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
|
||||
await Parse.Server.database.update(
|
||||
'_User',
|
||||
{ objectId: user.id },
|
||||
{
|
||||
_email_verify_token_expires_at: Parse._encode(new Date('2000')),
|
||||
}
|
||||
);
|
||||
|
||||
const obj = await Parse.Server.database.find(
|
||||
'_User',
|
||||
{ objectId: user.id },
|
||||
{},
|
||||
Auth.maintenance(Parse.Server)
|
||||
);
|
||||
const token = obj[0]._email_verify_token;
|
||||
|
||||
const res = await request({
|
||||
url: `http://localhost:8378/1/apps/test/verify_email?token=${token}`,
|
||||
method: 'GET',
|
||||
});
|
||||
expect(res.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
|
||||
);
|
||||
|
||||
const formUrl = `http://localhost:8378/1/apps/test/resend_verification_email`;
|
||||
const formResponse = await request({
|
||||
url: formUrl,
|
||||
method: 'POST',
|
||||
body: {
|
||||
token: token,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(formResponse.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/link_send_success.html`
|
||||
);
|
||||
});
|
||||
|
||||
it_id('9365c53c-b8b4-41f7-a3c1-77882f76a89c')(it)('can conditionally send emails', async () => {
|
||||
let sendEmailOptions;
|
||||
const emailAdapter = {
|
||||
@@ -614,8 +674,10 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const url = new URL(sendEmailOptions.link);
|
||||
const token = url.searchParams.get('token');
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity'
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -667,8 +729,10 @@ describe('Email Verification Token Expiration: ', () => {
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const url = new URL(sendEmailOptions.link);
|
||||
const token = url.searchParams.get('token');
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test'
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=${token}`
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('Pages Router', () => {
|
||||
const res = await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643&username=username`,
|
||||
body: `new_password=user1&token=43634643`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -124,7 +124,7 @@ describe('Pages Router', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=&token=132414&username=Johnny`,
|
||||
body: `new_password=&token=132414`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -137,30 +137,12 @@ describe('Pages Router', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('request_password_reset: responds with AJAX error on missing username', async () => {
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643&username=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.status).not.toBe(302);
|
||||
expect(error.text).toEqual('{"code":200,"error":"Missing username"}');
|
||||
}
|
||||
});
|
||||
|
||||
it('request_password_reset: responds with AJAX error on missing token', async () => {
|
||||
try {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=&username=Johnny`,
|
||||
body: `new_password=user1&token=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -577,7 +559,7 @@ describe('Pages Router', () => {
|
||||
spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile);
|
||||
|
||||
const response = await request({
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=${exampleLocale}`,
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=${exampleLocale}`,
|
||||
followRedirects: false,
|
||||
}).catch(e => e);
|
||||
expect(response.status).toEqual(200);
|
||||
@@ -626,7 +608,7 @@ describe('Pages Router', () => {
|
||||
await reconfigureServer(config);
|
||||
const response = await request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT',
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
||||
followRedirects: false,
|
||||
method: 'POST',
|
||||
});
|
||||
@@ -640,7 +622,7 @@ describe('Pages Router', () => {
|
||||
await reconfigureServer(config);
|
||||
const response = await request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&username=exampleUsername&locale=de-AT',
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
||||
followRedirects: false,
|
||||
method: 'GET',
|
||||
});
|
||||
@@ -676,13 +658,11 @@ describe('Pages Router', () => {
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const token = linkResponse.headers['x-parse-page-param-token'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
expect(locale).toBeDefined();
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(passwordResetPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.passwordReset.defaultFile}`)
|
||||
@@ -696,7 +676,6 @@ describe('Pages Router', () => {
|
||||
body: {
|
||||
token,
|
||||
locale,
|
||||
username,
|
||||
new_password: 'newPassword',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -793,15 +772,13 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`)
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
);
|
||||
|
||||
const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`;
|
||||
@@ -810,7 +787,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username,
|
||||
username: 'exampleUsername',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
@@ -847,17 +824,15 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
await jasmine.timeout();
|
||||
|
||||
const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(locale).toBe(exampleLocale);
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(invalidVerificationPagePath).toMatch(
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkExpired.defaultFile}`)
|
||||
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
||||
);
|
||||
|
||||
spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() =>
|
||||
@@ -870,7 +845,7 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
locale,
|
||||
username,
|
||||
username: 'exampleUsername',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
followRedirects: false,
|
||||
@@ -1155,12 +1130,10 @@ describe('Pages Router', () => {
|
||||
|
||||
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
||||
const token = linkResponse.headers['x-parse-page-param-token'];
|
||||
const username = linkResponse.headers['x-parse-page-param-username'];
|
||||
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
||||
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
||||
expect(appId).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
expect(username).toBeDefined();
|
||||
expect(publicServerUrl).toBeDefined();
|
||||
expect(passwordResetPagePath).toMatch(new RegExp(`\/${pages.passwordReset.defaultFile}`));
|
||||
pageResponse.calls.reset();
|
||||
@@ -1171,7 +1144,6 @@ describe('Pages Router', () => {
|
||||
method: 'POST',
|
||||
body: {
|
||||
token,
|
||||
username,
|
||||
new_password: 'newPassword',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
|
||||
@@ -969,7 +969,7 @@ describe('ParseLiveQuery', function () {
|
||||
const userController = new UserController(emailAdapter, 'test', {
|
||||
verifyUserEmails: true,
|
||||
});
|
||||
userController.verifyEmail(foundUser.username, foundUser._email_verify_token);
|
||||
userController.verifyEmail(foundUser._email_verify_token);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/;
|
||||
expect(response.text.match(re)).not.toBe(null);
|
||||
done();
|
||||
})
|
||||
@@ -622,7 +622,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -634,7 +634,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=has2init&token=${token}&username=user1`,
|
||||
body: `new_password=has2init&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -645,7 +645,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'has2init')
|
||||
@@ -714,7 +714,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -726,7 +726,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=hasnodigit&token=${token}&username=user1`,
|
||||
body: `new_password=hasnodigit&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -737,7 +737,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy`
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20should%20contain%20at%20least%20one%20digit.&app=passwordPolicy`
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'has 1 digit')
|
||||
@@ -900,7 +900,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -912,7 +912,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=xuser12&token=${token}&username=user1`,
|
||||
body: `new_password=xuser12&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -923,7 +923,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy`
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=Password%20cannot%20contain%20your%20username.&app=passwordPolicy`
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'r@nd0m')
|
||||
@@ -991,7 +991,7 @@ describe('Password Policy: ', () => {
|
||||
resolveWithFullResponse: true,
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1003,7 +1003,7 @@ describe('Password Policy: ', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=xuser12&token=${token}&username=user1`,
|
||||
body: `new_password=xuser12&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -1051,7 +1051,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1063,7 +1063,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=uuser11&token=${token}&username=user1`,
|
||||
body: `new_password=uuser11&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -1074,7 +1074,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'uuser11')
|
||||
@@ -1317,7 +1317,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1329,7 +1329,7 @@ describe('Password Policy: ', () => {
|
||||
request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=uuser11&token=${token}&username=user1`,
|
||||
body: `new_password=uuser11&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -1340,7 +1340,7 @@ describe('Password Policy: ', () => {
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
||||
);
|
||||
|
||||
Parse.User.logIn('user1', 'uuser11')
|
||||
@@ -1472,7 +1472,7 @@ describe('Password Policy: ', () => {
|
||||
})
|
||||
.then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1484,7 +1484,7 @@ describe('Password Policy: ', () => {
|
||||
return request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=${token}&username=user1`,
|
||||
body: `new_password=user1&token=${token}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -1500,7 +1500,7 @@ describe('Password Policy: ', () => {
|
||||
const token = data[1];
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy`
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy`
|
||||
);
|
||||
done();
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -10,28 +10,6 @@ const request = function (url, callback) {
|
||||
};
|
||||
|
||||
describe('public API', () => {
|
||||
it('should return missing username error on ajax request without username provided', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
});
|
||||
|
||||
try {
|
||||
await req({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=43634643&username=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
followRedirects: false,
|
||||
});
|
||||
} catch (error) {
|
||||
expect(error.status).not.toBe(302);
|
||||
expect(error.text).toEqual('{"code":200,"error":"Missing username"}');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return missing token error on ajax request without token provided', async () => {
|
||||
await reconfigureServer({
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
@@ -41,7 +19,7 @@ describe('public API', () => {
|
||||
await req({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=&username=Johnny`,
|
||||
body: `new_password=user1&token=`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -63,7 +41,7 @@ describe('public API', () => {
|
||||
await req({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=&token=132414&username=Johnny`,
|
||||
body: `new_password=&token=132414`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('Regex Vulnerabilities', () => {
|
||||
it('should not work with regex', async () => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
await request({
|
||||
url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token[$regex]=`,
|
||||
url: `${serverURL}/apps/test/verify_email?token[$regex]=`,
|
||||
method: 'GET',
|
||||
});
|
||||
await user.fetch({ useMasterKey: true });
|
||||
@@ -117,7 +117,7 @@ describe('Regex Vulnerabilities', () => {
|
||||
}).then(res => res.data);
|
||||
// It should work
|
||||
await request({
|
||||
url: `${serverURL}/apps/test/verify_email?username=someemail@somedomain.com&token=${current._email_verify_token}`,
|
||||
url: `${serverURL}/apps/test/verify_email?token=${current._email_verify_token}`,
|
||||
method: 'GET',
|
||||
});
|
||||
await user.fetch({ useMasterKey: true });
|
||||
@@ -144,7 +144,7 @@ describe('Regex Vulnerabilities', () => {
|
||||
});
|
||||
await user.fetch({ useMasterKey: true });
|
||||
const passwordResetResponse = await request({
|
||||
url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token[$regex]=`,
|
||||
url: `${serverURL}/apps/test/request_password_reset?token[$regex]=`,
|
||||
method: 'GET',
|
||||
});
|
||||
expect(passwordResetResponse.status).toEqual(302);
|
||||
@@ -192,7 +192,7 @@ describe('Regex Vulnerabilities', () => {
|
||||
}).then(res => res.data);
|
||||
const token = current._perishable_token;
|
||||
const passwordResetResponse = await request({
|
||||
url: `${serverURL}/apps/test/request_password_reset?username=someemail@somedomain.com&token=${token}`,
|
||||
url: `${serverURL}/apps/test/request_password_reset?token=${token}`,
|
||||
method: 'GET',
|
||||
});
|
||||
expect(passwordResetResponse.status).toEqual(302);
|
||||
|
||||
@@ -19,7 +19,6 @@ describe('UserController', () => {
|
||||
let emailOptions;
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
emailOptions = options;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const username = 'verificationUser';
|
||||
@@ -35,7 +34,8 @@ describe('UserController', () => {
|
||||
const rawToken = rawUser[0]._email_verify_token;
|
||||
expect(rawToken).toBeDefined();
|
||||
expect(rawUsername).toBe(username);
|
||||
expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}&username=${username}`);
|
||||
|
||||
expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,6 @@ describe('UserController', () => {
|
||||
let emailOptions;
|
||||
emailAdapter.sendVerificationEmail = options => {
|
||||
emailOptions = options;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const username = 'verificationUser';
|
||||
@@ -70,7 +69,8 @@ describe('UserController', () => {
|
||||
const rawToken = rawUser[0]._email_verify_token;
|
||||
expect(rawToken).toBeDefined();
|
||||
expect(rawUsername).toBe(username);
|
||||
expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}&username=${username}`);
|
||||
|
||||
expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const MockEmailAdapterWithOptions = require('./support/MockEmailAdapterWithOptions');
|
||||
const request = require('../lib/request');
|
||||
const Config = require('../lib/Config');
|
||||
const Auth = require('../lib/Auth');
|
||||
|
||||
describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
it('should set the custom pages', done => {
|
||||
@@ -334,7 +335,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html'
|
||||
);
|
||||
user = await new Parse.Query(Parse.User).first({ useMasterKey: true });
|
||||
expect(user.get('emailVerified')).toEqual(true);
|
||||
@@ -675,7 +676,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html'
|
||||
);
|
||||
user
|
||||
.fetch()
|
||||
@@ -734,12 +735,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
}).then(() => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga',
|
||||
url: 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf',
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=sadfasga&appId=test'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=asdfasdf'
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -779,12 +780,12 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {
|
||||
request({
|
||||
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv',
|
||||
url: 'http://localhost:8378/1/apps/test/verify_email?token=invalid',
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=zxcv&appId=test'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?appId=test&token=invalid'
|
||||
);
|
||||
user.fetch().then(() => {
|
||||
expect(user.get('emailVerified')).toEqual(false);
|
||||
@@ -824,7 +825,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&/;
|
||||
expect(response.text.match(re)).not.toBe(null);
|
||||
done();
|
||||
});
|
||||
@@ -864,8 +865,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
}).then(() => {
|
||||
request({
|
||||
url:
|
||||
'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf',
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
@@ -887,7 +887,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -907,7 +907,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
||||
);
|
||||
|
||||
Parse.User.logIn('zxcv', 'hello').then(
|
||||
@@ -964,7 +964,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
followRedirects: false,
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv%2B1/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -984,7 +984,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
}).then(response => {
|
||||
expect(response.status).toEqual(302);
|
||||
expect(response.text).toEqual(
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv%2B1'
|
||||
'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html'
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -1023,7 +1023,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
followRedirects: false,
|
||||
});
|
||||
expect(response.status).toEqual(302);
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/;
|
||||
const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&/;
|
||||
const match = response.text.match(re);
|
||||
if (!match) {
|
||||
fail('should have a token');
|
||||
@@ -1081,7 +1081,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
await request({
|
||||
method: 'POST',
|
||||
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
||||
body: `new_password=user1&token=12345&username=Johnny`,
|
||||
body: `new_password=user1&token=12345`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
@@ -1150,6 +1150,80 @@ describe('Custom Pages, Email Verification, Password Reset', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can resend email using an expired reset password token', async () => {
|
||||
const user = new Parse.User();
|
||||
const emailAdapter = {
|
||||
sendVerificationEmail: () => {},
|
||||
sendPasswordResetEmail: () => Promise.resolve(),
|
||||
sendMail: () => {},
|
||||
};
|
||||
await reconfigureServer({
|
||||
appName: 'emailVerifyToken',
|
||||
verifyUserEmails: true,
|
||||
emailAdapter: emailAdapter,
|
||||
emailVerifyTokenValidityDuration: 5, // 5 seconds
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
passwordPolicy: {
|
||||
resetTokenValidityDuration: 5 * 60, // 5 minutes
|
||||
},
|
||||
silent: false,
|
||||
});
|
||||
user.setUsername('test');
|
||||
user.setPassword('password');
|
||||
user.set('email', 'user@example.com');
|
||||
await user.signUp();
|
||||
await Parse.User.requestPasswordReset('user@example.com');
|
||||
|
||||
await Parse.Server.database.update(
|
||||
'_User',
|
||||
{ objectId: user.id },
|
||||
{
|
||||
_perishable_token_expires_at: Parse._encode(new Date('2000')),
|
||||
}
|
||||
);
|
||||
|
||||
let obj = await Parse.Server.database.find(
|
||||
'_User',
|
||||
{ objectId: user.id },
|
||||
{},
|
||||
Auth.maintenance(Parse.Server)
|
||||
);
|
||||
const token = obj[0]._perishable_token;
|
||||
const res = await request({
|
||||
url: `http://localhost:8378/1/apps/test/request_password_reset`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
token,
|
||||
new_password: 'newpassword',
|
||||
},
|
||||
});
|
||||
expect(res.text).toEqual(
|
||||
`Found. Redirecting to http://localhost:8378/1/apps/choose_password?id=test&error=The%20password%20reset%20link%20has%20expired&app=emailVerifyToken&token=${token}`
|
||||
);
|
||||
|
||||
await request({
|
||||
url: `http://localhost:8378/1/requestPasswordReset`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
token: token,
|
||||
},
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-REST-API-Key': 'rest',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
obj = await Parse.Server.database.find(
|
||||
'_User',
|
||||
{ objectId: user.id },
|
||||
{},
|
||||
Auth.maintenance(Parse.Server)
|
||||
);
|
||||
|
||||
expect(obj._perishable_token).not.toBe(token);
|
||||
});
|
||||
|
||||
it('should throw on an invalid reset password', async () => {
|
||||
await reconfigureServer({
|
||||
appName: 'coolapp',
|
||||
|
||||
102
spec/helper.js
102
spec/helper.js
@@ -227,65 +227,55 @@ beforeAll(async () => {
|
||||
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
const afterLogOut = async () => {
|
||||
// Jasmine process uses one connection
|
||||
if (Object.keys(openConnections).length > 1) {
|
||||
console.warn(`There were ${Object.keys(openConnections).length} open connections to the server left after the test finished`);
|
||||
}
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
SchemaCache.clear();
|
||||
if (didChangeConfiguration) {
|
||||
await reconfigureServer();
|
||||
} else {
|
||||
await databaseAdapter.performInitialization({ VolatileClassesSchemas });
|
||||
}
|
||||
done();
|
||||
};
|
||||
global.afterEachFn = async () => {
|
||||
Parse.Cloud._removeAllHooks();
|
||||
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient();
|
||||
defaults.protectedFields = { _User: { '*': ['email'] } };
|
||||
databaseAdapter
|
||||
.getAllClasses()
|
||||
.then(allSchemas => {
|
||||
allSchemas.forEach(schema => {
|
||||
const className = schema.className;
|
||||
expect(className).toEqual({
|
||||
asymmetricMatch: className => {
|
||||
if (!className.startsWith('_')) {
|
||||
return true;
|
||||
} else {
|
||||
// Other system classes will break Parse.com, so make sure that we don't save anything to _SCHEMA that will
|
||||
// break it.
|
||||
return (
|
||||
[
|
||||
'_User',
|
||||
'_Installation',
|
||||
'_Role',
|
||||
'_Session',
|
||||
'_Product',
|
||||
'_Audience',
|
||||
'_Idempotency',
|
||||
].indexOf(className) >= 0
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => Parse.User.logOut())
|
||||
.then(
|
||||
() => {},
|
||||
() => {}
|
||||
) // swallow errors
|
||||
.then(() => {
|
||||
// Connection close events are not immediate on node 10+... wait a bit
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
})
|
||||
.then(afterLogOut);
|
||||
});
|
||||
|
||||
const allSchemas = await databaseAdapter.getAllClasses().catch(() => []);
|
||||
|
||||
allSchemas.forEach(schema => {
|
||||
const className = schema.className;
|
||||
expect(className).toEqual({
|
||||
asymmetricMatch: className => {
|
||||
if (!className.startsWith('_')) {
|
||||
return true;
|
||||
}
|
||||
return [
|
||||
'_User',
|
||||
'_Installation',
|
||||
'_Role',
|
||||
'_Session',
|
||||
'_Product',
|
||||
'_Audience',
|
||||
'_Idempotency',
|
||||
].includes(className);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await Parse.User.logOut().catch(() => {});
|
||||
|
||||
// Connection close events are not immediate on node 10+, so wait a bit
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
// After logout operations
|
||||
if (Object.keys(openConnections).length > 1) {
|
||||
console.warn(
|
||||
`There were ${Object.keys(openConnections).length} open connections to the server left after the test finished`
|
||||
);
|
||||
}
|
||||
|
||||
await TestUtils.destroyAllDataPermanently(true);
|
||||
SchemaCache.clear();
|
||||
|
||||
if (didChangeConfiguration) {
|
||||
await reconfigureServer();
|
||||
} else {
|
||||
await databaseAdapter.performInitialization({ VolatileClassesSchemas });
|
||||
}
|
||||
}
|
||||
afterEach(global.afterEachFn);
|
||||
|
||||
afterAll(() => {
|
||||
global.displayTestStats();
|
||||
|
||||
@@ -108,6 +108,7 @@ global.retryFlakyTests = function() {
|
||||
}
|
||||
if (isFlaky) {
|
||||
retryMap[spec.result.fullName] = (retryMap[spec.result.fullName] || 0) + 1;
|
||||
await global.afterEachFn();
|
||||
}
|
||||
}
|
||||
if (exceptionCaught) {
|
||||
|
||||
Reference in New Issue
Block a user