mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Cover oauth with e2e tests and fix some old and newly introduced bugs
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
describe('/dev/applications - user', () => {
|
||||
before(() => {
|
||||
cy.login({ account: 'default' }).then(({ user }) => {
|
||||
cy.login({ accounts: ['default'] }).then(({ user }) => {
|
||||
cy.visit('/dev/applications');
|
||||
|
||||
// remove all previousely added apps
|
||||
// remove all previously added apps
|
||||
cy.window().then(async (/** @type {any} */ { oauthApi }) => {
|
||||
const apps = await oauthApi.getAppsByUser(user.id);
|
||||
|
||||
|
321
tests-e2e/cypress/integration/oauth/user.test.ts
Normal file
321
tests-e2e/cypress/integration/oauth/user.test.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
const defaults = {
|
||||
client_id: 'ely',
|
||||
redirect_uri: 'http://ely.by/authorization/oauth',
|
||||
response_type: 'code',
|
||||
scope: 'account_info,account_email',
|
||||
};
|
||||
|
||||
it('should complete oauth', () => {
|
||||
cy.login({ accounts: ['default'] });
|
||||
|
||||
cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`);
|
||||
|
||||
cy.url().should('equal', 'https://ely.by/');
|
||||
});
|
||||
|
||||
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.getByTestId('auth-header').should('contain', 'Choose an account');
|
||||
|
||||
cy.getByTestId('auth-body')
|
||||
.contains(account.email)
|
||||
.click();
|
||||
|
||||
cy.url().should('equal', 'https://ely.by/');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// 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',
|
||||
})}`,
|
||||
);
|
||||
|
||||
cy.wait('@complete');
|
||||
|
||||
assertPermissions();
|
||||
|
||||
cy.server({ enable: false });
|
||||
|
||||
cy.getByTestId('auth-controls')
|
||||
.contains('Approve')
|
||||
.click();
|
||||
|
||||
cy.url().should('match', /^http:\/\/localhost:8080\/?\?code=[^&]+&state=$/);
|
||||
});
|
||||
|
||||
// TODO: enable, when backend api will return correct response on auth decline
|
||||
xit('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.getByTestId('auth-secondary-controls')
|
||||
.contains('Decline')
|
||||
.click();
|
||||
|
||||
cy.url().should('include', 'error=access_denied');
|
||||
});
|
||||
|
||||
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: account.id,
|
||||
})}`,
|
||||
);
|
||||
|
||||
cy.url().should('equal', 'https://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://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://ely.by/');
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prompts', () => {
|
||||
it('should prompt for account', () => {
|
||||
cy.login({ accounts: ['default'] });
|
||||
|
||||
cy.visit(
|
||||
`/oauth2/v1/ely?${new URLSearchParams({
|
||||
...defaults,
|
||||
prompt: 'select_account',
|
||||
})}`,
|
||||
);
|
||||
|
||||
cy.url().should('include', '/oauth/choose-account');
|
||||
|
||||
cy.getByTestId('auth-header').should('contain', 'Choose an account');
|
||||
});
|
||||
|
||||
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.getByTestId('auth-controls')
|
||||
.contains('Approve')
|
||||
.click();
|
||||
|
||||
cy.url().should('match', /^http:\/\/localhost:8080\/?\?code=[^&]+&state=$/);
|
||||
});
|
||||
|
||||
// TODO: enable, when backend api will return correct response on auth decline
|
||||
xit('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.getByTestId('auth-secondary-controls')
|
||||
.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.getByTestId('auth-header').should('contain', 'Choose an account');
|
||||
|
||||
cy.getByTestId('auth-body')
|
||||
.contains(account.email)
|
||||
.click();
|
||||
|
||||
assertPermissions();
|
||||
|
||||
cy.getByTestId('auth-controls')
|
||||
.contains('Approve')
|
||||
.click();
|
||||
|
||||
cy.url().should(
|
||||
'match',
|
||||
/^http:\/\/localhost:8080\/?\?code=[^&]+&state=$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('static pages', () => {
|
||||
it('should authenticate using static page', () => {
|
||||
cy.login({ accounts: ['default'] });
|
||||
|
||||
cy.visit(
|
||||
`/oauth2/v1/ely?${new URLSearchParams({
|
||||
...defaults,
|
||||
client_id: 'tlauncher',
|
||||
redirect_uri: 'static_page',
|
||||
})}`,
|
||||
);
|
||||
|
||||
cy.url().should('include', 'oauth/finish#{%22auth_code%22:');
|
||||
});
|
||||
|
||||
it('should authenticate using static page with code', () => {
|
||||
cy.login({ accounts: ['default'] });
|
||||
|
||||
cy.visit(
|
||||
`/oauth2/v1/ely?${new URLSearchParams({
|
||||
...defaults,
|
||||
client_id: 'tlauncher',
|
||||
redirect_uri: 'static_page_with_code',
|
||||
})}`,
|
||||
);
|
||||
|
||||
cy.url().should('include', 'oauth/finish#{%22auth_code%22:');
|
||||
|
||||
cy.getByTestId('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.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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
@@ -31,40 +31,52 @@ const accountsMap = {
|
||||
default2: account1,
|
||||
};
|
||||
|
||||
Cypress.Commands.add('login', async ({ account }) => {
|
||||
let credentials;
|
||||
Cypress.Commands.add('login', async ({ accounts }) => {
|
||||
const accountsData = await Promise.all(
|
||||
accounts.map(async account => {
|
||||
let credentials;
|
||||
|
||||
if (account) {
|
||||
credentials = accountsMap[account];
|
||||
if (account) {
|
||||
credentials = accountsMap[account];
|
||||
|
||||
if (!credentials) {
|
||||
throw new Error(`Unknown account name: ${account}`);
|
||||
}
|
||||
}
|
||||
if (!credentials) {
|
||||
throw new Error(`Unknown account name: ${account}`);
|
||||
}
|
||||
}
|
||||
|
||||
const resp = await fetch('/api/authentication/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
body: `login=${credentials.login}&password=${credentials.password}&rememberMe=1`,
|
||||
}).then(resp => resp.json());
|
||||
const resp = await fetch('/api/authentication/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
body: `${new URLSearchParams({
|
||||
login: credentials.login,
|
||||
password: credentials.password,
|
||||
rememberMe: '1',
|
||||
})}`,
|
||||
}).then(rawResp => rawResp.json());
|
||||
|
||||
const state = createState([
|
||||
{
|
||||
id: credentials.id,
|
||||
username: credentials.username,
|
||||
email: credentials.email,
|
||||
token: resp.access_token,
|
||||
refreshToken: resp.refresh_token,
|
||||
},
|
||||
]);
|
||||
return {
|
||||
id: credentials.id,
|
||||
username: credentials.username,
|
||||
email: credentials.email,
|
||||
token: resp.access_token,
|
||||
refreshToken: resp.refresh_token,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const state = createState(accountsData);
|
||||
|
||||
localStorage.setItem('redux-storage', JSON.stringify(state));
|
||||
|
||||
return state;
|
||||
return { accounts: accountsData };
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getByTestId', (id, options) =>
|
||||
cy.get(`[data-testid=${id}]`, options),
|
||||
);
|
||||
|
||||
function createState(accounts) {
|
||||
return {
|
||||
accounts: {
|
||||
|
19
tests-e2e/cypress/support/index.d.ts
vendored
19
tests-e2e/cypress/support/index.d.ts
vendored
@@ -1,5 +1,15 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
type AccountAlias = 'default' | 'default2';
|
||||
|
||||
interface Account {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
@@ -8,7 +18,12 @@ declare namespace Cypress {
|
||||
* @example cy.login(account)
|
||||
*/
|
||||
login(options: {
|
||||
account: 'default' | 'default2';
|
||||
}): Promise<{ [key: string]: any }>;
|
||||
accounts: AccountAlias[];
|
||||
}): Promise<{ accounts: Account[] }>;
|
||||
|
||||
getByTestId<S = any>(
|
||||
id: string,
|
||||
options?: Partial<Loggable & Timeoutable & Withinable>,
|
||||
): Chainable<S>;
|
||||
}
|
||||
}
|
||||
|
@@ -13,16 +13,9 @@
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
Cypress.on('window:before:load', win => {
|
||||
/**
|
||||
* define @fetch alias for asserting fetch requests
|
||||
* Example:
|
||||
* cy
|
||||
* .get('@fetch')
|
||||
* .should('be.calledWith', '/api/options');
|
||||
*/
|
||||
cy.spy(win, 'fetch').as('fetch');
|
||||
// remove fetch to enable correct api mocking with cypress xhr mocks
|
||||
win.fetch = null;
|
||||
});
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"moduleResolution": "node",
|
||||
"lib": ["es5", "dom"],
|
||||
"lib": ["es6", "dom"],
|
||||
"types": ["cypress"],
|
||||
"resolveJsonModule": true,
|
||||
"noImplicitAny": false
|
||||
|
Reference in New Issue
Block a user