From 5ca4c323c74d6fbd3141c31650729d13ec2a8953 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Sun, 29 Dec 2019 18:26:51 +0200 Subject: [PATCH] Fix the rest of the tests --- packages/app/pages/root/RootPage.tsx | 7 +- .../authFlow/AuthFlow.functional.test.ts | 10 ++- packages/app/services/authFlow/AuthFlow.ts | 20 ++--- .../app/services/authFlow/CompleteState.ts | 2 +- packages/app/services/authFlow/LoginState.ts | 2 +- .../app/services/authFlow/PasswordState.ts | 6 +- .../auth/invalid-refreshToken.test.ts | 90 ++++++++++--------- .../cypress/integration/auth/oauth.test.ts | 54 ++++++----- .../cypress/integration/auth/sign-in.test.ts | 10 +++ tests-e2e/cypress/support/index.d.ts | 14 ++- 10 files changed, 128 insertions(+), 87 deletions(-) diff --git a/packages/app/pages/root/RootPage.tsx b/packages/app/pages/root/RootPage.tsx index f5f2abc..dd02234 100644 --- a/packages/app/pages/root/RootPage.tsx +++ b/packages/app/pages/root/RootPage.tsx @@ -71,7 +71,12 @@ class RootPage extends React.PureComponent<{ >
- +
diff --git a/packages/app/services/authFlow/AuthFlow.functional.test.ts b/packages/app/services/authFlow/AuthFlow.functional.test.ts index 12b059c..673139c 100644 --- a/packages/app/services/authFlow/AuthFlow.functional.test.ts +++ b/packages/app/services/authFlow/AuthFlow.functional.test.ts @@ -68,7 +68,7 @@ describe('AuthFlow.functional', () => { it('should redirect guest / -> /login', () => { navigate('/'); - expect(flow.navigate, 'was called once'); + expect(flow.navigate, 'was called twice'); expect(flow.navigate, 'to have a call satisfying', ['/login']); }); @@ -80,8 +80,12 @@ describe('AuthFlow.functional', () => { navigate('/login'); navigate('/'); - expect(flow.navigate, 'was called twice'); - expect(flow.navigate, 'to have a call satisfying', ['/login']); + expect(flow.navigate, 'was called thrice'); + expect(flow.navigate, 'to have calls satisfying', [ + ['/login'], + ['/login'], + ['/login'], + ]); }); }); diff --git a/packages/app/services/authFlow/AuthFlow.ts b/packages/app/services/authFlow/AuthFlow.ts index 30e3be2..f9fab91 100644 --- a/packages/app/services/authFlow/AuthFlow.ts +++ b/packages/app/services/authFlow/AuthFlow.ts @@ -115,12 +115,6 @@ export default class AuthFlow implements AuthContext { options.replace = true; } - this.currentRequest = { - path: route, - params: {}, - query: new URLSearchParams(), - }; - if (this.replace) { this.replace(route); } @@ -288,18 +282,18 @@ export default class AuthFlow implements AuthContext { * * @returns {bool} - whether oauth state is being restored */ - private restoreOAuthState() { - if (/^\/(register|oauth2)/.test(this.getRequest().path)) { - // allow register or the new oauth requests - return; - } - + private restoreOAuthState(): boolean { if (this.oAuthStateRestored) { - return; + return false; } this.oAuthStateRestored = true; + if (/^\/(register|oauth2)/.test(this.getRequest().path)) { + // allow register or the new oauth requests + return false; + } + try { const data = JSON.parse(localStorage.getItem('oauthData')); const expirationTime = 2 * 60 * 60 * 1000; // 2h diff --git a/packages/app/services/authFlow/CompleteState.ts b/packages/app/services/authFlow/CompleteState.ts index b617c4a..ed10f33 100644 --- a/packages/app/services/authFlow/CompleteState.ts +++ b/packages/app/services/authFlow/CompleteState.ts @@ -105,7 +105,7 @@ export default class CompleteState extends AbstractState { (resp: { redirectUri: string }) => { // TODO: пусть в стейт попадает флаг или тип авторизации // вместо волшебства над редирект урлой - if (resp.redirectUri.indexOf('static_page') === 0) { + if (resp.redirectUri.includes('static_page')) { context.setState(new FinishState()); } else { return context.run('redirect', resp.redirectUri); diff --git a/packages/app/services/authFlow/LoginState.ts b/packages/app/services/authFlow/LoginState.ts index c0bcdfd..03dc7dd 100644 --- a/packages/app/services/authFlow/LoginState.ts +++ b/packages/app/services/authFlow/LoginState.ts @@ -12,7 +12,7 @@ export default class LoginState extends AbstractState { const { user } = context.getState(); const isUserAddsSecondAccount = - !user.isGuest && /login|password/.test(context.getRequest().path); // TODO: improve me + !user.isGuest && /login|password/.test(location.pathname); // TODO: improve me // TODO: it may not allow user to leave password state till he click back or enters password if (login) { diff --git a/packages/app/services/authFlow/PasswordState.ts b/packages/app/services/authFlow/PasswordState.ts index 6590706..97fe88a 100644 --- a/packages/app/services/authFlow/PasswordState.ts +++ b/packages/app/services/authFlow/PasswordState.ts @@ -15,9 +15,11 @@ import { AuthContext } from './AuthFlow'; export default class PasswordState extends AbstractState { enter(context: AuthContext) { - const { login } = getCredentials(context.getState()); + const { login, isTotpRequired } = getCredentials(context.getState()); - if (login) { + if (isTotpRequired) { + context.setState(new MfaState()); + } else if (login) { context.navigate('/password'); } else { context.setState(new CompleteState()); diff --git a/tests-e2e/cypress/integration/auth/invalid-refreshToken.test.ts b/tests-e2e/cypress/integration/auth/invalid-refreshToken.test.ts index 4f5fa9a..3a70518 100644 --- a/tests-e2e/cypress/integration/auth/invalid-refreshToken.test.ts +++ b/tests-e2e/cypress/integration/auth/invalid-refreshToken.test.ts @@ -9,31 +9,25 @@ singleAccount.accounts.available = singleAccount.accounts.available.filter( account => account.id === singleAccount.accounts.active, ); -describe("when user's token and refreshToken are invalid", () => { - before(() => +describe('User with invalid token and refreshToken', () => { + before(() => { // ensure we always have one account with correct token - cy.visit('/').then(() => - fetch('/api/authentication/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', - }, - body: `login=${account1.login}&password=${account1.password}`, - }) - .then(resp => resp.json()) - .then(resp => { - const account = multiAccount.accounts.available.find( - item => item.username === account1.username, - ); + cy.login({ + accounts: ['default'], + updateState: false, + rawApiResp: true, + }).then(({ accounts: [resp] }) => { + const account = multiAccount.accounts.available.find( + item => item.username === account1.username, + ); - if (!account) { - throw new Error('Can not find an account'); - } + if (!account) { + throw new Error('Can not find an account'); + } - account.token = resp.access_token; - }), - ), - ); + account.token = resp.access_token; + }); + }); beforeEach(() => localStorage.setItem('redux-storage', JSON.stringify(multiAccount)), @@ -46,7 +40,7 @@ describe("when user's token and refreshToken are invalid", () => { cy.get('[name="password"]').type(`${account2.password}{enter}`); - cy.location('pathname', { timeout: 15000 }).should('eq', '/'); + cy.location('pathname').should('eq', '/'); cy.contains('account preferences'); }); @@ -60,18 +54,15 @@ describe("when user's token and refreshToken are invalid", () => { .contains('Ely.by') .click(); - // TODO: currently we can not skip redirect to /, but we will in future - cy.location('pathname', { timeout: 15000 }).should('eq', '/'); - cy.url({ timeout: 15000 }).should('include', '/password'); + cy.url().should('include', '/password'); }); it('should allow select account', () => { - // TODO: need a way to get valid token for one of the accounts cy.visit('/'); cy.get('[data-e2e-go-back]').click(); - cy.url().should('include', '/choose-account'); + cy.location('pathname').should('eq', '/choose-account'); cy.get('[data-e2e-content]') .contains(account2.email) @@ -80,8 +71,9 @@ describe("when user's token and refreshToken are invalid", () => { cy.get('[data-e2e-content]') .contains(account1.username) .click(); + cy.get('[name="password"]').type(`${account2.password}{enter}`); - cy.location('pathname', { timeout: 15000 }).should('eq', '/'); + cy.location('pathname').should('eq', '/'); cy.contains('account preferences'); }); @@ -101,12 +93,20 @@ describe("when user's token and refreshToken are invalid", () => { }); it('should allow logout', () => { + cy.server(); + cy.route({ + url: `/api/v1/accounts/${account2.id}`, + }).as('account'); + cy.route({ + method: 'POST', + url: '/api/authentication/logout', + }).as('logout'); + cy.visit('/'); - cy.get('@fetch', { timeout: 15000 }).should( - 'be.calledWith', - `/api/v1/accounts/${account2.id}`, - ); + cy.wait('@account') + .its('status') + .should('eq', 401); cy.getByTestId('toolbar') .contains(account2.username) @@ -115,10 +115,7 @@ describe("when user's token and refreshToken are invalid", () => { .contains('Log out') .click(); - cy.get('@fetch', { timeout: 15000 }).should( - 'be.calledWith', - '/api/authentication/logout', - ); + cy.wait('@logout'); cy.getByTestId('toolbar') .contains(account2.email) .should('not.exist'); @@ -128,12 +125,16 @@ describe("when user's token and refreshToken are invalid", () => { }); it('should allow enter new login from choose account', () => { + cy.server(); + cy.route({ + url: `/api/v1/accounts/${account2.id}`, + }).as('account'); + cy.visit('/'); - cy.get('@fetch', { timeout: 15000 }).should( - 'be.calledWith', - `/api/v1/accounts/${account2.id}`, - ); + cy.wait('@account') + .its('status') + .should('eq', 401); cy.url().should('include', '/password'); @@ -152,9 +153,10 @@ describe("when user's token and refreshToken are invalid", () => { cy.get('[name=password]').type(account1.password); cy.get('[name=rememberMe]').should('be.checked'); + cy.get('[type=submit]').should('have.length', 1); // wait till transition ends cy.get('[type=submit]').click(); - cy.location('pathname', { timeout: 15000 }).should('eq', '/'); + cy.location('pathname').should('eq', '/'); }); it('should allow logout from all accounts while choosing an account', () => { @@ -195,7 +197,7 @@ describe("when user's token and refreshToken are invalid", () => { cy.wait(1000); cy.get('[name="password"]').type(`${account1.password}{enter}`); - cy.location('pathname', { timeout: 15000 }).should('eq', '/'); + cy.location('pathname').should('eq', '/'); cy.contains('account preferences'); }); @@ -254,7 +256,7 @@ describe("when user's token and refreshToken are invalid", () => { cy.get('[name=password]').type(`${account2.password}{enter}`); - cy.url({ timeout: 15000 }).should('contain', '/oauth/choose-account'); + cy.url().should('contain', '/oauth/choose-account'); cy.get('[data-e2e-content]') .contains(account2.username) diff --git a/tests-e2e/cypress/integration/auth/oauth.test.ts b/tests-e2e/cypress/integration/auth/oauth.test.ts index 4704b4f..c191ad8 100644 --- a/tests-e2e/cypress/integration/auth/oauth.test.ts +++ b/tests-e2e/cypress/integration/auth/oauth.test.ts @@ -161,8 +161,8 @@ describe('OAuth', () => { ...defaults, // suggest preferred username // https://docs.ely.by/ru/oauth.html#id3 - login_hint: account.id, - })}`, + login_hint: String(account.id), + }).toString()}`, ); cy.url().should('equal', 'https://ely.by/'); @@ -368,6 +368,12 @@ describe('OAuth', () => { describe('static pages', () => { it('should authenticate using static page', () => { + cy.server(); + cy.route({ + method: 'POST', + url: '/api/oauth2/v1/complete**', + }).as('complete'); + cy.login({ accounts: ['default'] }); cy.visit( @@ -378,10 +384,18 @@ describe('OAuth', () => { })}`, ); + cy.wait('@complete'); + cy.url().should('include', 'oauth/finish#{%22auth_code%22:'); }); it('should authenticate using static page with code', () => { + cy.server(); + cy.route({ + method: 'POST', + url: '/api/oauth2/v1/complete**', + }).as('complete'); + cy.login({ accounts: ['default'] }); cy.visit( @@ -392,6 +406,8 @@ describe('OAuth', () => { })}`, ); + cy.wait('@complete'); + cy.url().should('include', 'oauth/finish#{%22auth_code%22:'); cy.getByTestId('oauth-code-container').should( @@ -404,25 +420,21 @@ describe('OAuth', () => { // https://github.com/cypress-io/cypress/issues/2752 cy.getByTestId('oauth-code-container') .contains('Copy') - .click({ - // TODO: forcing, because currently we have needless re-renders, that causing - // button to disappear for some time and to be unclickable - force: true, - }); + .click(); }); }); - - function assertPermissions() { - cy.url().should('include', '/oauth/permissions'); - - cy.getByTestId('auth-header').should('contain', 'Application permissions'); - cy.getByTestId('auth-body').should( - 'contain', - 'Access to your profile data (except E‑mail)', - ); - cy.getByTestId('auth-body').should( - 'contain', - 'Access to your E‑mail address', - ); - } }); + +function assertPermissions() { + cy.url().should('include', '/oauth/permissions'); + + cy.getByTestId('auth-header').should('contain', 'Application permissions'); + cy.getByTestId('auth-body').should( + 'contain', + 'Access to your profile data (except E‑mail)', + ); + cy.getByTestId('auth-body').should( + 'contain', + 'Access to your E‑mail address', + ); +} diff --git a/tests-e2e/cypress/integration/auth/sign-in.test.ts b/tests-e2e/cypress/integration/auth/sign-in.test.ts index 5e3fc2f..6669dba 100644 --- a/tests-e2e/cypress/integration/auth/sign-in.test.ts +++ b/tests-e2e/cypress/integration/auth/sign-in.test.ts @@ -46,6 +46,16 @@ describe('Sign in / Log out', () => { }); }); + it('should force to /login page', () => { + cy.visit('/'); + + cy.location('pathname').should('eq', '/login'); + + cy.getByTestId('home-page').click(); + + cy.location('pathname').should('eq', '/login'); + }); + it('should sign in without remember me', () => { cy.visit('/'); diff --git a/tests-e2e/cypress/support/index.d.ts b/tests-e2e/cypress/support/index.d.ts index 33df5a7..ddcb51c 100644 --- a/tests-e2e/cypress/support/index.d.ts +++ b/tests-e2e/cypress/support/index.d.ts @@ -30,8 +30,20 @@ declare namespace Cypress { /** * Whether return raw api response without any conversion. Defaults to: `false` */ - rawApiResp?: boolean; + rawApiResp?: false; }): Promise<{ accounts: TAccount[] }>; + login(options: { + accounts: AccountAlias[]; + updateState?: boolean; + rawApiResp: true; + }): Promise<{ + accounts: { + success: true; + access_token: string; + expires_in: number; + refresh_token: string; + }[]; + }>; getByTestId( id: string,