accounts-frontend/tests-e2e/cypress/integration/auth/oauth.test.ts
ErickSkrauch be08857edc
Add E2E tests for device code grant flow.
Handle more errors for device code.
Dispatch a BSOD for an any unhandled exception from auth flow state
2024-12-18 01:02:02 +01:00

560 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { account1 } from '../../fixtures/accounts.json';
import { OAuthState } from 'app/components/auth/reducer';
import { UserResponse } from 'app/services/api/accounts';
const defaults = {
client_id: 'ely',
redirect_uri: 'https://dev.ely.by/authorization/oauth',
response_type: 'code',
scope: 'account_info,account_email',
};
describe('OAuth', () => {
describe('AuthCode grant flow', () => {
it('should complete oauth', () => {
cy.login({ accounts: ['default'] });
cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`);
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();
});
});
});
describe('DeviceCode grant flow', () => {
it('should complete flow by complete uri', () => {
cy.login({ accounts: ['default'] });
cy.visit('/code?user_code=E2E-APPROVED');
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', () => {
it('should ask to choose an account if user has multiple', () => {
cy.login({ accounts: ['default', 'default2'] }).then(({ accounts: [account] }) => {
cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`);
cy.url().should('include', '/oauth/choose-account');
cy.findByTestId('auth-header').should('contain', 'Choose an account');
cy.findByTestId('auth-body').contains(account.email).click();
cy.url().should('equal', 'https://dev.ely.by/');
});
});
});
describe('Permissions prompt', () => {
// TODO: remove api mocks, when we will be able to revoke permissions
it('should prompt for permissions', () => {
cy.server();
cy.route({
method: 'POST',
// NOTE: can not use cypress glob syntax, because it will break due to
// '%2F%2F' (//) in redirect_uri
// url: '/api/oauth2/v1/complete/*',
url: new RegExp('/api/oauth2/v1/complete'),
response: {
statusCode: 401,
error: 'accept_required',
},
status: 401,
}).as('complete');
cy.login({ accounts: ['default'] });
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
client_id: 'tlauncher',
redirect_uri: 'http://localhost:8080',
state: '123',
})}`,
);
cy.wait('@complete');
assertPermissions();
cy.server({ enable: false });
cy.findByTestId('auth-controls').contains('Approve').click();
cy.url().should('match', /^http:\/\/localhost:8080\/?\?code=[^&]+&state=123$/);
});
it('should redirect to error page, when permission request declined', () => {
cy.server();
cy.route({
method: 'POST',
// NOTE: can not use cypress glob syntax, because it will break due to
// '%2F%2F' (//) in redirect_uri
// url: '/api/oauth2/v1/complete/*',
url: new RegExp('/api/oauth2/v1/complete'),
response: {
statusCode: 401,
error: 'accept_required',
},
status: 401,
}).as('complete');
cy.login({ accounts: ['default'] });
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
client_id: 'tlauncher',
redirect_uri: 'http://localhost:8080',
})}`,
);
cy.wait('@complete');
assertPermissions();
cy.server({ enable: false });
cy.findByTestId('auth-controls-secondary').contains('Decline').click();
cy.url().should('include', 'error=access_denied');
});
});
describe('Sign-in during oauth', () => {
it('should allow sign in during oauth (guest oauth)', () => {
cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`);
cy.location('pathname').should('eq', '/login');
cy.get('[name=login]').type(`${account1.login}{enter}`);
cy.url().should('include', '/password');
cy.get('[name=password]').type(`${account1.password}{enter}`);
cy.url().should('equal', 'https://dev.ely.by/');
});
});
describe('Deleted account', () => {
it('should show account switcher and then abort oauth and redirect to profile', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account1.id}`,
response: {
id: account.id,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: account.username,
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: account.email,
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.findByTestId('auth-header').should('contain', 'Choose an account');
cy.findByTestId('auth-body').contains(account.email).click();
cy.location('pathname').should('eq', '/');
cy.findByTestId('deletedAccount').should('contain', 'Account is deleted');
});
});
it('should allow sign and then abort oauth and redirect to profile', () => {
cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`);
cy.location('pathname').should('eq', '/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');
});
});
describe('login_hint', () => {
it('should automatically choose account, when id in login_hint is present', () => {
cy.login({ accounts: ['default', 'default2'] }).then(({ accounts: [account] }) => {
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
// suggest preferred username
// https://docs.ely.by/ru/oauth.html#id3
login_hint: String(account.id),
}).toString()}`,
);
cy.url().should('equal', 'https://dev.ely.by/');
});
});
it('should automatically choose account, when email in login_hint is present', () => {
cy.login({ accounts: ['default', 'default2'] }).then(({ accounts: [account] }) => {
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
// suggest preferred username
// https://docs.ely.by/ru/oauth.html#id3
login_hint: account.email,
})}`,
);
cy.url().should('equal', 'https://dev.ely.by/');
});
});
it('should automatically choose account, when username in login_hint is present and it is not an active account', () => {
cy.login({ accounts: ['default2', 'default'] }).then(
({
// try to authenticate with an account, that is not currently active one
accounts: [, account],
}) => {
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
// suggest preferred username
// https://docs.ely.by/ru/oauth.html#id3
login_hint: account.username,
})}`,
);
cy.url().should('equal', 'https://dev.ely.by/');
},
);
});
});
describe('prompts', () => {
it('should prompt for account', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
prompt: 'select_account',
})}`,
);
cy.url().should('include', '/oauth/choose-account');
cy.findByTestId('auth-header').should('contain', 'Choose an account');
cy.findByTestId('auth-body').contains(account.email).click();
cy.url().should('equal', 'https://dev.ely.by/');
});
});
it('should allow sign in with another account', () => {
cy.login({ accounts: ['default2'] });
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
prompt: 'select_account',
})}`,
);
cy.url().should('include', '/oauth/choose-account');
cy.findByTestId('auth-controls').contains('another account').click();
cy.location('pathname').should('eq', '/login');
cy.get('[name=login]').type(`${account1.login}{enter}`);
cy.url().should('include', '/password');
cy.get('[name=password]').type(`${account1.password}{enter}`);
cy.url().should('equal', 'https://dev.ely.by/');
});
it('should prompt for permissions', () => {
cy.login({ accounts: ['default'] });
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
client_id: 'tlauncher',
redirect_uri: 'http://localhost:8080',
prompt: 'consent',
})}`,
);
assertPermissions();
cy.findByTestId('auth-controls').contains('Approve').click();
cy.url().should('match', /^http:\/\/localhost:8080\/?\?code=[^&]+$/);
});
it('should redirect to error page, when permission request declined', () => {
cy.login({ accounts: ['default'] });
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
client_id: 'tlauncher',
redirect_uri: 'http://localhost:8080',
prompt: 'consent',
})}`,
);
cy.url().should('include', '/oauth/permissions');
cy.findByTestId('auth-controls-secondary').contains('Decline').click();
cy.url().should('include', 'error=access_denied');
});
it('should prompt for both account and permissions', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
client_id: 'tlauncher',
redirect_uri: 'http://localhost:8080',
prompt: 'select_account,consent',
})}`,
);
cy.url().should('include', '/oauth/choose-account');
cy.findByTestId('auth-header').should('contain', 'Choose an account');
cy.findByTestId('auth-body').contains(account.email).click();
assertPermissions();
cy.findByTestId('auth-controls').contains('Approve').click();
cy.url().should('match', /^http:\/\/localhost:8080\/?\?code=[^&]+$/);
});
});
it('should allow sign in during oauth (guest oauth)', () => {
cy.visit(
`/oauth2/v1/ely?${new URLSearchParams({
...defaults,
client_id: 'tlauncher',
redirect_uri: 'http://localhost:8080',
prompt: 'select_account,consent',
})}`,
);
cy.location('pathname').should('eq', '/login');
cy.get('[name=login]').type(`${account1.login}{enter}`);
cy.url().should('include', '/password');
cy.get('[name=password]').type(`${account1.password}{enter}`);
assertPermissions();
cy.findByTestId('auth-controls').contains('Approve').click();
cy.url().should('match', /^http:\/\/localhost:8080\/?\?code=[^&]+$/);
});
});
});
function assertPermissions() {
cy.url().should('include', '/oauth/permissions');
cy.findByTestId('auth-header').should('contain', 'Application permissions');
cy.findByTestId('auth-body').should('contain', 'Access to your profile data (except Email)');
cy.findByTestId('auth-body').should('contain', 'Access to your Email address');
}