Cover register and activation with e2e tests

This commit is contained in:
SleepWalker 2019-12-27 22:00:13 +02:00
parent f6f0aedc65
commit c8b0168c69
5 changed files with 216 additions and 47 deletions

View File

@ -288,11 +288,9 @@ export default class AuthFlow implements AuthContext {
* Tries to restore last oauth request, if it was stored in localStorage * Tries to restore last oauth request, if it was stored in localStorage
* in last 2 hours * in last 2 hours
* *
* @api private
*
* @returns {bool} - whether oauth state is being restored * @returns {bool} - whether oauth state is being restored
*/ */
restoreOAuthState() { private restoreOAuthState() {
if (/^\/(register|oauth2)/.test(this.getRequest().path)) { if (/^\/(register|oauth2)/.test(this.getRequest().path)) {
// allow register or the new oauth requests // allow register or the new oauth requests
return; return;

View File

@ -7,9 +7,10 @@ let sitekey;
export type CaptchaID = string; export type CaptchaID = string;
export default { class Captcha {
/** /**
* @param {DOMNode|string} el - dom node or id of element where to render captcha * @param {DOMNode|string} el - dom node or id of element where to render captcha
* @param {object} options
* @param {string} options.skin - skin color (dark|light) * @param {string} options.skin - skin color (dark|light)
* @param {Function} options.onSetCode - the callback, that will be called with * @param {Function} options.onSetCode - the callback, that will be called with
* captcha verification code, after user successfully solves captcha * captcha verification code, after user successfully solves captcha
@ -26,6 +27,9 @@ export default {
onSetCode: (code: string) => void; onSetCode: (code: string) => void;
}, },
): Promise<CaptchaID> { ): Promise<CaptchaID> {
// for testing purposes only
(window as any).e2eCaptchaSetCode = callback;
return this.loadApi().then(() => return this.loadApi().then(() =>
(window as any).grecaptcha.render(el, { (window as any).grecaptcha.render(el, {
sitekey, sitekey,
@ -33,14 +37,16 @@ export default {
callback, callback,
}), }),
); );
}, }
/** /**
* @param {string} captchaId - captcha id, returned from render promise * @param {string} captchaId - captcha id, returned from render promise
*/ */
reset(captchaId: CaptchaID) { reset(captchaId: CaptchaID) {
delete (window as any).e2eCaptchaSetCode;
this.loadApi().then(() => (window as any).grecaptcha.reset(captchaId)); this.loadApi().then(() => (window as any).grecaptcha.reset(captchaId));
}, }
/** /**
* @param {stirng} newLang * @param {stirng} newLang
@ -49,7 +55,7 @@ export default {
*/ */
setLang(newLang: string) { setLang(newLang: string) {
lang = newLang; lang = newLang;
}, }
/** /**
* @param {string} apiKey * @param {string} apiKey
@ -58,14 +64,12 @@ export default {
*/ */
setApiKey(apiKey: string) { setApiKey(apiKey: string) {
sitekey = apiKey; sitekey = apiKey;
}, }
/** /**
* @api private
*
* @returns {Promise} * @returns {Promise}
*/ */
loadApi(): Promise<void> { private loadApi(): Promise<void> {
if (!readyPromise) { if (!readyPromise) {
readyPromise = Promise.all([ readyPromise = Promise.all([
new Promise(resolve => { new Promise(resolve => {
@ -80,5 +84,7 @@ export default {
} }
return readyPromise; return readyPromise;
}, }
}; }
export default new Captcha();

View File

@ -0,0 +1,145 @@
it('should register', () => {
const username = `test${Date.now()}`;
const email = `${Date.now()}@gmail.com`;
const password = String(Date.now());
const captchaCode = 'captchaCode';
const activationKey = 'activationKey';
cy.server();
cy.route({
method: 'POST',
url: '/api/signup',
response: {
success: true,
},
}).as('signup');
cy.login({
accounts: ['default'],
updateState: false,
rawApiResp: true,
}).then(({ accounts: [account] }) => {
cy.route({
method: 'POST',
url: '/api/signup/confirm',
response: account,
}).as('activate');
});
cy.visit('/');
cy.getByTestId('toolbar')
.contains('Join')
.click();
cy.location('pathname').should('eq', '/register');
cy.get('[name=username]').type(username);
cy.get('[name=email]').type(email);
cy.get('[name=password]').type(password);
cy.get('[name=rePassword]').type(password);
cy.get('[name=rulesAgreement]').should('not.be.checked');
cy.get('[name=rulesAgreement]')
.parent()
.click();
cy.get('[name=rulesAgreement]').should('be.checked');
cy.window().should('have.property', 'e2eCaptchaSetCode');
cy.window().then(win => {
// fake captcha response
// @ts-ignore
win.e2eCaptchaSetCode(captchaCode);
});
cy.get('[type=submit]').click();
cy.wait('@signup')
.its('requestBody')
.should(
'eq',
new URLSearchParams({
email,
username,
password,
rePassword: password,
rulesAgreement: '1',
lang: 'en',
captcha: captchaCode,
}).toString(),
);
cy.location('pathname').should('eq', '/activation');
cy.get('[name=key]').type(`${activationKey}{enter}`);
cy.wait('@activate')
.its('requestBody')
.should('eq', `key=${activationKey}`);
cy.location('pathname').should('eq', '/');
});
it('should allow activation', () => {
const activationKey = 'activationKey';
cy.server();
cy.login({
accounts: ['default'],
updateState: false,
rawApiResp: true,
}).then(({ accounts: [account] }) => {
cy.route({
method: 'POST',
url: '/api/signup/confirm',
response: account,
}).as('activate');
});
cy.visit('/register');
cy.getByTestId('auth-secondary-controls')
.contains('Already have')
.click();
cy.location('pathname').should('eq', '/activation');
cy.get('[name=key]').type(`${activationKey}{enter}`);
cy.wait('@activate')
.its('requestBody')
.should('eq', `key=${activationKey}`);
cy.location('pathname').should('eq', '/');
});
it('should allow resend code', () => {
const email = `${Date.now()}@gmail.com`;
const captchaCode = 'captchaCode';
cy.server();
cy.route({
method: 'POST',
url: '/api/signup/repeat-message',
response: { success: true },
}).as('resend');
cy.visit('/register');
cy.getByTestId('auth-secondary-controls')
.contains('not received')
.click();
cy.location('pathname').should('eq', '/resend-activation');
cy.get('[name=email]').type(email);
cy.window().should('have.property', 'e2eCaptchaSetCode');
cy.window().then(win => {
// fake captcha response
// @ts-ignore
win.e2eCaptchaSetCode(captchaCode);
});
cy.get('[type=submit]').click();
cy.wait('@resend')
.its('requestBody')
.should(
'eq',
new URLSearchParams({ email, captcha: captchaCode }).toString(),
);
cy.location('pathname').should('eq', '/activation');
});

View File

@ -31,47 +31,56 @@ const accountsMap = {
default2: account1, default2: account1,
}; };
Cypress.Commands.add('login', async ({ accounts }) => { Cypress.Commands.add(
const accountsData = await Promise.all( 'login',
accounts.map(async account => { async ({ accounts, updateState = true, rawApiResp = false }) => {
let credentials; const accountsData = await Promise.all(
accounts.map(async account => {
let credentials;
if (account) { if (account) {
credentials = accountsMap[account]; credentials = accountsMap[account];
if (!credentials) { if (!credentials) {
throw new Error(`Unknown account name: ${account}`); throw new Error(`Unknown account name: ${account}`);
}
} }
}
const resp = await fetch('/api/authentication/login', { const resp = await fetch('/api/authentication/login', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
}, },
body: `${new URLSearchParams({ body: `${new URLSearchParams({
login: credentials.login, login: credentials.login,
password: credentials.password, password: credentials.password,
rememberMe: '1', rememberMe: '1',
})}`, })}`,
}).then(rawResp => rawResp.json()); }).then(rawResp => rawResp.json());
return { if (rawApiResp) {
id: credentials.id, return resp;
username: credentials.username, }
email: credentials.email,
token: resp.access_token,
refreshToken: resp.refresh_token,
};
}),
);
const state = createState(accountsData); return {
id: credentials.id,
username: credentials.username,
email: credentials.email,
token: resp.access_token,
refreshToken: resp.refresh_token,
};
}),
);
localStorage.setItem('redux-storage', JSON.stringify(state)); if (updateState) {
const state = createState(accountsData);
return { accounts: accountsData }; localStorage.setItem('redux-storage', JSON.stringify(state));
}); }
return { accounts: accountsData };
},
);
Cypress.Commands.add('getByTestId', (id, options) => Cypress.Commands.add('getByTestId', (id, options) =>
cy.get(`[data-testid=${id}]`, options), cy.get(`[data-testid=${id}]`, options),

View File

@ -15,10 +15,21 @@ declare namespace Cypress {
/** /**
* Custom command to log in the user * Custom command to log in the user
* *
* @example cy.login(account) * @example cy.login({ accounts: ['default'] })
*/ */
login(options: { login(options: {
accounts: AccountAlias[]; accounts: AccountAlias[];
/**
* defaults to `true`. if `false` than only api response will
* be returned without mutating app state
* (useful for custom scenarios such as mocking of other api responses
* or checking whether account is registered)
*/
updateState?: boolean;
/**
* Whether return raw api response without any conversion. Defaults to: `false`
*/
rawApiResp?: boolean;
}): Promise<{ accounts: Account[] }>; }): Promise<{ accounts: Account[] }>;
getByTestId<S = any>( getByTestId<S = any>(