diff --git a/packages/app/services/authFlow/AcceptRulesState.test.ts b/packages/app/services/authFlow/AcceptRulesState.test.ts index 75de1ca..cabecaf 100644 --- a/packages/app/services/authFlow/AcceptRulesState.test.ts +++ b/packages/app/services/authFlow/AcceptRulesState.test.ts @@ -47,6 +47,20 @@ describe('AcceptRulesState', () => { state.enter(context); }); + + it('should transition to complete state if account is deleted even if user should accept rules', () => { + context.getState.returns({ + user: { + shouldAcceptRules: true, + isGuest: false, + isDeleted: true, + }, + }); + + expectState(mock, CompleteState); + + state.enter(context); + }); }); describe('#resolve', () => { diff --git a/packages/app/services/authFlow/AcceptRulesState.ts b/packages/app/services/authFlow/AcceptRulesState.ts index 7e01504..d5c8308 100644 --- a/packages/app/services/authFlow/AcceptRulesState.ts +++ b/packages/app/services/authFlow/AcceptRulesState.ts @@ -8,7 +8,7 @@ export default class AcceptRulesState extends AbstractState { enter(context: AuthContext): Promise | void { const { user } = context.getState(); - if (user.shouldAcceptRules) { + if (!user.isDeleted && user.shouldAcceptRules) { context.navigate('/accept-rules'); } else { context.setState(new CompleteState()); diff --git a/packages/app/services/authFlow/CompleteState.test.ts b/packages/app/services/authFlow/CompleteState.test.ts index 66270a7..e718dcb 100644 --- a/packages/app/services/authFlow/CompleteState.test.ts +++ b/packages/app/services/authFlow/CompleteState.test.ts @@ -71,6 +71,22 @@ describe('CompleteState', () => { state.enter(context); }); + it('should navigate to the / if account is deleted', () => { + context.getState.returns({ + user: { + isGuest: false, + isActive: true, + shouldAcceptRules: true, + isDeleted: true, + }, + auth: {}, + }); + + expectNavigate(mock, '/'); + + state.enter(context); + }); + it('should transition to accept-rules if shouldAcceptRules', () => { context.getState.returns({ user: { diff --git a/packages/app/services/authFlow/CompleteState.ts b/packages/app/services/authFlow/CompleteState.ts index 888708b..19a8027 100644 --- a/packages/app/services/authFlow/CompleteState.ts +++ b/packages/app/services/authFlow/CompleteState.ts @@ -34,6 +34,8 @@ export default class CompleteState extends AbstractState { context.setState(new LoginState()); } else if (!user.isActive) { context.setState(new ActivationState()); + } else if (user.isDeleted) { + context.navigate('/'); } else if (user.shouldAcceptRules) { context.setState(new AcceptRulesState()); } else if (oauth && oauth.clientId) { diff --git a/tests-e2e/cypress/integration/auth/oauth.test.ts b/tests-e2e/cypress/integration/auth/oauth.test.ts index 5245ed0..f4d3965 100644 --- a/tests-e2e/cypress/integration/auth/oauth.test.ts +++ b/tests-e2e/cypress/integration/auth/oauth.test.ts @@ -1,4 +1,5 @@ import { account1 } from '../../fixtures/accounts.json'; +import { UserResponse } from 'app/services/api/accounts'; const defaults = { client_id: 'ely', @@ -16,6 +17,36 @@ describe('OAuth', () => { cy.url().should('equal', 'https://dev.ely.by/'); }); + it('should not complete oauth if account is deleted', () => { + cy.login({ accounts: ['default'] }); + + cy.server(); + cy.route({ + method: 'GET', + url: `/api/v1/accounts/${account1.id}`, + response: { + id: 7, + uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe', + username: 'SleepWalker', + isOtpEnabled: false, + registeredAt: 1475568334, + lang: 'en', + elyProfileLink: 'http://ely.by/u7', + email: 'danilenkos@auroraglobal.com', + isActive: true, + isDeleted: true, // force user into the deleted state + passwordChangedAt: 1476075696, + hasMojangUsernameCollision: true, + shouldAcceptRules: false, + } as UserResponse, + }); + + cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`); + + cy.location('pathname').should('eq', '/'); + cy.findByTestId('deletedAccount').should('contain', 'Account is deleted'); + }); + it('should restore previous oauthData if any', () => { localStorage.setItem( 'oauthData', @@ -105,6 +136,42 @@ describe('OAuth', () => { cy.url().should('equal', 'https://dev.ely.by/'); }); + it('should allow sign in during oauth and not finish process if the account is deleted', () => { + cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`); + + cy.url().should('include', '/login'); + + cy.get('[name=login]').type(`${account1.login}{enter}`); + + cy.url().should('include', '/password'); + + cy.server(); + cy.route({ + method: 'GET', + url: `/api/v1/accounts/${account1.id}`, + response: { + id: 7, + uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe', + username: 'SleepWalker', + isOtpEnabled: false, + registeredAt: 1475568334, + lang: 'en', + elyProfileLink: 'http://ely.by/u7', + email: 'danilenkos@auroraglobal.com', + isActive: true, + isDeleted: true, // force user into the deleted state + passwordChangedAt: 1476075696, + hasMojangUsernameCollision: true, + shouldAcceptRules: false, + } as UserResponse, + }); + + cy.get('[name=password]').type(`${account1.password}{enter}`); + + cy.location('pathname').should('eq', '/'); + cy.findByTestId('deletedAccount').should('contain', 'Account is deleted'); + }); + // TODO: enable, when backend api will return correct response on auth decline xit('should redirect to error page, when permission request declined', () => { cy.server(); diff --git a/tests-e2e/cypress/integration/auth/sign-in.test.ts b/tests-e2e/cypress/integration/auth/sign-in.test.ts index f10863a..f3280b2 100644 --- a/tests-e2e/cypress/integration/auth/sign-in.test.ts +++ b/tests-e2e/cypress/integration/auth/sign-in.test.ts @@ -1,4 +1,6 @@ import { account1, account2 } from '../../fixtures/accounts.json'; +import { UserResponse } from 'app/services/api/accounts'; +import { confirmWithPassword } from '../profile/utils'; describe('Sign in / Log out', () => { it('should sign in', () => { @@ -131,6 +133,189 @@ describe('Sign in / Log out', () => { cy.findByTestId('toolbar').should('contain', 'Join'); }); + it("should prompt for user agreement when the project's rules are changed", () => { + cy.visit('/'); + + cy.get('[name=login]').type(`${account1.login}{enter}`); + + cy.url().should('include', '/password'); + + cy.get('[name=password]').type(account1.password); + cy.get('[name=rememberMe]').should('be.checked'); + + cy.server(); + cy.route({ + method: 'POST', + url: `/api/v1/accounts/${account1.id}/rules`, + }).as('rulesAgreement'); + cy.route({ + method: 'GET', + url: `/api/v1/accounts/${account1.id}`, + response: { + id: 7, + uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe', + username: 'SleepWalker', + isOtpEnabled: false, + registeredAt: 1475568334, + lang: 'en', + elyProfileLink: 'http://ely.by/u7', + email: 'danilenkos@auroraglobal.com', + isActive: true, + isDeleted: false, + passwordChangedAt: 1476075696, + hasMojangUsernameCollision: true, + shouldAcceptRules: true, // force user to accept updated user agreement + } as UserResponse, + }); + + cy.get('[type=submit]').click(); + + cy.location('pathname').should('eq', '/accept-rules'); + + cy.get('[type=submit]').last().click(); // add .last() to match the new state during its transition + cy.wait('@rulesAgreement').its('requestBody').should('be.empty'); + + cy.location('pathname').should('eq', '/'); + cy.findByTestId('profile-index').should('contain', account1.username); + }); + + it('should allow logout from the user agreement prompt', () => { + cy.visit('/'); + + cy.get('[name=login]').type(`${account1.login}{enter}`); + + cy.url().should('include', '/password'); + + cy.get('[name=password]').type(account1.password); + cy.get('[name=rememberMe]').should('be.checked'); + + cy.server(); + cy.route({ + method: 'GET', + url: `/api/v1/accounts/${account1.id}`, + response: { + id: 7, + uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe', + username: 'SleepWalker', + isOtpEnabled: false, + registeredAt: 1475568334, + lang: 'en', + elyProfileLink: 'http://ely.by/u7', + email: 'danilenkos@auroraglobal.com', + isActive: true, + isDeleted: false, + passwordChangedAt: 1476075696, + hasMojangUsernameCollision: true, + shouldAcceptRules: true, // force user to accept updated user agreement + } as UserResponse, + }); + + cy.get('[type=submit]').click(); + + cy.location('pathname').should('eq', '/accept-rules'); + + cy.findByTestId('auth-controls-secondary').contains('Decline and logout').click(); + + cy.location('pathname').should('eq', '/login'); + cy.findByTestId('toolbar').should('contain', 'Join'); + }); + + it('should allow user to delete its own account from the user agreement prompt', () => { + cy.visit('/'); + + cy.get('[name=login]').type(`${account1.login}{enter}`); + + cy.url().should('include', '/password'); + + cy.get('[name=password]').type(account1.password); + cy.get('[name=rememberMe]').should('be.checked'); + + cy.server(); + cy.route({ + method: 'GET', + url: `/api/v1/accounts/${account1.id}`, + response: { + id: 7, + uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe', + username: 'SleepWalker', + isOtpEnabled: false, + registeredAt: 1475568334, + lang: 'en', + elyProfileLink: 'http://ely.by/u7', + email: 'danilenkos@auroraglobal.com', + isActive: true, + isDeleted: false, + passwordChangedAt: 1476075696, + hasMojangUsernameCollision: true, + shouldAcceptRules: true, // force user to accept updated user agreement + } as UserResponse, + }); + + cy.get('[type=submit]').click(); + + cy.location('pathname').should('eq', '/accept-rules'); + + cy.findByTestId('auth-controls-secondary').contains('Delete account').click(); + + cy.location('pathname').should('eq', '/profile/delete'); + + cy.route({ + method: 'DELETE', + url: `/api/v1/accounts/${account1.id}`, + }).as('deleteAccount'); + cy.route({ + method: 'GET', + url: `/api/v1/accounts/${account1.id}`, + response: { + id: 7, + uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe', + username: 'SleepWalker', + isOtpEnabled: false, + registeredAt: 1475568334, + lang: 'en', + elyProfileLink: 'http://ely.by/u7', + email: 'danilenkos@auroraglobal.com', + isActive: true, + isDeleted: true, // mock deleted state since the delete will not perform the real request + passwordChangedAt: 1476075696, + hasMojangUsernameCollision: true, + shouldAcceptRules: true, // rules still aren't accepted + } as UserResponse, + }); + + cy.get('[type=submit]').click(); + + cy.wait('@deleteAccount') + .its('requestBody') + .should( + 'eq', + new URLSearchParams({ + password: '', + }).toString(), + ); + + cy.route({ + method: 'DELETE', + url: `/api/v1/accounts/${account1.id}`, + response: { success: true }, + }).as('deleteAccount'); + + confirmWithPassword(account1.password); + + cy.wait('@deleteAccount') + .its('requestBody') + .should( + 'eq', + new URLSearchParams({ + password: account1.password, + }).toString(), + ); + + cy.location('pathname').should('eq', '/'); + + cy.findByTestId('deletedAccount').should('contain', 'Account is deleted'); + }); + describe('multi account', () => { it('should allow sign in with another account', () => { cy.login({ accounts: ['default2'] });