diff --git a/packages/app/components/auth/actions.ts b/packages/app/components/auth/actions.ts index 12a85ee..c84ce29 100644 --- a/packages/app/components/auth/actions.ts +++ b/packages/app/components/auth/actions.ts @@ -19,7 +19,6 @@ import { activate as activateEndpoint, resendActivation as resendActivationEndpoint, } from 'app/services/api/signup'; -import dispatchBsod from 'app/components/ui/bsod/dispatchBsod'; import { create as createPopup } from 'app/components/ui/popup/actions'; import ContactForm from 'app/components/contact'; import { Account } from 'app/components/accounts/reducer'; @@ -455,11 +454,6 @@ function handleOauthParamsValidation( userMessage?: string; } = {}, ) { - // TODO: it would be better to dispatch BSOD from the initial request performers - if (resp.error !== 'invalid_user_code') { - dispatchBsod(); - } - localStorage.removeItem('oauthData'); // eslint-disable-next-line no-alert diff --git a/packages/app/components/auth/authError/AuthError.tsx b/packages/app/components/auth/authError/AuthError.tsx index b3582e8..369c946 100644 --- a/packages/app/components/auth/authError/AuthError.tsx +++ b/packages/app/components/auth/authError/AuthError.tsx @@ -31,7 +31,7 @@ const AuthError: ComponentType = ({ error, onClose }) => { }, [error, onClose]); return ( - + {resolveError(error)} ); diff --git a/packages/app/components/auth/deviceCode/DeviceCodeBody.tsx b/packages/app/components/auth/deviceCode/DeviceCodeBody.tsx index ddbaa33..e0e97d4 100644 --- a/packages/app/components/auth/deviceCode/DeviceCodeBody.tsx +++ b/packages/app/components/auth/deviceCode/DeviceCodeBody.tsx @@ -20,7 +20,6 @@ export default class DeviceCodeBody extends BaseAuthBody { { onClose?: () => void; } -export const PanelBodyHeader: FC = ({ type = 'default', onClose, children }) => { +export const PanelBodyHeader: FC = ({ type = 'default', onClose, children, ...props }) => { const [isClosed, setIsClosed] = useState(false); const handleCloseClick = useCallback(() => { setIsClosed(true); @@ -71,6 +71,7 @@ export const PanelBodyHeader: FC = ({ type = 'default', on [styles.errorBodyHeader]: type === 'error', [styles.isClosed]: isClosed, })} + {...props} > {type === 'error' && } {children} diff --git a/packages/app/containers/AuthFlowRouteContents.tsx b/packages/app/containers/AuthFlowRouteContents.tsx index ab3e526..ca03d16 100644 --- a/packages/app/containers/AuthFlowRouteContents.tsx +++ b/packages/app/containers/AuthFlowRouteContents.tsx @@ -13,6 +13,10 @@ const AuthFlowRouteContents: FC = ({ component: WantedComponent, location const [component, setComponent] = useState(null); useEffect(() => { + // Promise that will be resolved after handleRequest might contain already non-actual component to render, + // so set it to false in the effect's clear function to prevent unwanted UI state + let isActual = true; + authFlow.handleRequest( { path: location.pathname, @@ -21,11 +25,15 @@ const AuthFlowRouteContents: FC = ({ component: WantedComponent, location }, history.push, () => { - if (isMounted()) { + if (isActual && isMounted()) { setComponent(); } }, ); + + return () => { + isActual = false; + }; }, [location.pathname, location.search]); return component; diff --git a/packages/app/services/authFlow/AcceptRulesState.ts b/packages/app/services/authFlow/AcceptRulesState.ts index d5c8308..3acb4b2 100644 --- a/packages/app/services/authFlow/AcceptRulesState.ts +++ b/packages/app/services/authFlow/AcceptRulesState.ts @@ -1,5 +1,3 @@ -import logger from 'app/services/logger'; - import AbstractState from './AbstractState'; import { AuthContext } from './AuthFlow'; import CompleteState from './CompleteState'; @@ -16,10 +14,7 @@ export default class AcceptRulesState extends AbstractState { } resolve(context: AuthContext): Promise | void { - context - .run('acceptRules') - .then(() => context.setState(new CompleteState())) - .catch((err = {}) => err.errors || logger.warn('Error accepting rules', err)); + return context.run('acceptRules').then(() => context.setState(new CompleteState())); } reject(context: AuthContext, payload: Record): void { diff --git a/packages/app/services/authFlow/ActivationState.ts b/packages/app/services/authFlow/ActivationState.ts index 8581b20..9112af5 100644 --- a/packages/app/services/authFlow/ActivationState.ts +++ b/packages/app/services/authFlow/ActivationState.ts @@ -1,4 +1,3 @@ -import logger from 'app/services/logger'; import { AuthContext } from 'app/services/authFlow'; import AbstractState from './AbstractState'; @@ -12,10 +11,7 @@ export default class ActivationState extends AbstractState { } resolve(context: AuthContext, payload: { key: string }): Promise | void { - context - .run('activate', payload.key) - .then(() => context.setState(new CompleteState())) - .catch((err = {}) => err.errors || logger.warn('Error activating account', err)); + return context.run('activate', payload.key).then(() => context.setState(new CompleteState())); } reject(context: AuthContext): void { diff --git a/packages/app/services/authFlow/AuthFlow.ts b/packages/app/services/authFlow/AuthFlow.ts index 9c11d22..2611a17 100644 --- a/packages/app/services/authFlow/AuthFlow.ts +++ b/packages/app/services/authFlow/AuthFlow.ts @@ -10,8 +10,9 @@ import { } from 'app/components/accounts/actions'; import * as actions from 'app/components/auth/actions'; import { updateUser } from 'app/components/user/actions'; -import FinishState from './FinishState'; +import dispatchBsod from 'app/components/ui/bsod/dispatchBsod'; +import FinishState from './FinishState'; import RegisterState from './RegisterState'; import LoginState from './LoginState'; import InitOAuthAuthCodeFlowState from './InitOAuthAuthCodeFlowState'; @@ -121,11 +122,23 @@ export default class AuthFlow implements AuthContext { } resolve(payload: Record = {}) { - this.state.resolve(this, payload); + const maybePromise = this.state.resolve(this, payload); + + if (maybePromise && maybePromise.catch) { + maybePromise.catch((err) => { + dispatchBsod(); + throw err; + }); + } } reject(payload: Record = {}) { - this.state.reject(this, payload); + try { + this.state.reject(this, payload); + } catch (err) { + dispatchBsod(); + throw err; + } } goBack() { @@ -133,11 +146,11 @@ export default class AuthFlow implements AuthContext { } run(actionId: T, payload?: Parameters[0]): Promise { - // @ts-ignore the extended version of redux with thunk will return the correct promise + // @ts-expect-error the extended version of redux with thunk will return the correct promise return Promise.resolve(this.dispatch(this.actions[actionId](payload))); } - setState(state: State) { + setState(state: State): Promise | void { this.state?.leave(this); this.prevState = this.state; this.state = state; @@ -256,8 +269,6 @@ export default class AuthFlow implements AuthContext { /** * Tries to restore last oauth request, if it was stored in localStorage * in last 2 hours - * - * @returns {bool} - whether oauth state is being restored */ private restoreOAuthState(): boolean { if (this.oAuthStateRestored) { diff --git a/packages/app/services/authFlow/ChooseAccountState.ts b/packages/app/services/authFlow/ChooseAccountState.ts index a04879b..143341e 100644 --- a/packages/app/services/authFlow/ChooseAccountState.ts +++ b/packages/app/services/authFlow/ChooseAccountState.ts @@ -1,4 +1,5 @@ import type { Account } from 'app/components/accounts/reducer'; +import logger from 'app/services/logger'; import AbstractState from './AbstractState'; import { AuthContext } from './AuthFlow'; @@ -21,10 +22,16 @@ export default class ChooseAccountState extends AbstractState { // So if there is no `id` property, it's an empty object resolve(context: AuthContext, payload: Account): Promise | void { if (payload.id) { - return context - .run('authenticate', payload) - .then(() => context.run('setAccountSwitcher', false)) - .then(() => context.setState(new CompleteState())); + return ( + context + .run('authenticate', payload) + .then(() => context.run('setAccountSwitcher', false)) + .then(() => context.setState(new CompleteState())) + // By default, this error must cause a BSOD. But by I don't know why reasons it shouldn't, + // because somebody somewhere catches an invalid authentication result and routes the user + // to the password entering form. To keep this behavior we catch all errors, log it and suppress + .catch((err) => err.errors || logger.warn('Error choosing an account', err)) + ); } // log in to another account diff --git a/packages/app/services/authFlow/CompleteState.ts b/packages/app/services/authFlow/CompleteState.ts index 95a46ee..3ea52d5 100644 --- a/packages/app/services/authFlow/CompleteState.ts +++ b/packages/app/services/authFlow/CompleteState.ts @@ -92,45 +92,49 @@ export default class CompleteState extends AbstractState { user.isDeleted || hasPrompt(oauth.prompt, PROMPT_ACCOUNT_CHOOSE)) ) { - context.setState(new ChooseAccountState()); - } else if (user.isDeleted) { + return context.setState(new ChooseAccountState()); + } + + if (user.isDeleted) { // you shall not pass // if we are here, this means that user have already seen account // switcher and now we should redirect him to his profile, // because oauth is not available for deleted accounts - context.navigate('/'); - } else if (oauth.code) { - context.setState(new FinishState()); - } else { - const data: Record = {}; - - if (typeof this.isPermissionsAccepted !== 'undefined') { - data.accept = this.isPermissionsAccepted; - } else if (oauth.acceptRequired || hasPrompt(oauth.prompt, PROMPT_PERMISSIONS)) { - context.setState(new PermissionsState()); - - return; - } - - // TODO: it seems that oAuthComplete may be a separate state - return context - .run('oAuthComplete', data) - .then((resp: { redirectUri?: string }) => { - // TODO: пусть в стейт попадает флаг или тип авторизации - // вместо волшебства над редирект урлой - if (!resp.redirectUri || resp.redirectUri.includes('static_page')) { - context.setState(new FinishState()); - } else { - return context.run('redirect', resp.redirectUri); - } - }) - .catch((resp) => { - if (resp.unauthorized) { - context.setState(new LoginState()); - } else if (resp.acceptRequired) { - context.setState(new PermissionsState()); - } - }); + return context.navigate('/'); } + + if (oauth.code) { + return context.setState(new FinishState()); + } + + const data: Record = {}; + + if (typeof this.isPermissionsAccepted !== 'undefined') { + data.accept = this.isPermissionsAccepted; + } else if (oauth.acceptRequired || hasPrompt(oauth.prompt, PROMPT_PERMISSIONS)) { + context.setState(new PermissionsState()); + + return; + } + + // TODO: it seems that oAuthComplete may be a separate state + return context + .run('oAuthComplete', data) + .then((resp: { redirectUri?: string }) => { + // TODO: пусть в стейт попадает флаг или тип авторизации + // вместо волшебства над редирект урлой + if (!resp.redirectUri || resp.redirectUri.includes('static_page')) { + context.setState(new FinishState()); + } else { + return context.run('redirect', resp.redirectUri); + } + }) + .catch((resp) => { + if (resp.unauthorized) { + context.setState(new LoginState()); + } else if (resp.acceptRequired) { + context.setState(new PermissionsState()); + } + }); } } diff --git a/packages/app/services/authFlow/DeviceCodeState.ts b/packages/app/services/authFlow/DeviceCodeState.ts index 4d80169..a4adb50 100644 --- a/packages/app/services/authFlow/DeviceCodeState.ts +++ b/packages/app/services/authFlow/DeviceCodeState.ts @@ -6,7 +6,7 @@ export default class DeviceCodeState extends AbstractState { async resolve(context: AuthContext, payload: { user_code: string }): Promise { const { query } = context.getRequest(); - context + return context .run('oAuthValidate', { params: { userCode: payload.user_code, @@ -16,7 +16,7 @@ export default class DeviceCodeState extends AbstractState { }) .then(() => context.setState(new CompleteState())) .catch((err) => { - if (err.error === 'invalid_user_code') { + if (['invalid_user_code', 'expired_token', 'used_user_code'].includes(err.error)) { return context.run('setErrors', { [err.parameter]: err.error }); } diff --git a/packages/app/services/authFlow/ForgotPasswordState.ts b/packages/app/services/authFlow/ForgotPasswordState.ts index d323b4f..2e5920f 100644 --- a/packages/app/services/authFlow/ForgotPasswordState.ts +++ b/packages/app/services/authFlow/ForgotPasswordState.ts @@ -1,5 +1,3 @@ -import logger from 'app/services/logger'; - import AbstractState from './AbstractState'; import { AuthContext } from './AuthFlow'; import LoginState from './LoginState'; @@ -11,13 +9,10 @@ export default class ForgotPasswordState extends AbstractState { } resolve(context: AuthContext, payload: { login: string; captcha: string }): Promise | void { - context - .run('forgotPassword', payload) - .then(() => { - context.run('setLogin', payload.login); - context.setState(new RecoverPasswordState()); - }) - .catch((err = {}) => err.errors || logger.warn('Error requesting password recoverage', err)); + return context.run('forgotPassword', payload).then(() => { + context.run('setLogin', payload.login); + context.setState(new RecoverPasswordState()); + }); } goBack(context: AuthContext): void { diff --git a/packages/app/services/authFlow/InitOAuthDeviceCodeFlowState.ts b/packages/app/services/authFlow/InitOAuthDeviceCodeFlowState.ts index 022f1db..74d14ee 100644 --- a/packages/app/services/authFlow/InitOAuthDeviceCodeFlowState.ts +++ b/packages/app/services/authFlow/InitOAuthDeviceCodeFlowState.ts @@ -16,7 +16,8 @@ export default class InitOAuthDeviceCodeFlowState extends AbstractState { description: query.get('description')!, prompt: query.get('prompt')!, }); - await context.setState(new CompleteState()); + + return context.setState(new CompleteState()); } catch { // Ok, fallback to the default } diff --git a/packages/app/services/authFlow/LoginState.ts b/packages/app/services/authFlow/LoginState.ts index b67b2b9..3f6b583 100644 --- a/packages/app/services/authFlow/LoginState.ts +++ b/packages/app/services/authFlow/LoginState.ts @@ -30,7 +30,7 @@ export default class LoginState extends AbstractState { login: string; }, ): Promise | void { - context + return context .run('login', payload) .then(() => context.setState(new PasswordState())) .catch((err = {}) => err.errors || logger.warn('Error validating login', err)); diff --git a/packages/app/services/authFlow/PermissionsState.ts b/packages/app/services/authFlow/PermissionsState.ts index 6aa70a8..2d59a23 100644 --- a/packages/app/services/authFlow/PermissionsState.ts +++ b/packages/app/services/authFlow/PermissionsState.ts @@ -12,15 +12,15 @@ export default class PermissionsState extends AbstractState { } resolve(context: AuthContext): Promise | void { - this.process(context, true); + return this.process(context, true); } reject(context: AuthContext): void { this.process(context, false); } - process(context: AuthContext, accept: boolean): void { - context.setState( + process(context: AuthContext, accept: boolean): Promise | void { + return context.setState( new CompleteState({ accept, }), diff --git a/packages/app/services/authFlow/RecoverPasswordState.ts b/packages/app/services/authFlow/RecoverPasswordState.ts index 79f9bfd..88803c6 100644 --- a/packages/app/services/authFlow/RecoverPasswordState.ts +++ b/packages/app/services/authFlow/RecoverPasswordState.ts @@ -1,5 +1,3 @@ -import logger from 'app/services/logger'; - import AbstractState from './AbstractState'; import { AuthContext } from './AuthFlow'; import LoginState from './LoginState'; @@ -17,10 +15,7 @@ export default class RecoverPasswordState extends AbstractState { context: AuthContext, payload: { key: string; newPassword: string; newRePassword: string }, ): Promise | void { - context - .run('recoverPassword', payload) - .then(() => context.setState(new CompleteState())) - .catch((err = {}) => err.errors || logger.warn('Error recovering password', err)); + return context.run('recoverPassword', payload).then(() => context.setState(new CompleteState())); } goBack(context: AuthContext): void { diff --git a/packages/app/services/authFlow/RegisterState.ts b/packages/app/services/authFlow/RegisterState.ts index fbc0561..a41b850 100644 --- a/packages/app/services/authFlow/RegisterState.ts +++ b/packages/app/services/authFlow/RegisterState.ts @@ -1,5 +1,3 @@ -import logger from 'app/services/logger'; - import AbstractState from './AbstractState'; import { AuthContext } from './AuthFlow'; import CompleteState from './CompleteState'; @@ -22,10 +20,7 @@ export default class RegisterState extends AbstractState { rulesAgreement: boolean; }, ): Promise | void { - context - .run('register', payload) - .then(() => context.setState(new CompleteState())) - .catch((err = {}) => err.errors || logger.warn('Error registering', err)); + return context.run('register', payload).then(() => context.setState(new CompleteState())); } reject(context: AuthContext, payload: Record): void { diff --git a/packages/app/services/authFlow/ResendActivationState.ts b/packages/app/services/authFlow/ResendActivationState.ts index 5796976..1e1dd1b 100644 --- a/packages/app/services/authFlow/ResendActivationState.ts +++ b/packages/app/services/authFlow/ResendActivationState.ts @@ -1,5 +1,4 @@ import { AuthContext } from 'app/services/authFlow'; -import logger from 'app/services/logger'; import AbstractState from './AbstractState'; import ActivationState from './ActivationState'; @@ -11,10 +10,7 @@ export default class ResendActivationState extends AbstractState { } resolve(context: AuthContext, payload: { email: string; captcha: string }): Promise | void { - context - .run('resendActivation', payload) - .then(() => context.setState(new ActivationState())) - .catch((err = {}) => err.errors || logger.warn('Error resending activation', err)); + return context.run('resendActivation', payload).then(() => context.setState(new ActivationState())); } reject(context: AuthContext): void { diff --git a/packages/app/services/errorsDict/errorsDict.tsx b/packages/app/services/errorsDict/errorsDict.tsx index c19fc8b..87775bc 100644 --- a/packages/app/services/errorsDict/errorsDict.tsx +++ b/packages/app/services/errorsDict/errorsDict.tsx @@ -129,6 +129,13 @@ const errorsMap: Record) => ReactElement> = 'error.redirectUri_invalid': () => , invalid_user_code: () => , + expired_token: () => ( + + ), + used_user_code: () => , }; interface ErrorLiteral { diff --git a/tests-e2e/cypress/integration/auth/oauth.test.ts b/tests-e2e/cypress/integration/auth/oauth.test.ts index fbae030..3f54909 100644 --- a/tests-e2e/cypress/integration/auth/oauth.test.ts +++ b/tests-e2e/cypress/integration/auth/oauth.test.ts @@ -10,35 +10,175 @@ const defaults = { }; describe('OAuth', () => { - it('should complete oauth', () => { - cy.login({ accounts: ['default'] }); + describe('AuthCode grant flow', () => { + it('should complete oauth', () => { + cy.login({ accounts: ['default'] }); - cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`); + cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`); - cy.url().should('equal', 'https://dev.ely.by/'); + cy.url().should('equal', 'https://dev.ely.by/'); + }); + + it('should restore previous oauthData if any', () => { + localStorage.setItem( + 'oauthData', + JSON.stringify({ + timestamp: Date.now() - 3600, + payload: { + params: { + clientId: 'ely', + redirectUrl: 'https://dev.ely.by/authorization/oauth', + responseType: 'code', + state: '', + scope: 'account_info account_email', + }, + } as OAuthState, + }), + ); + cy.login({ accounts: ['default'] }); + + cy.visit('/'); + + cy.url().should('equal', 'https://dev.ely.by/'); + }); + + 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( + `/oauth2/v1/ely?${new URLSearchParams({ + ...defaults, + client_id: 'tlauncher', + redirect_uri: 'static_page', + })}`, + ); + + 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( + `/oauth2/v1/ely?${new URLSearchParams({ + ...defaults, + client_id: 'tlauncher', + redirect_uri: 'static_page_with_code', + })}`, + ); + + cy.wait('@complete'); + + cy.url().should('include', 'oauth/finish#{%22auth_code%22:'); + + cy.findByTestId('oauth-code-container').should('contain', 'provide the following code'); + + // just click on copy, but we won't assert if the string was copied + // because it is a little bit complicated + // https://github.com/cypress-io/cypress/issues/2752 + cy.findByTestId('oauth-code-container').contains('Copy').click(); + }); + }); }); - it('should restore previous oauthData if any', () => { - localStorage.setItem( - 'oauthData', - JSON.stringify({ - timestamp: Date.now() - 3600, - payload: { - params: { - clientId: 'ely', - redirectUrl: 'https://dev.ely.by/authorization/oauth', - responseType: 'code', - state: '', - scope: 'account_info account_email', - }, - } as OAuthState, - }), - ); - cy.login({ accounts: ['default'] }); + describe('DeviceCode grant flow', () => { + it('should complete flow by complete uri', () => { + cy.login({ accounts: ['default'] }); - cy.visit('/'); + cy.visit('/code?user_code=E2E-APPROVED'); - cy.url().should('equal', 'https://dev.ely.by/'); + cy.location('pathname').should('eq', '/oauth/finish'); + cy.get('[data-e2e-content]').contains('successfully completed'); + }); + + it('should complete flow with manual approve', () => { + cy.login({ accounts: ['default'] }); + + cy.visit('/code'); + + cy.get('[name=user_code]').type('E2E-UNAPPROVED{enter}'); + + cy.location('pathname').should('eq', '/oauth/permissions'); + + cy.findByTestId('auth-controls').contains('Approve').click(); + + cy.location('pathname').should('eq', '/oauth/finish'); + cy.get('[data-e2e-content]').contains('successfully completed'); + }); + + it('should complete flow with auto approve', () => { + cy.login({ accounts: ['default'] }); + + cy.visit('/code'); + + cy.get('[name=user_code]').type('E2E-APPROVED{enter}'); + + cy.location('pathname').should('eq', '/oauth/finish'); + cy.get('[data-e2e-content]').contains('successfully completed'); + }); + + it('should complete flow by declining the code', () => { + cy.login({ accounts: ['default'] }); + + cy.visit('/code'); + + cy.get('[name=user_code]').type('E2E-UNAPPROVED{enter}'); + + cy.location('pathname').should('eq', '/oauth/permissions'); + + cy.findByTestId('auth-controls-secondary').contains('Decline').click(); + + cy.location('pathname').should('eq', '/oauth/finish'); + cy.get('[data-e2e-content]').contains('was failed'); + }); + + it('should show an error for an unknown code', () => { + cy.login({ accounts: ['default'] }); + + cy.visit('/code'); + + cy.get('[name=user_code]').type('UNKNOWN-CODE{enter}'); + + cy.location('pathname').should('eq', '/code'); + cy.findByTestId('auth-error').contains('Invalid Device Code'); + }); + + it('should show an error for an expired code', () => { + cy.login({ accounts: ['default'] }); + + cy.visit('/code'); + + cy.get('[name=user_code]').type('E2E-EXPIRED{enter}'); + + cy.location('pathname').should('eq', '/code'); + cy.findByTestId('auth-error').contains('The code has expired'); + }); + + it('should show an error for an expired code', () => { + cy.login({ accounts: ['default'] }); + + cy.visit('/code'); + + cy.get('[name=user_code]').type('E2E-COMPLETED{enter}'); + + cy.location('pathname').should('eq', '/code'); + cy.findByTestId('auth-error').contains('This code has been already used'); + }); }); describe('AccountSwitcher', () => { @@ -408,59 +548,6 @@ describe('OAuth', () => { cy.url().should('match', /^http:\/\/localhost:8080\/?\?code=[^&]+$/); }); }); - - 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( - `/oauth2/v1/ely?${new URLSearchParams({ - ...defaults, - client_id: 'tlauncher', - redirect_uri: 'static_page', - })}`, - ); - - 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( - `/oauth2/v1/ely?${new URLSearchParams({ - ...defaults, - client_id: 'tlauncher', - redirect_uri: 'static_page_with_code', - })}`, - ); - - cy.wait('@complete'); - - cy.url().should('include', 'oauth/finish#{%22auth_code%22:'); - - cy.findByTestId('oauth-code-container').should('contain', 'provide the following code'); - - // just click on copy, but we won't assert if the string was copied - // because it is a little bit complicated - // https://github.com/cypress-io/cypress/issues/2752 - cy.findByTestId('oauth-code-container').contains('Copy').click(); - }); - }); }); function assertPermissions() {