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.
1184 lines
45 KiB
JavaScript
1184 lines
45 KiB
JavaScript
'use strict';
|
|
|
|
const request = require('../lib/request');
|
|
const fs = require('fs').promises;
|
|
const mustache = require('mustache');
|
|
const Utils = require('../lib/Utils');
|
|
const { Page } = require('../lib/Page');
|
|
const Config = require('../lib/Config');
|
|
const Definitions = require('../lib/Options/Definitions');
|
|
const UserController = require('../lib/Controllers/UserController').UserController;
|
|
const {
|
|
PagesRouter,
|
|
pages,
|
|
pageParams,
|
|
pageParamHeaderPrefix,
|
|
} = require('../lib/Routers/PagesRouter');
|
|
|
|
describe('Pages Router', () => {
|
|
describe('basic request', () => {
|
|
let config;
|
|
|
|
beforeEach(async () => {
|
|
config = {
|
|
appId: 'test',
|
|
appName: 'exampleAppname',
|
|
publicServerURL: 'http://localhost:8378/1',
|
|
pages: { enableRouter: true },
|
|
};
|
|
await reconfigureServer(config);
|
|
});
|
|
|
|
it('responds with file content on direct page request', async () => {
|
|
const urls = [
|
|
'http://localhost:8378/1/apps/email_verification_link_invalid.html',
|
|
'http://localhost:8378/1/apps/choose_password?appId=test',
|
|
'http://localhost:8378/1/apps/email_verification_success.html',
|
|
'http://localhost:8378/1/apps/password_reset_success.html',
|
|
'http://localhost:8378/1/apps/custom_json.html',
|
|
];
|
|
for (const url of urls) {
|
|
const response = await request({ url }).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
}
|
|
});
|
|
|
|
it('can load file from custom pages path', async () => {
|
|
config.pages.pagesPath = './public';
|
|
await reconfigureServer(config);
|
|
|
|
const response = await request({
|
|
url: 'http://localhost:8378/1/apps/email_verification_link_invalid.html',
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
it('can load file from custom pages endpoint', async () => {
|
|
config.pages.pagesEndpoint = 'pages';
|
|
await reconfigureServer(config);
|
|
|
|
const response = await request({
|
|
url: `http://localhost:8378/1/pages/email_verification_link_invalid.html`,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
});
|
|
|
|
it('responds with 404 if publicServerURL is not configured', async () => {
|
|
await reconfigureServer({
|
|
appName: 'unused',
|
|
pages: { enableRouter: true },
|
|
});
|
|
const urls = [
|
|
'http://localhost:8378/1/apps/test/verify_email',
|
|
'http://localhost:8378/1/apps/choose_password?appId=test',
|
|
'http://localhost:8378/1/apps/test/request_password_reset',
|
|
];
|
|
for (const url of urls) {
|
|
const response = await request({ url }).catch(e => e);
|
|
expect(response.status).toBe(404);
|
|
}
|
|
});
|
|
|
|
it('responds with 403 access denied with invalid appId', async () => {
|
|
const reqs = [
|
|
{ url: 'http://localhost:8378/1/apps/invalid/verify_email', method: 'GET' },
|
|
{ url: 'http://localhost:8378/1/apps/choose_password?id=invalid', method: 'GET' },
|
|
{ url: 'http://localhost:8378/1/apps/invalid/request_password_reset', method: 'GET' },
|
|
{ url: 'http://localhost:8378/1/apps/invalid/request_password_reset', method: 'POST' },
|
|
{ url: 'http://localhost:8378/1/apps/invalid/resend_verification_email', method: 'POST' },
|
|
];
|
|
for (const req of reqs) {
|
|
const response = await request(req).catch(e => e);
|
|
expect(response.status).toBe(403);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('AJAX requests', () => {
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
appName: 'exampleAppname',
|
|
publicServerURL: 'http://localhost:8378/1',
|
|
pages: { enableRouter: true },
|
|
});
|
|
});
|
|
|
|
it('request_password_reset: responds with AJAX success', async () => {
|
|
spyOn(UserController.prototype, 'updatePassword').and.callFake(() => Promise.resolve());
|
|
const res = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
|
body: `new_password=user1&token=43634643`,
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(res.status).toBe(200);
|
|
expect(res.text).toEqual('"Password successfully reset"');
|
|
});
|
|
|
|
it('request_password_reset: responds with AJAX error on missing password', async () => {
|
|
try {
|
|
await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/apps/test/request_password_reset',
|
|
body: `new_password=&token=132414`,
|
|
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":201,"error":"Missing password"}');
|
|
}
|
|
});
|
|
|
|
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=`,
|
|
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":-1,"error":"Missing token"}');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('pages', () => {
|
|
let router = new PagesRouter();
|
|
let req;
|
|
let config;
|
|
let goToPage;
|
|
let pageResponse;
|
|
let redirectResponse;
|
|
let readFile;
|
|
let exampleLocale;
|
|
|
|
const fillPlaceholders = (text, fill) => text.replace(/({{2,3}.*?}{2,3})/g, fill);
|
|
async function reconfigureServerWithPagesConfig(pagesConfig) {
|
|
config.pages = pagesConfig;
|
|
await reconfigureServer(config);
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
router = new PagesRouter();
|
|
readFile = spyOn(fs, 'readFile').and.callThrough();
|
|
goToPage = spyOn(PagesRouter.prototype, 'goToPage').and.callThrough();
|
|
pageResponse = spyOn(PagesRouter.prototype, 'pageResponse').and.callThrough();
|
|
redirectResponse = spyOn(PagesRouter.prototype, 'redirectResponse').and.callThrough();
|
|
exampleLocale = 'de-AT';
|
|
config = {
|
|
appId: 'test',
|
|
appName: 'ExampleAppName',
|
|
verifyUserEmails: true,
|
|
emailAdapter: {
|
|
sendVerificationEmail: () => Promise.resolve(),
|
|
sendPasswordResetEmail: () => Promise.resolve(),
|
|
sendMail: () => {},
|
|
},
|
|
publicServerURL: 'http://localhost:8378/1',
|
|
pages: {
|
|
enableRouter: true,
|
|
enableLocalization: true,
|
|
customUrls: {},
|
|
},
|
|
};
|
|
req = {
|
|
method: 'GET',
|
|
config,
|
|
query: {
|
|
locale: exampleLocale,
|
|
},
|
|
};
|
|
});
|
|
|
|
describe('server options', () => {
|
|
it('uses default configuration when none is set', async () => {
|
|
await reconfigureServerWithPagesConfig({});
|
|
expect(Config.get(Parse.applicationId).pages.enableRouter).toBe(
|
|
Definitions.PagesOptions.enableRouter.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.enableLocalization).toBe(
|
|
Definitions.PagesOptions.enableLocalization.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.localizationJsonPath).toBe(
|
|
Definitions.PagesOptions.localizationJsonPath.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.localizationFallbackLocale).toBe(
|
|
Definitions.PagesOptions.localizationFallbackLocale.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.placeholders).toBe(
|
|
Definitions.PagesOptions.placeholders.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.forceRedirect).toBe(
|
|
Definitions.PagesOptions.forceRedirect.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.pagesPath).toBe(
|
|
Definitions.PagesOptions.pagesPath.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.pagesEndpoint).toBe(
|
|
Definitions.PagesOptions.pagesEndpoint.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.customUrls).toBe(
|
|
Definitions.PagesOptions.customUrls.default
|
|
);
|
|
expect(Config.get(Parse.applicationId).pages.customRoutes).toBe(
|
|
Definitions.PagesOptions.customRoutes.default
|
|
);
|
|
});
|
|
|
|
it('throws on invalid configuration', async () => {
|
|
const options = [
|
|
[],
|
|
'a',
|
|
0,
|
|
true,
|
|
{ enableRouter: 'a' },
|
|
{ enableRouter: 0 },
|
|
{ enableRouter: {} },
|
|
{ enableRouter: [] },
|
|
{ enableLocalization: 'a' },
|
|
{ enableLocalization: 0 },
|
|
{ enableLocalization: {} },
|
|
{ enableLocalization: [] },
|
|
{ forceRedirect: 'a' },
|
|
{ forceRedirect: 0 },
|
|
{ forceRedirect: {} },
|
|
{ forceRedirect: [] },
|
|
{ placeholders: true },
|
|
{ placeholders: 'a' },
|
|
{ placeholders: 0 },
|
|
{ placeholders: [] },
|
|
{ pagesPath: true },
|
|
{ pagesPath: 0 },
|
|
{ pagesPath: {} },
|
|
{ pagesPath: [] },
|
|
{ pagesEndpoint: true },
|
|
{ pagesEndpoint: 0 },
|
|
{ pagesEndpoint: {} },
|
|
{ pagesEndpoint: [] },
|
|
{ customUrls: true },
|
|
{ customUrls: 0 },
|
|
{ customUrls: 'a' },
|
|
{ customUrls: [] },
|
|
{ localizationJsonPath: true },
|
|
{ localizationJsonPath: 0 },
|
|
{ localizationJsonPath: {} },
|
|
{ localizationJsonPath: [] },
|
|
{ localizationFallbackLocale: true },
|
|
{ localizationFallbackLocale: 0 },
|
|
{ localizationFallbackLocale: {} },
|
|
{ localizationFallbackLocale: [] },
|
|
{ customRoutes: true },
|
|
{ customRoutes: 0 },
|
|
{ customRoutes: 'a' },
|
|
{ customRoutes: {} },
|
|
];
|
|
for (const option of options) {
|
|
await expectAsync(reconfigureServerWithPagesConfig(option)).toBeRejected();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('placeholders', () => {
|
|
it('replaces placeholder in response content', async () => {
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
|
|
expect(readFile.calls.all()[0].returnValue).toBeDefined();
|
|
const originalContent = await readFile.calls.all()[0].returnValue;
|
|
expect(originalContent).toContain('{{appName}}');
|
|
|
|
expect(pageResponse.calls.all()[0].returnValue).toBeDefined();
|
|
const replacedContent = await pageResponse.calls.all()[0].returnValue;
|
|
expect(replacedContent.text).not.toContain('{{appName}}');
|
|
expect(replacedContent.text).toContain(req.config.appName);
|
|
});
|
|
|
|
it('removes undefined placeholder in response content', async () => {
|
|
await expectAsync(router.goToPage(req, pages.passwordReset)).toBeResolved();
|
|
|
|
expect(readFile.calls.all()[0].returnValue).toBeDefined();
|
|
const originalContent = await readFile.calls.all()[0].returnValue;
|
|
expect(originalContent).toContain('{{error}}');
|
|
|
|
// There is no error placeholder value set by default, so the
|
|
// {{error}} placeholder should just be removed from content
|
|
expect(pageResponse.calls.all()[0].returnValue).toBeDefined();
|
|
const replacedContent = await pageResponse.calls.all()[0].returnValue;
|
|
expect(replacedContent.text).not.toContain('{{error}}');
|
|
});
|
|
|
|
it('fills placeholders from config object', async () => {
|
|
config.pages.enableLocalization = false;
|
|
config.pages.placeholders = {
|
|
title: 'setViaConfig',
|
|
};
|
|
await reconfigureServer(config);
|
|
const response = await request({
|
|
url: 'http://localhost:8378/1/apps/custom_json.html',
|
|
followRedirects: false,
|
|
method: 'GET',
|
|
});
|
|
expect(response.status).toEqual(200);
|
|
expect(response.text).toContain(config.pages.placeholders.title);
|
|
});
|
|
|
|
it('fills placeholders from config function', async () => {
|
|
config.pages.enableLocalization = false;
|
|
config.pages.placeholders = () => {
|
|
return { title: 'setViaConfig' };
|
|
};
|
|
await reconfigureServer(config);
|
|
const response = await request({
|
|
url: 'http://localhost:8378/1/apps/custom_json.html',
|
|
followRedirects: false,
|
|
method: 'GET',
|
|
});
|
|
expect(response.status).toEqual(200);
|
|
expect(response.text).toContain(config.pages.placeholders().title);
|
|
});
|
|
|
|
it('fills placeholders from config promise', async () => {
|
|
config.pages.enableLocalization = false;
|
|
config.pages.placeholders = async () => {
|
|
return { title: 'setViaConfig' };
|
|
};
|
|
await reconfigureServer(config);
|
|
const response = await request({
|
|
url: 'http://localhost:8378/1/apps/custom_json.html',
|
|
followRedirects: false,
|
|
method: 'GET',
|
|
});
|
|
expect(response.status).toEqual(200);
|
|
expect(response.text).toContain((await config.pages.placeholders()).title);
|
|
});
|
|
});
|
|
|
|
describe('localization', () => {
|
|
it('returns default file if localization is disabled', async () => {
|
|
delete req.config.pages.enableLocalization;
|
|
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse.calls.all()[0].args[0]).toBeDefined();
|
|
expect(pageResponse.calls.all()[0].args[0]).not.toMatch(
|
|
new RegExp(`\/de(-AT)?\/${pages.passwordResetLinkInvalid.defaultFile}`)
|
|
);
|
|
});
|
|
|
|
it('returns default file if no locale is specified', async () => {
|
|
delete req.query.locale;
|
|
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse.calls.all()[0].args[0]).toBeDefined();
|
|
expect(pageResponse.calls.all()[0].args[0]).not.toMatch(
|
|
new RegExp(`\/de(-AT)?\/${pages.passwordResetLinkInvalid.defaultFile}`)
|
|
);
|
|
});
|
|
|
|
it('returns custom page regardless of localization enabled', async () => {
|
|
req.config.pages.customUrls = {
|
|
passwordResetLinkInvalid: 'http://invalid-link.example.com',
|
|
};
|
|
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse).not.toHaveBeenCalled();
|
|
expect(redirectResponse.calls.all()[0].args[0]).toBe(
|
|
req.config.pages.customUrls.passwordResetLinkInvalid
|
|
);
|
|
});
|
|
|
|
it('returns file for locale match', async () => {
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse.calls.all()[0].args[0]).toBeDefined();
|
|
expect(pageResponse.calls.all()[0].args[0]).toMatch(
|
|
new RegExp(`\/${req.query.locale}\/${pages.passwordResetLinkInvalid.defaultFile}`)
|
|
);
|
|
});
|
|
|
|
it('returns file for language match', async () => {
|
|
// Pretend no locale matching file exists
|
|
spyOn(Utils, 'fileExists').and.callFake(async path => {
|
|
return !path.includes(
|
|
`/${req.query.locale}/${pages.passwordResetLinkInvalid.defaultFile}`
|
|
);
|
|
});
|
|
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse.calls.all()[0].args[0]).toBeDefined();
|
|
expect(pageResponse.calls.all()[0].args[0]).toMatch(
|
|
new RegExp(`\/de\/${pages.passwordResetLinkInvalid.defaultFile}`)
|
|
);
|
|
});
|
|
|
|
it('returns default file for neither locale nor language match', async () => {
|
|
req.query.locale = 'yo-LO';
|
|
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse.calls.all()[0].args[0]).toBeDefined();
|
|
expect(pageResponse.calls.all()[0].args[0]).not.toMatch(
|
|
new RegExp(`\/yo(-LO)?\/${pages.passwordResetLinkInvalid.defaultFile}`)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('localization with JSON resource', () => {
|
|
let jsonPageFile;
|
|
let jsonPageUrl;
|
|
let jsonResource;
|
|
|
|
beforeEach(async () => {
|
|
jsonPageFile = 'custom_json.html';
|
|
jsonPageUrl = new URL(`${config.publicServerURL}/apps/${jsonPageFile}`);
|
|
jsonResource = require('../public/custom_json.json');
|
|
|
|
config.pages.enableLocalization = true;
|
|
config.pages.localizationJsonPath = './public/custom_json.json';
|
|
config.pages.localizationFallbackLocale = 'en';
|
|
await reconfigureServer(config);
|
|
});
|
|
|
|
it('does not localize with JSON resource if localization is disabled', async () => {
|
|
config.pages.enableLocalization = false;
|
|
config.pages.localizationJsonPath = './public/custom_json.json';
|
|
config.pages.localizationFallbackLocale = 'en';
|
|
await reconfigureServer(config);
|
|
|
|
const response = await request({
|
|
url: jsonPageUrl.toString(),
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
expect(pageResponse.calls.all()[0].args[1]).toEqual({});
|
|
expect(pageResponse.calls.all()[0].args[2]).toEqual({});
|
|
|
|
// Ensure header contains no page params
|
|
const pageParamHeaders = Object.keys(response.headers).filter(header =>
|
|
header.startsWith(pageParamHeaderPrefix)
|
|
);
|
|
expect(pageParamHeaders.length).toBe(0);
|
|
|
|
// Ensure page response does not contain any translation
|
|
const flattenedJson = Utils.flattenObject(jsonResource);
|
|
for (const value of Object.values(flattenedJson)) {
|
|
const valueWithoutPlaceholder = fillPlaceholders(value, '');
|
|
expect(response.text).not.toContain(valueWithoutPlaceholder);
|
|
}
|
|
});
|
|
|
|
it('localizes static page with JSON resource and fallback locale', async () => {
|
|
const response = await request({
|
|
url: jsonPageUrl.toString(),
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
|
|
// Ensure page response contains translation of fallback locale
|
|
const translation = jsonResource[config.pages.localizationFallbackLocale].translation;
|
|
for (const value of Object.values(translation)) {
|
|
const valueWithoutPlaceholder = fillPlaceholders(value, '');
|
|
expect(response.text).toContain(valueWithoutPlaceholder);
|
|
}
|
|
});
|
|
|
|
it('localizes static page with JSON resource and request locale', async () => {
|
|
// Add locale to request URL
|
|
jsonPageUrl.searchParams.set('locale', exampleLocale);
|
|
|
|
const response = await request({
|
|
url: jsonPageUrl.toString(),
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
|
|
// Ensure page response contains translations of request locale
|
|
const translation = jsonResource[exampleLocale].translation;
|
|
for (const value of Object.values(translation)) {
|
|
const valueWithoutPlaceholder = fillPlaceholders(value, '');
|
|
expect(response.text).toContain(valueWithoutPlaceholder);
|
|
}
|
|
});
|
|
|
|
it('localizes static page with JSON resource and language matching request locale', async () => {
|
|
// Add locale to request URL that has no locale match but only a language
|
|
// match in the JSON resource
|
|
jsonPageUrl.searchParams.set('locale', 'de-CH');
|
|
|
|
const response = await request({
|
|
url: jsonPageUrl.toString(),
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
|
|
// Ensure page response contains translations of requst language
|
|
const translation = jsonResource['de'].translation;
|
|
for (const value of Object.values(translation)) {
|
|
const valueWithoutPlaceholder = fillPlaceholders(value, '');
|
|
expect(response.text).toContain(valueWithoutPlaceholder);
|
|
}
|
|
});
|
|
|
|
it('localizes static page with JSON resource and fills placeholders in JSON values', async () => {
|
|
// Add app ID to request URL so that the request is assigned to a Parse Server app
|
|
// and placeholders within translations strings can be replaced with default page
|
|
// parameters such as `appId`
|
|
jsonPageUrl.searchParams.set('appId', config.appId);
|
|
jsonPageUrl.searchParams.set('locale', exampleLocale);
|
|
|
|
const response = await request({
|
|
url: jsonPageUrl.toString(),
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
|
|
// Fill placeholders in transation
|
|
let translation = jsonResource[exampleLocale].translation;
|
|
translation = JSON.stringify(translation);
|
|
translation = mustache.render(translation, { appName: config.appName });
|
|
translation = JSON.parse(translation);
|
|
|
|
// Ensure page response contains translation of request locale
|
|
for (const value of Object.values(translation)) {
|
|
expect(response.text).toContain(value);
|
|
}
|
|
});
|
|
|
|
it('localizes feature page with JSON resource and fills placeholders in JSON values', async () => {
|
|
// Fake any page to load the JSON page file
|
|
spyOnProperty(Page.prototype, 'defaultFile').and.returnValue(jsonPageFile);
|
|
|
|
const response = await request({
|
|
url: `http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=${exampleLocale}`,
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toEqual(200);
|
|
|
|
// Fill placeholders in transation
|
|
let translation = jsonResource[exampleLocale].translation;
|
|
translation = JSON.stringify(translation);
|
|
translation = mustache.render(translation, { appName: config.appName });
|
|
translation = JSON.parse(translation);
|
|
|
|
// Ensure page response contains translation of request locale
|
|
for (const value of Object.values(translation)) {
|
|
expect(response.text).toContain(value);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('response type', () => {
|
|
it('returns a file for GET request', async () => {
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse).toHaveBeenCalled();
|
|
expect(redirectResponse).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns a redirect for POST request', async () => {
|
|
req.method = 'POST';
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse).not.toHaveBeenCalled();
|
|
expect(redirectResponse).toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns a redirect for custom pages for GET and POST request', async () => {
|
|
req.config.pages.customUrls = {
|
|
passwordResetLinkInvalid: 'http://invalid-link.example.com',
|
|
};
|
|
|
|
for (const method of ['GET', 'POST']) {
|
|
req.method = method;
|
|
await expectAsync(router.goToPage(req, pages.passwordResetLinkInvalid)).toBeResolved();
|
|
expect(pageResponse).not.toHaveBeenCalled();
|
|
expect(redirectResponse).toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it('responds to POST request with redirect response', async () => {
|
|
await reconfigureServer(config);
|
|
const response = await request({
|
|
url:
|
|
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
|
followRedirects: false,
|
|
method: 'POST',
|
|
});
|
|
expect(response.status).toEqual(303);
|
|
expect(response.headers.location).toContain(
|
|
'http://localhost:8378/1/apps/de-AT/password_reset_link_invalid.html'
|
|
);
|
|
});
|
|
|
|
it('responds to GET request with content response', async () => {
|
|
await reconfigureServer(config);
|
|
const response = await request({
|
|
url:
|
|
'http://localhost:8378/1/apps/test/request_password_reset?token=exampleToken&locale=de-AT',
|
|
followRedirects: false,
|
|
method: 'GET',
|
|
});
|
|
expect(response.status).toEqual(200);
|
|
expect(response.text).toContain('<html>');
|
|
});
|
|
});
|
|
|
|
describe('end-to-end tests', () => {
|
|
it('localizes end-to-end for password reset: success', async () => {
|
|
await reconfigureServer(config);
|
|
const sendPasswordResetEmail = spyOn(
|
|
config.emailAdapter,
|
|
'sendPasswordResetEmail'
|
|
).and.callThrough();
|
|
const user = new Parse.User();
|
|
user.setUsername('exampleUsername');
|
|
user.setPassword('examplePassword');
|
|
user.set('email', 'mail@example.com');
|
|
await user.signUp();
|
|
await Parse.User.requestPasswordReset(user.getEmail());
|
|
|
|
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
|
|
const linkWithLocale = new URL(link);
|
|
linkWithLocale.searchParams.append(pageParams.locale, exampleLocale);
|
|
|
|
const linkResponse = await request({
|
|
url: linkWithLocale.toString(),
|
|
followRedirects: false,
|
|
});
|
|
expect(linkResponse.status).toBe(200);
|
|
|
|
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 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(publicServerUrl).toBeDefined();
|
|
expect(passwordResetPagePath).toMatch(
|
|
new RegExp(`\/${exampleLocale}\/${pages.passwordReset.defaultFile}`)
|
|
);
|
|
pageResponse.calls.reset();
|
|
|
|
const formUrl = `${publicServerUrl}/apps/${appId}/request_password_reset`;
|
|
const formResponse = await request({
|
|
url: formUrl,
|
|
method: 'POST',
|
|
body: {
|
|
token,
|
|
locale,
|
|
new_password: 'newPassword',
|
|
},
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
followRedirects: false,
|
|
});
|
|
expect(formResponse.status).toEqual(200);
|
|
expect(pageResponse.calls.all()[0].args[0]).toContain(
|
|
`/${locale}/${pages.passwordResetSuccess.defaultFile}`
|
|
);
|
|
});
|
|
|
|
it('localizes end-to-end for password reset: invalid link', async () => {
|
|
await reconfigureServer(config);
|
|
const sendPasswordResetEmail = spyOn(
|
|
config.emailAdapter,
|
|
'sendPasswordResetEmail'
|
|
).and.callThrough();
|
|
const user = new Parse.User();
|
|
user.setUsername('exampleUsername');
|
|
user.setPassword('examplePassword');
|
|
user.set('email', 'mail@example.com');
|
|
await user.signUp();
|
|
await Parse.User.requestPasswordReset(user.getEmail());
|
|
|
|
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
|
|
const linkWithLocale = new URL(link);
|
|
linkWithLocale.searchParams.append(pageParams.locale, exampleLocale);
|
|
linkWithLocale.searchParams.set(pageParams.token, 'invalidToken');
|
|
|
|
const linkResponse = await request({
|
|
url: linkWithLocale.toString(),
|
|
followRedirects: false,
|
|
});
|
|
expect(linkResponse.status).toBe(200);
|
|
|
|
const pagePath = pageResponse.calls.all()[0].args[0];
|
|
expect(pagePath).toMatch(
|
|
new RegExp(`\/${exampleLocale}\/${pages.passwordResetLinkInvalid.defaultFile}`)
|
|
);
|
|
});
|
|
|
|
it_id('2845c2ea-23ba-45d2-a33f-63181d419bca')(it)('localizes end-to-end for verify email: success', async () => {
|
|
await reconfigureServer(config);
|
|
const sendVerificationEmail = spyOn(
|
|
config.emailAdapter,
|
|
'sendVerificationEmail'
|
|
).and.callThrough();
|
|
const user = new Parse.User();
|
|
user.setUsername('exampleUsername');
|
|
user.setPassword('examplePassword');
|
|
user.set('email', 'mail@example.com');
|
|
await user.signUp();
|
|
await jasmine.timeout();
|
|
|
|
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
|
const linkWithLocale = new URL(link);
|
|
linkWithLocale.searchParams.append(pageParams.locale, exampleLocale);
|
|
|
|
const linkResponse = await request({
|
|
url: linkWithLocale.toString(),
|
|
followRedirects: false,
|
|
});
|
|
expect(linkResponse.status).toBe(200);
|
|
|
|
const pagePath = pageResponse.calls.all()[0].args[0];
|
|
expect(pagePath).toMatch(
|
|
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationSuccess.defaultFile}`)
|
|
);
|
|
});
|
|
|
|
it_id('f2272b94-b4ac-474f-8e47-1ca74de136f5')(it)('localizes end-to-end for verify email: invalid verification link - link send success', async () => {
|
|
await reconfigureServer(config);
|
|
const sendVerificationEmail = spyOn(
|
|
config.emailAdapter,
|
|
'sendVerificationEmail'
|
|
).and.callThrough();
|
|
const user = new Parse.User();
|
|
user.setUsername('exampleUsername');
|
|
user.setPassword('examplePassword');
|
|
user.set('email', 'mail@example.com');
|
|
await user.signUp();
|
|
await jasmine.timeout();
|
|
|
|
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
|
const linkWithLocale = new URL(link);
|
|
linkWithLocale.searchParams.append(pageParams.locale, exampleLocale);
|
|
linkWithLocale.searchParams.set(pageParams.token, 'invalidToken');
|
|
|
|
const linkResponse = await request({
|
|
url: linkWithLocale.toString(),
|
|
followRedirects: false,
|
|
});
|
|
expect(linkResponse.status).toBe(200);
|
|
|
|
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
|
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
|
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(publicServerUrl).toBeDefined();
|
|
expect(invalidVerificationPagePath).toMatch(
|
|
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
|
);
|
|
|
|
const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`;
|
|
const formResponse = await request({
|
|
url: formUrl,
|
|
method: 'POST',
|
|
body: {
|
|
locale,
|
|
username: 'exampleUsername',
|
|
},
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
followRedirects: false,
|
|
});
|
|
expect(formResponse.status).toEqual(303);
|
|
expect(formResponse.text).toContain(
|
|
`/${locale}/${pages.emailVerificationSendSuccess.defaultFile}`
|
|
);
|
|
});
|
|
|
|
it_id('1d46d36a-e455-4ae7-8717-e0d286e95f02')(it)('localizes end-to-end for verify email: invalid verification link - link send fail', async () => {
|
|
await reconfigureServer(config);
|
|
const sendVerificationEmail = spyOn(
|
|
config.emailAdapter,
|
|
'sendVerificationEmail'
|
|
).and.callThrough();
|
|
const user = new Parse.User();
|
|
user.setUsername('exampleUsername');
|
|
user.setPassword('examplePassword');
|
|
user.set('email', 'mail@example.com');
|
|
await user.signUp();
|
|
await jasmine.timeout();
|
|
|
|
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
|
const linkWithLocale = new URL(link);
|
|
linkWithLocale.searchParams.append(pageParams.locale, exampleLocale);
|
|
linkWithLocale.searchParams.set(pageParams.token, 'invalidToken');
|
|
|
|
const linkResponse = await request({
|
|
url: linkWithLocale.toString(),
|
|
followRedirects: false,
|
|
});
|
|
expect(linkResponse.status).toBe(200);
|
|
|
|
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
|
const locale = linkResponse.headers['x-parse-page-param-locale'];
|
|
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(publicServerUrl).toBeDefined();
|
|
expect(invalidVerificationPagePath).toMatch(
|
|
new RegExp(`\/${exampleLocale}\/${pages.emailVerificationLinkInvalid.defaultFile}`)
|
|
);
|
|
|
|
spyOn(UserController.prototype, 'resendVerificationEmail').and.callFake(() =>
|
|
Promise.reject('failed to resend verification email')
|
|
);
|
|
|
|
const formUrl = `${publicServerUrl}/apps/${appId}/resend_verification_email`;
|
|
const formResponse = await request({
|
|
url: formUrl,
|
|
method: 'POST',
|
|
body: {
|
|
locale,
|
|
username: 'exampleUsername',
|
|
},
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
followRedirects: false,
|
|
});
|
|
expect(formResponse.status).toEqual(303);
|
|
expect(formResponse.text).toContain(
|
|
`/${locale}/${pages.emailVerificationSendFail.defaultFile}`
|
|
);
|
|
});
|
|
|
|
it('localizes end-to-end for resend verification email: invalid link', async () => {
|
|
await reconfigureServer(config);
|
|
const formUrl = `${config.publicServerURL}/apps/${config.appId}/resend_verification_email`;
|
|
const formResponse = await request({
|
|
url: formUrl,
|
|
method: 'POST',
|
|
body: {
|
|
locale: exampleLocale,
|
|
},
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
followRedirects: false,
|
|
});
|
|
expect(formResponse.status).toEqual(303);
|
|
expect(formResponse.text).toContain(
|
|
`/${exampleLocale}/${pages.emailVerificationLinkInvalid.defaultFile}`
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('failing with missing parameters', () => {
|
|
it('verifyEmail: throws on missing server configuration', async () => {
|
|
delete req.config;
|
|
const verifyEmail = req => (() => new PagesRouter().verifyEmail(req)).bind(null);
|
|
expect(verifyEmail(req)).toThrow();
|
|
});
|
|
|
|
it('resendVerificationEmail: throws on missing server configuration', async () => {
|
|
delete req.config;
|
|
const resendVerificationEmail = req =>
|
|
(() => new PagesRouter().resendVerificationEmail(req)).bind(null);
|
|
expect(resendVerificationEmail(req)).toThrow();
|
|
});
|
|
|
|
it('requestResetPassword: throws on missing server configuration', async () => {
|
|
delete req.config;
|
|
const requestResetPassword = req =>
|
|
(() => new PagesRouter().requestResetPassword(req)).bind(null);
|
|
expect(requestResetPassword(req)).toThrow();
|
|
});
|
|
|
|
it('resetPassword: throws on missing server configuration', async () => {
|
|
delete req.config;
|
|
const resetPassword = req => (() => new PagesRouter().resetPassword(req)).bind(null);
|
|
expect(resetPassword(req)).toThrow();
|
|
});
|
|
|
|
it('verifyEmail: responds with invalid link on missing username', async () => {
|
|
req.query.token = 'exampleToken';
|
|
req.params = {};
|
|
req.config.userController = { verifyEmail: () => Promise.reject() };
|
|
const verifyEmail = req => new PagesRouter().verifyEmail(req);
|
|
|
|
await verifyEmail(req);
|
|
expect(goToPage.calls.all()[0].args[1]).toBe(pages.emailVerificationLinkInvalid);
|
|
});
|
|
|
|
it('resetPassword: responds with page choose password with error message on failed password update', async () => {
|
|
req.body = {
|
|
token: 'exampleToken',
|
|
username: 'exampleUsername',
|
|
new_password: 'examplePassword',
|
|
};
|
|
const error = 'exampleError';
|
|
req.config.userController = { updatePassword: () => Promise.reject(error) };
|
|
const resetPassword = req => new PagesRouter().resetPassword(req);
|
|
|
|
await resetPassword(req);
|
|
expect(goToPage.calls.all()[0].args[1]).toBe(pages.passwordReset);
|
|
expect(goToPage.calls.all()[0].args[2].error).toBe(error);
|
|
});
|
|
|
|
it('resetPassword: responds with AJAX error with error message on failed password update', async () => {
|
|
req.xhr = true;
|
|
req.body = {
|
|
token: 'exampleToken',
|
|
username: 'exampleUsername',
|
|
new_password: 'examplePassword',
|
|
};
|
|
const error = 'exampleError';
|
|
req.config.userController = { updatePassword: () => Promise.reject(error) };
|
|
const resetPassword = req => new PagesRouter().resetPassword(req).catch(e => e);
|
|
|
|
const response = await resetPassword(req);
|
|
expect(response.code).toBe(Parse.Error.OTHER_CAUSE);
|
|
});
|
|
});
|
|
|
|
describe('exploits', () => {
|
|
it('rejects requesting file outside of pages scope with UNIX path patterns', async () => {
|
|
await reconfigureServer(config);
|
|
|
|
// Do not compose this URL with `new URL(...)` because that would normalize
|
|
// the URL and remove path patterns; the path patterns must reach the router
|
|
const url = `${config.publicServerURL}/apps/../.gitignore`;
|
|
const response = await request({
|
|
url: url,
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(404);
|
|
expect(response.text).toBe('Not found.');
|
|
});
|
|
});
|
|
|
|
describe('custom route', () => {
|
|
it('handles custom route with GET', async () => {
|
|
config.pages.customRoutes = [
|
|
{
|
|
method: 'GET',
|
|
path: 'custom_page',
|
|
handler: async req => {
|
|
expect(req).toBeDefined();
|
|
expect(req.method).toBe('GET');
|
|
return { file: 'custom_page.html' };
|
|
},
|
|
},
|
|
];
|
|
await reconfigureServer(config);
|
|
const handlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough();
|
|
|
|
const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`;
|
|
const response = await request({
|
|
url: url,
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
expect(response.text).toMatch(config.appName);
|
|
expect(handlerSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles custom route with POST', async () => {
|
|
config.pages.customRoutes = [
|
|
{
|
|
method: 'POST',
|
|
path: 'custom_page',
|
|
handler: async req => {
|
|
expect(req).toBeDefined();
|
|
expect(req.method).toBe('POST');
|
|
return { file: 'custom_page.html' };
|
|
},
|
|
},
|
|
];
|
|
const handlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough();
|
|
await reconfigureServer(config);
|
|
|
|
const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`;
|
|
const response = await request({
|
|
url: url,
|
|
followRedirects: false,
|
|
method: 'POST',
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
expect(response.text).toMatch(config.appName);
|
|
expect(handlerSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles multiple custom routes', async () => {
|
|
config.pages.customRoutes = [
|
|
{
|
|
method: 'GET',
|
|
path: 'custom_page',
|
|
handler: async req => {
|
|
expect(req).toBeDefined();
|
|
expect(req.method).toBe('GET');
|
|
return { file: 'custom_page.html' };
|
|
},
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: 'custom_page',
|
|
handler: async req => {
|
|
expect(req).toBeDefined();
|
|
expect(req.method).toBe('POST');
|
|
return { file: 'custom_page.html' };
|
|
},
|
|
},
|
|
];
|
|
const getHandlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough();
|
|
const postHandlerSpy = spyOn(config.pages.customRoutes[1], 'handler').and.callThrough();
|
|
await reconfigureServer(config);
|
|
|
|
const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`;
|
|
const getResponse = await request({
|
|
url: url,
|
|
followRedirects: false,
|
|
method: 'GET',
|
|
}).catch(e => e);
|
|
expect(getResponse.status).toBe(200);
|
|
expect(getResponse.text).toMatch(config.appName);
|
|
expect(getHandlerSpy).toHaveBeenCalled();
|
|
|
|
const postResponse = await request({
|
|
url: url,
|
|
followRedirects: false,
|
|
method: 'POST',
|
|
}).catch(e => e);
|
|
expect(postResponse.status).toBe(200);
|
|
expect(postResponse.text).toMatch(config.appName);
|
|
expect(postHandlerSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('handles custom route with async handler', async () => {
|
|
config.pages.customRoutes = [
|
|
{
|
|
method: 'GET',
|
|
path: 'custom_page',
|
|
handler: async req => {
|
|
expect(req).toBeDefined();
|
|
expect(req.method).toBe('GET');
|
|
const file = await new Promise(resolve =>
|
|
setTimeout(resolve('custom_page.html'), 1000)
|
|
);
|
|
return { file };
|
|
},
|
|
},
|
|
];
|
|
await reconfigureServer(config);
|
|
const handlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough();
|
|
|
|
const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`;
|
|
const response = await request({
|
|
url: url,
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(200);
|
|
expect(response.text).toMatch(config.appName);
|
|
expect(handlerSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns 404 if custom route does not return page', async () => {
|
|
config.pages.customRoutes = [
|
|
{
|
|
method: 'GET',
|
|
path: 'custom_page',
|
|
handler: async () => {},
|
|
},
|
|
];
|
|
await reconfigureServer(config);
|
|
const handlerSpy = spyOn(config.pages.customRoutes[0], 'handler').and.callThrough();
|
|
|
|
const url = `${config.publicServerURL}/apps/${config.appId}/custom_page`;
|
|
const response = await request({
|
|
url: url,
|
|
followRedirects: false,
|
|
}).catch(e => e);
|
|
expect(response.status).toBe(404);
|
|
expect(response.text).toMatch('Not found');
|
|
expect(handlerSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('custom endpoint', () => {
|
|
it('password reset works with custom endpoint', async () => {
|
|
config.pages.pagesEndpoint = 'customEndpoint';
|
|
await reconfigureServer(config);
|
|
const sendPasswordResetEmail = spyOn(
|
|
config.emailAdapter,
|
|
'sendPasswordResetEmail'
|
|
).and.callThrough();
|
|
const user = new Parse.User();
|
|
user.setUsername('exampleUsername');
|
|
user.setPassword('examplePassword');
|
|
user.set('email', 'mail@example.com');
|
|
await user.signUp();
|
|
await Parse.User.requestPasswordReset(user.getEmail());
|
|
|
|
const link = sendPasswordResetEmail.calls.all()[0].args[0].link;
|
|
const linkResponse = await request({
|
|
url: link,
|
|
followRedirects: false,
|
|
});
|
|
expect(linkResponse.status).toBe(200);
|
|
|
|
const appId = linkResponse.headers['x-parse-page-param-appid'];
|
|
const token = linkResponse.headers['x-parse-page-param-token'];
|
|
const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl'];
|
|
const passwordResetPagePath = pageResponse.calls.all()[0].args[0];
|
|
expect(appId).toBeDefined();
|
|
expect(token).toBeDefined();
|
|
expect(publicServerUrl).toBeDefined();
|
|
expect(passwordResetPagePath).toMatch(new RegExp(`\/${pages.passwordReset.defaultFile}`));
|
|
pageResponse.calls.reset();
|
|
|
|
const formUrl = `${publicServerUrl}/${config.pages.pagesEndpoint}/${appId}/request_password_reset`;
|
|
const formResponse = await request({
|
|
url: formUrl,
|
|
method: 'POST',
|
|
body: {
|
|
token,
|
|
new_password: 'newPassword',
|
|
},
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
followRedirects: false,
|
|
});
|
|
expect(formResponse.status).toEqual(200);
|
|
expect(pageResponse.calls.all()[0].args[0]).toContain(
|
|
`/${pages.passwordResetSuccess.defaultFile}`
|
|
);
|
|
});
|
|
|
|
it_id('81c1c28e-5dfd-4ffb-a09b-283156c08483')(it)('email verification works with custom endpoint', async () => {
|
|
config.pages.pagesEndpoint = 'customEndpoint';
|
|
await reconfigureServer(config);
|
|
const sendVerificationEmail = spyOn(
|
|
config.emailAdapter,
|
|
'sendVerificationEmail'
|
|
).and.callThrough();
|
|
const user = new Parse.User();
|
|
user.setUsername('exampleUsername');
|
|
user.setPassword('examplePassword');
|
|
user.set('email', 'mail@example.com');
|
|
await user.signUp();
|
|
await jasmine.timeout();
|
|
|
|
const link = sendVerificationEmail.calls.all()[0].args[0].link;
|
|
const linkResponse = await request({
|
|
url: link,
|
|
followRedirects: false,
|
|
});
|
|
expect(linkResponse.status).toBe(200);
|
|
const pagePath = pageResponse.calls.all()[0].args[0];
|
|
expect(pagePath).toMatch(new RegExp(`\/${pages.emailVerificationSuccess.defaultFile}`));
|
|
});
|
|
});
|
|
});
|
|
});
|