From 9b8b16c89351ad270ad6c04943e797db90d8bd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Fr=C4=85k?= Date: Sat, 20 Jul 2024 16:22:25 +0200 Subject: [PATCH] test: Refactor E2E tests towards greater maintainability and robustness --- .github/workflows/maven.yml | 2 +- docker/docker-compose.yml | 1 + docker/e2e/cypress/e2e/accountDetailsPage.js | 37 ++ docker/e2e/cypress/e2e/forgotPasswordPage.js | 21 ++ docker/e2e/cypress/e2e/keycloak.js | 23 ++ docker/e2e/cypress/e2e/loginPage.js | 29 ++ docker/e2e/cypress/e2e/migrating_users.cy.js | 327 +++++------------- .../e2e/cypress/e2e/passwordPoliciesPage.js | 50 +++ .../e2e/cypress/e2e/realmSettingsEmailPage.js | 35 ++ .../e2e/cypress/e2e/realmSettingsLoginPage.js | 22 ++ docker/e2e/cypress/e2e/resetPasswordEmail.js | 29 ++ docker/e2e/cypress/e2e/resetPasswordPage.js | 15 + .../e2e/updateAccountInformationPage.js | 12 + docker/e2e/cypress/e2e/updatePasswordPage.js | 15 + docker/e2e/cypress/e2e/userDetailsPage.js | 30 ++ docker/e2e/cypress/e2e/userFederationPage.js | 58 ++++ .../cypress/e2e/userMigrationProviderPage.js | 36 ++ docker/e2e/cypress/e2e/usersPage.js | 73 ++++ 18 files changed, 571 insertions(+), 244 deletions(-) create mode 100644 docker/e2e/cypress/e2e/accountDetailsPage.js create mode 100644 docker/e2e/cypress/e2e/forgotPasswordPage.js create mode 100644 docker/e2e/cypress/e2e/keycloak.js create mode 100644 docker/e2e/cypress/e2e/loginPage.js create mode 100644 docker/e2e/cypress/e2e/passwordPoliciesPage.js create mode 100644 docker/e2e/cypress/e2e/realmSettingsEmailPage.js create mode 100644 docker/e2e/cypress/e2e/realmSettingsLoginPage.js create mode 100644 docker/e2e/cypress/e2e/resetPasswordEmail.js create mode 100644 docker/e2e/cypress/e2e/resetPasswordPage.js create mode 100644 docker/e2e/cypress/e2e/updateAccountInformationPage.js create mode 100644 docker/e2e/cypress/e2e/updatePasswordPage.js create mode 100644 docker/e2e/cypress/e2e/userDetailsPage.js create mode 100644 docker/e2e/cypress/e2e/userFederationPage.js create mode 100644 docker/e2e/cypress/e2e/userMigrationProviderPage.js create mode 100644 docker/e2e/cypress/e2e/usersPage.js diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 24d05f37..bf2cb61c 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -98,7 +98,7 @@ jobs: working-directory: docker/e2e # Video recording turned off due to free GitHub CI runners seemingly not being powerful enough for it. # Headed Chrome used to minimize issues with tests failing for no reason. - run: npx cypress run --headed --browser chrome --config video=false + run: npx cypress run --headed --browser electron --config video=false - name: Archive videos if: always() diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3b1d3ffb..eb7b5430 100755 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,6 +11,7 @@ services: environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin + KC_LOG_LEVEL: DEBUG DB_VENDOR: h2 ports: - "8024:8080" diff --git a/docker/e2e/cypress/e2e/accountDetailsPage.js b/docker/e2e/cypress/e2e/accountDetailsPage.js new file mode 100644 index 00000000..7f83a032 --- /dev/null +++ b/docker/e2e/cypress/e2e/accountDetailsPage.js @@ -0,0 +1,37 @@ +class AccountDetailsPage { + + /** + * @param {Keycloak} keycloak + */ + constructor(keycloak) { + this.keycloak = keycloak; + } + + elements = { + emailInput: () => cy.get('#email'), + firstNameInput: () => cy.get('#firstName'), + lastNameInput: () => cy.get('#lastName'), + saveBtn: () => cy.get('button').contains('Save') + } + + visit() { + cy.intercept('GET', '/realms/master/account/**') + .as("accountDetails"); + cy.visit('/realms/master/account'); + cy.wait('@accountDetails'); + } + + configurePersonalInfo(email, firstName, lastName) { + this.elements.emailInput().clear(); + this.elements.emailInput().type(email); + this.elements.firstNameInput().clear().type(firstName); + this.elements.lastNameInput().clear().type(lastName); + + cy.intercept('POST', '/realms/master/account/**') + .as("saveAccountDetails"); + this.elements.saveBtn().click(); + cy.wait("@saveAccountDetails"); + } +} + +module.exports = AccountDetailsPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/forgotPasswordPage.js b/docker/e2e/cypress/e2e/forgotPasswordPage.js new file mode 100644 index 00000000..e6300b10 --- /dev/null +++ b/docker/e2e/cypress/e2e/forgotPasswordPage.js @@ -0,0 +1,21 @@ +class ForgotPasswordPage { + elements = { + userNameInput: () => cy.get('#username'), + submitBtn: () => cy.get('input[type=submit]') + } + + visit() { + cy.visit('/realms/master/login-actions/reset-credentials'); + } + + triggerPasswordReset(userEmail) { + this.elements.userNameInput().clear().type(userEmail); + this.elements.submitBtn().click(); + cy.get('body').should('contain.text', + 'You should receive an email shortly with further instructions.'); + cy.mhGetMailsBySubject('Reset password') + .should('have.length', 1); + } +} + +module.exports = ForgotPasswordPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/keycloak.js b/docker/e2e/cypress/e2e/keycloak.js new file mode 100644 index 00000000..77824498 --- /dev/null +++ b/docker/e2e/cypress/e2e/keycloak.js @@ -0,0 +1,23 @@ +class Keycloak { + + elements = { + userDropdown: () => cy.get('#user-dropdown'), + notification: () => cy.get('.pf-c-alert__title'), + accountDropdown: () => cy.get('[data-testid="options"]') + } + + signOutViaUIAndClearCache() { + this.elements.userDropdown().click() + cy.get('#sign-out').get('a').contains('Sign out').click({force: true}); + + cy.clearAllCookies(); + cy.clearAllLocalStorage(); + cy.clearAllSessionStorage(); + } + + assertIsLoggedInAsUser(userFirstName, userLastName) { + this.elements.accountDropdown().should('contain', userFirstName + ' ' + userLastName); + } +} + +module.exports = Keycloak; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/loginPage.js b/docker/e2e/cypress/e2e/loginPage.js new file mode 100644 index 00000000..5f950143 --- /dev/null +++ b/docker/e2e/cypress/e2e/loginPage.js @@ -0,0 +1,29 @@ +class LoginPage { + + elements = { + usernameField: () => cy.get('#username'), + passwordField: () => cy.get('#password'), + logInBtn: () => cy.get('#kc-login'), + forgotPasswordBtn: () => cy.get("a").contains("Forgot Password?") + } + + visitForAdmin() { + cy.visit('/admin'); + } + + visitForUser() { + cy.visit('/realms/master/account'); + } + + logIn(login, password) { + this.elements.usernameField().type(login); + this.elements.passwordField().type(password); + + cy.intercept('POST', '/realms/master/login-actions/authenticate*') + .as("loginSubmit"); + this.elements.logInBtn().click(); + cy.wait("@loginSubmit"); + } +} + +module.exports = LoginPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/migrating_users.cy.js b/docker/e2e/cypress/e2e/migrating_users.cy.js index 9cca7b24..5f6a5160 100644 --- a/docker/e2e/cypress/e2e/migrating_users.cy.js +++ b/docker/e2e/cypress/e2e/migrating_users.cy.js @@ -5,7 +5,21 @@ // check out the link below and learn how to write your first test: // https://on.cypress.io/writing-first-test -const quotedPrintable = require('quoted-printable'); +const LoginPage = require("./loginPage"); +const Keycloak = require("./keycloak"); +const RealmSettingsLoginPage = require("./realmSettingsLoginPage"); +const UserMigrationProviderPage = require("./userMigrationProviderPage"); +const UserFederationPage = require("./userFederationPage"); +const UsersPage = require("./usersPage"); +const UserDetailsPage = require("./userDetailsPage"); +const AccountDetailsPage = require("./accountDetailsPage"); +const RealmSettingsEmailPage = require("./realmSettingsEmailPage"); +const PasswordPoliciesPage = require("./passwordPoliciesPage"); +const ResetPasswordPage = require("./resetPasswordPage"); +const ForgotPasswordPage = require("./forgotPasswordPage"); +const ResetPasswordEmail = require("./resetPasswordEmail"); +const UpdatePasswordPage = require("./updatePasswordPage"); +const UpdateAccountInformationPage = require("./updateAccountInformationPage"); const LEGACY_SYSTEM_URL = "http://legacy-system-example:8080/user-migration-support"; @@ -19,93 +33,55 @@ const ADMIN_EMAIL = 'admin@example.com'; const LEGACY_USER_USERNAME = "lucy"; const LEGACY_USER_PASSWORD = "password"; +const LEGACY_USER_PASSWORD_WHICH_MEETS_POLICY = "pa$$word"; const LEGACY_USER_EMAIL = 'lucy@example.com'; const LEGACY_USER_FIRST_NAME = 'Lucy'; const LEGACY_USER_LAST_NAME = 'Brennan'; -const USER_DETAILS_EMAIL_ID = '#email'; -const ADMIN_PERSONAL_INFO_EMAIL_ID = '#email'; -const ADMIN_PERSONAL_INFO_FIRST_NAME_ID = '#firstName'; -const ADMIN_PERSONAL_INFO_LAST_NAME_ID = '#lastName'; -const ACCOUNT_INFORMATION_EMAIL_ID = '#email'; -const ACCOUNT_INFORMATION_FIRST_NAME_ID = '#firstName'; -const ACCOUNT_INFORMATION_LAST_NAME_ID = '#lastName'; -const ACCOUNT_DROPDOWN_SELECTOR = '[data-testid="options"]'; -const RESET_PASSWORD_EMAIL_ID = '#username'; -const RESET_PASSWORD_NEW_PASSWORD_ID = '#password-new'; -const RESET_PASSWORD_CONFIRM_NEW_PASSWORD_ID = '#password-confirm'; +const PLUGIN_NAME = 'Custom REST Client Provider'; + +const keycloak = new Keycloak(); +const loginPage = new LoginPage(); +const realmSettingsLoginPage = new RealmSettingsLoginPage(); +const userFederationPage = new UserFederationPage(keycloak, PLUGIN_NAME); +const userMigrationProviderPage = new UserMigrationProviderPage(keycloak, userFederationPage, + PLUGIN_NAME); +const usersPage = new UsersPage(keycloak); +const userDetailsPage = new UserDetailsPage(usersPage); +const accountDetailsPage = new AccountDetailsPage(keycloak); +const realmSettingsEmailPage = new RealmSettingsEmailPage(keycloak); +const passwordPoliciesPage = new PasswordPoliciesPage(keycloak); +const forgotPasswordPage = new ForgotPasswordPage(); +const resetPasswordEmail = new ResetPasswordEmail(); +const resetPasswordPage = new ResetPasswordPage(); +const updatePasswordPage = new UpdatePasswordPage(); +const updateAccountInformationPage = new UpdateAccountInformationPage(); + describe('user migration plugin', () => { before(() => { signInAsAdmin(); configureLoginSettings(); - configureMigrationPlugin(); configureEmails(); - signOutViaUIAndClearCache(); + configureMigrationPlugin(); + keycloak.signOutViaUIAndClearCache(); }); function signInAsAdmin() { - cy.visit('/admin'); - submitCredentials(ADMIN_USERNAME, ADMIN_PASSWORD); - } - - function submitCredentials(user, password) { - cy.get('#username').type(user); - cy.get('#password').type(password); - cy.intercept('POST', 'http://localhost:8024/realms/master/login-actions/authenticate*') - .as("loginSubmit"); - cy.get('#kc-login').click(); - cy.wait("@loginSubmit"); - } - - function signOutViaUIAndClearCache() { - cy.get('#user-dropdown').click() - cy.get('#sign-out').get('a').contains('Sign out').click({force: true}); - cy.clearAllCookies(); - cy.clearAllLocalStorage(); - cy.clearAllSessionStorage(); + loginPage.visitForAdmin(); + loginPage.logIn(ADMIN_USERNAME, ADMIN_PASSWORD) } function configureLoginSettings() { - cy.visit('/admin/master/console/#/master/realm-settings/login'); - - cy.get('#kc-forgot-pw-switch').then(($checkbox) => { - if (!$checkbox.prop('checked')) { - cy.intercept('PUT', 'http://localhost:8024/admin/realms/master') - .as("saveForgotPassword"); - cy.wrap($checkbox).check({ force: true }); - cy.wait("@saveForgotPassword"); - - } - }); + realmSettingsLoginPage.visit(); + realmSettingsLoginPage.toggleForgotPasswordSwitchTo(true); } function configureMigrationPlugin() { - visitMigrationConfigPage(); - cy.get('#kc-ui-display-name') - .invoke('val', '') // clear() doesn't seem to work here for some reason - .type('RESTclientprovider'); - cy.get('#URI').clear().type(LEGACY_SYSTEM_URL); - cy.get('button').contains('Save').click() - cy.get('.pf-c-alert__title').should('contain', "User federation provider successfully"); - } - - /** - * Navigate to plugin config page. - * Edit existing plugin config or create new migration config. - */ - function visitMigrationConfigPage() { - cy.intercept('GET', '/admin/realms/master') - .as("realm"); - cy.visit('/admin/master/console/#/master/user-federation'); - cy.get("h1").should('contain', 'User federation'); - cy.wait("@realm"); - // Either add provider, or edit existing: - cy.get('*[data-testid="User migration using a REST client-card"], ' + - 'div[class="pf-l-gallery pf-m-gutter"] *[data-testid="keycloak-card-title"] a') - .first() - .click({force: true}); - cy.get("h1").should('contain', 'User migration using a REST client'); + userFederationPage.visit(); + userFederationPage.removePluginIfExists(); + userFederationPage.goToUserMigrationPluginPage(); + userMigrationProviderPage.addPlugin(LEGACY_SYSTEM_URL); } function configureEmails() { @@ -116,62 +92,29 @@ describe('user migration plugin', () => { /** * Write Admin user info first, so it becomes visible in account console. - * If fields are not populated here, the will not be visible in user account (KC Bug??) + * If fields are not populated here, they will not be visible in user account (KC Bug??) */ function writeAdminPersonalInfo() { - cy.visit('/admin/master/console/#/master/users'); - cy.url().should('include', '/admin/master/console/#/master/users'); - cy.get('*[placeholder="Search user"').clear().type('*'); - cy.get('.pf-c-input-group').get('.pf-c-button.pf-m-control').click(); - cy.get('table').get('td[data-label="Username"]').get('a').contains(ADMIN_USERNAME).click(); - cy.get(USER_DETAILS_EMAIL_ID).clear(); - cy.get(USER_DETAILS_EMAIL_ID).clear().type(ADMIN_EMAIL); - cy.get('*[data-testid="firstName"]').clear().type(ADMIN_USERNAME); - cy.get('*[data-testid="lastName"]').clear().type(ADMIN_USERNAME); - cy.get('.pf-c-form__actions').get('button[type="submit"]').contains('Save').click({force: true}); - + userDetailsPage.visit(ADMIN_USERNAME); + userDetailsPage.writePersonalInfo(ADMIN_EMAIL, ADMIN_USERNAME); } function configureAdminPersonalInfo() { - cy.intercept('GET', '/realms/master/account/**') - .as("accountDetails"); - cy.visit('/realms/master/account'); - cy.wait('@accountDetails'); - - // Wait a while, otherwise Keycloak overrides the inputs randomly - cy.wait(2000); - cy.get(ADMIN_PERSONAL_INFO_EMAIL_ID).clear(); - // Wait for email to be cleared - cy.wait(2000); - cy.get(ADMIN_PERSONAL_INFO_EMAIL_ID).clear().type(ADMIN_EMAIL); - cy.get(ADMIN_PERSONAL_INFO_FIRST_NAME_ID).clear().type(ADMIN_USERNAME); - cy.get(ADMIN_PERSONAL_INFO_LAST_NAME_ID).clear().type(ADMIN_USERNAME); - - cy.get('button').contains('Save').click(); - - cy.get('.pf-c-alert__title').should('contain', "Your account has been updated"); + accountDetailsPage.visit(); + accountDetailsPage.configurePersonalInfo(ADMIN_EMAIL, ADMIN_USERNAME, ADMIN_USERNAME); } function configureSmtpSettings() { - cy.visit('/admin/master/console/#/master/realm-settings/email'); - - cy.get('#kc-host').clear().type(SMTP_HOST); - cy.get('#kc-port').clear().type(SMTP_PORT); - cy.get('#kc-sender-email-address').clear().type(SMTP_FROM); - - cy.get('button').contains('Test connection').click(); - cy.get('.pf-c-alert__title').should('contain', "Success! SMTP connection successful. E-mail was sent!"); - - cy.get('button').contains('Save').click(); + realmSettingsEmailPage.visit(); + realmSettingsEmailPage.configureSmtpSettings(SMTP_HOST, SMTP_PORT, SMTP_FROM); } beforeEach(() => { deleteEmails(); signInAsAdmin(); - deleteTestUserIfExists().then(() => { - deletePasswordPoliciesIfExist() - .then(() => signOutViaUIAndClearCache()); - }); + return deleteTestUserIfExists() + .then(() => deletePasswordPoliciesIfExist() + .then(() => keycloak.signOutViaUIAndClearCache())); }); function deleteEmails() { @@ -181,94 +124,30 @@ describe('user migration plugin', () => { } function deleteTestUserIfExists() { - cy.visit('/admin/master/console/#/master/users'); - cy.get('.pf-c-input-group').get('input').clear(); - cy.get('.pf-c-input-group').get('input').type('*'); - cy.get('.pf-c-input-group').get('.pf-c-button.pf-m-control').click(); - - let userButton = cy.get('table').get('td[data-label="Username"]').get('a').contains(LEGACY_USER_USERNAME); - return userButton - .should('have.length.gte', 0).then(userElement => { - if (!userElement.length) { - return; - } - userButton.click(); - cy.get('div[data-testid="action-dropdown"]').click(); - cy.get('.pf-c-dropdown__menu-item').contains('Delete').click(); - cy.get('#modal-confirm').click({force: true}); - }); + usersPage.visit(); + return usersPage.deleteUserIfExists(LEGACY_USER_USERNAME); } function deletePasswordPoliciesIfExist() { - goToPasswordPoliciesPage(); - return deleteEveryPasswordPolicyAndSave(); - } - - function deleteEveryPasswordPolicyAndSave() { - // Will not find policies if not waiting here. - cy.wait(2000); - return cy.get('body') - .then($body => { - if ($body.find('.keycloak__policies_authentication__form').length) { - cy.log("Deleting password policies..."); - let deleteButtons = cy.get('#pf-tab-section-1-passwordPolicy').get('.keycloak__policies_authentication__minus-icon'); - return deleteButtons - .should('have.length.gte', 0).then(btn => { - - if (!btn.length) { - return; - } - cy.wrap(btn).click({multiple: true}); - cy.get('button[data-testid="save"]').contains('Save').click(); - cy.get('.pf-c-alert__title').should('contain', "Password policies successfully updated"); - }); - } else { - return 'OK'; - } - }); - } - - function goToPasswordPoliciesPage() { - // Can't get cypress to navigate to the policies page unless adding a "wait" here. - cy.wait(1000); - cy.intercept('GET', '/admin/realms/master/authentication/required-actions').as("masterGet"); - cy.visit('/admin/master/console/#/master/authentication/policies'); - cy.wait('@masterGet'); - cy.get("h1").should('contain', 'Authentication'); + passwordPoliciesPage.visit(); + return passwordPoliciesPage.deleteEveryPasswordPolicy(); } it('should migrate user', () => { signInAsLegacyUser(); + updateAccountInformationPage.confirmAccountInformation(); updateAccountInformation(); - assertIsLoggedInAsLegacyUser(); + keycloak.assertIsLoggedInAsUser(LEGACY_USER_FIRST_NAME, LEGACY_USER_LAST_NAME); }); - function clickSignBtnInIfExists() { - cy.get('body').then((body) => { - const element = "#landingSignInButton"; - if (body.find(element).length > 0) { - // Only click if exists - cy.get(element).click(); - } - }); - } - function signInAsLegacyUser() { - cy.visit('/realms/master/account'); - clickSignBtnInIfExists(); - submitCredentials(LEGACY_USER_USERNAME, LEGACY_USER_PASSWORD); + loginPage.visitForUser(); + loginPage.logIn(LEGACY_USER_USERNAME, LEGACY_USER_PASSWORD); } function updateAccountInformation() { - cy.get(ACCOUNT_INFORMATION_EMAIL_ID).should('have.value', LEGACY_USER_EMAIL); - cy.get(ACCOUNT_INFORMATION_FIRST_NAME_ID).should('have.value', LEGACY_USER_FIRST_NAME); - cy.get(ACCOUNT_INFORMATION_LAST_NAME_ID).should('have.value', LEGACY_USER_LAST_NAME); - cy.get("input").contains("Submit").click(); - } - - function assertIsLoggedInAsLegacyUser() { - cy.get(ACCOUNT_DROPDOWN_SELECTOR) - .should('contain', LEGACY_USER_FIRST_NAME + ' ' + LEGACY_USER_LAST_NAME); + accountDetailsPage.visit(); + accountDetailsPage.configurePersonalInfo(LEGACY_USER_EMAIL, LEGACY_USER_FIRST_NAME, LEGACY_USER_LAST_NAME); } it('should reset password after inputting wrong credentials', () => { @@ -277,58 +156,29 @@ describe('user migration plugin', () => { resetPasswordViaEmail(); }); - function resetPasswordViaEmail() { - cy.mhGetMailsBySubject('Reset password').mhFirst().mhGetBody() - .then(bodyQuotedPrintable => { - clickPasswordResetLink(bodyQuotedPrintable); - updateAccountInformation(); - inputNewPassword(); - assertIsLoggedInAsLegacyUser(); - }); - } - function attemptLoginWithWrongPassword() { - cy.visit('/realms/master/account'); - clickSignBtnInIfExists(); - submitCredentials(LEGACY_USER_USERNAME, "wrongPassword"); + loginPage.visitForUser(); + loginPage.logIn(LEGACY_USER_USERNAME, "wrongPassword"); } function triggerPasswordReset() { - cy.intercept('GET', '/realms/master/login-actions/reset-credentials*') - .as('resetCredentials'); - cy.get("a").contains("Forgot Password?").click(); - cy.wait('@resetCredentials'); - cy.get(RESET_PASSWORD_EMAIL_ID).clear().type(LEGACY_USER_EMAIL); - cy.get('input[type=submit]').click(); - cy.get('body').should('contain.text', - 'You should receive an email shortly with further instructions.'); - cy.mhGetMailsBySubject('Reset password') - .should('have.length', 1); - } - - function clickPasswordResetLink(bodyQuotedPrintable) { - const body = quotedPrintable.decode(bodyQuotedPrintable); - const resetPassUrl = getUrlFromLink(body, 'Link to reset credentials'); - - cy.visit(resetPassUrl); - } - - function getUrlFromLink(body, linkText) { - const linkPattern = new RegExp(''); - return linkPattern.exec(body)[1] - .toString() - .replace(/(\r\n|\n|\r)/gm, ""); + forgotPasswordPage.visit(); + forgotPasswordPage.triggerPasswordReset(LEGACY_USER_EMAIL); } - function inputNewPassword() { - cy.get(RESET_PASSWORD_NEW_PASSWORD_ID).type(LEGACY_USER_PASSWORD); - cy.get(RESET_PASSWORD_CONFIRM_NEW_PASSWORD_ID).type(LEGACY_USER_PASSWORD); - cy.get('input[type=submit]').click(); + function resetPasswordViaEmail() { + resetPasswordEmail.getResetPasswordUrl() + .then(resetPassUrl => { + cy.visit(resetPassUrl); + updateAccountInformationPage.confirmAccountInformation(); + resetPasswordPage.chooseNewPassword(LEGACY_USER_PASSWORD) + keycloak.assertIsLoggedInAsUser(LEGACY_USER_FIRST_NAME, LEGACY_USER_LAST_NAME); + }); } it('should reset password before user is migrated', () => { cy.visit('/realms/master/account'); - clickSignBtnInIfExists(); + // clickSignBtnInIfExists(); triggerPasswordReset(); resetPasswordViaEmail() }); @@ -336,25 +186,16 @@ describe('user migration plugin', () => { it('should migrate user when password breaks policy', () => { signInAsAdmin(); addSpecialCharactersPasswordPolicy(); - signOutViaUIAndClearCache(); + keycloak.signOutViaUIAndClearCache(); signInAsLegacyUser(); - provideNewPassword(); - updateAccountInformation(); - assertIsLoggedInAsLegacyUser(); + updatePasswordPage.chooseNewPassword(LEGACY_USER_PASSWORD_WHICH_MEETS_POLICY); + updateAccountInformationPage.confirmAccountInformation(); + keycloak.assertIsLoggedInAsUser(LEGACY_USER_FIRST_NAME, LEGACY_USER_LAST_NAME); }); function addSpecialCharactersPasswordPolicy() { - cy.visit('/admin/master/console/#/master/authentication/policies'); - cy.get('.pf-c-select__toggle').click() - cy.get('button[role="option"]').contains('Special Characters').click(); - cy.get('button[data-testid="save"]').contains('Save').click(); - cy.get('.pf-c-alert__title').should('contain', "Password policies successfully updated"); - } - - function provideNewPassword() { - cy.get('#password-new').type("pa$$word"); - cy.get('#password-confirm').type("pa$$word"); - cy.get("input").contains("Submit").click(); + passwordPoliciesPage.visit(); + passwordPoliciesPage.addSpecialCharactersPasswordPolicy(); } }); diff --git a/docker/e2e/cypress/e2e/passwordPoliciesPage.js b/docker/e2e/cypress/e2e/passwordPoliciesPage.js new file mode 100644 index 00000000..f7898030 --- /dev/null +++ b/docker/e2e/cypress/e2e/passwordPoliciesPage.js @@ -0,0 +1,50 @@ +const REMOVE_PASSWORD_POLICY_BTN_SELECTOR = '[data-testid^=remove-]'; +const NO_POLICY_ICON_SELECTOR = '.pf-c-empty-state__icon'; + +class PasswordPoliciesPage { + + /** + * @param {Keycloak} keycloak + */ + constructor(keycloak) { + this.keycloak = keycloak; + } + + elements = { + header: () => cy.get('h1'), + removePasswordPolicyBtn: () => cy.get(REMOVE_PASSWORD_POLICY_BTN_SELECTOR), + saveBtn: () => cy.get('button[data-testid="save"]'), + addPolicyBtn: () => cy.get('.pf-c-select__toggle'), + addSpecialCharactersPolicyBtn: () => cy.get('button[role="option"]') + .contains('Special Characters') + } + + visit() { + cy.visit('/admin/master/console/#/master/authentication/policies'); + cy.get(REMOVE_PASSWORD_POLICY_BTN_SELECTOR + "," + NO_POLICY_ICON_SELECTOR) + .should('be.visible'); + } + + deleteEveryPasswordPolicy() { + return cy.document().then((doc) => { + if (doc.querySelectorAll(REMOVE_PASSWORD_POLICY_BTN_SELECTOR).length) { + cy.log("Deleting password policies..."); + this.elements.removePasswordPolicyBtn().click({multiple: true}); + this.elements.saveBtn().contains('Save').click(); + this.keycloak.elements.notification().should('contain', + "Password policies successfully updated"); + } else { + cy.log("No password policies to remove.") + } + }); + } + + addSpecialCharactersPasswordPolicy() { + this.elements.addPolicyBtn().click() + this.elements.addSpecialCharactersPolicyBtn().click(); + this.elements.saveBtn().click(); + this.keycloak.elements.notification().should('contain', "Password policies successfully updated"); + } +} + +module.exports = PasswordPoliciesPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/realmSettingsEmailPage.js b/docker/e2e/cypress/e2e/realmSettingsEmailPage.js new file mode 100644 index 00000000..52e02642 --- /dev/null +++ b/docker/e2e/cypress/e2e/realmSettingsEmailPage.js @@ -0,0 +1,35 @@ +class RealmSettingsEmailPage { + + /** + * @param {Keycloak} keycloak + */ + constructor(keycloak) { + this.keycloak = keycloak; + } + + elements = { + smtpHostInput: () => cy.get('#kc-host'), + smtpPortInput: () => cy.get('#kc-port'), + emailFromInput: () => cy.get('#kc-sender-email-address'), + testConnectionBtn: () => cy.get('button').contains('Test connection'), + saveBtn: () => cy.get('button').contains('Save') + } + + visit = () => { + cy.visit('/admin/master/console/#/master/realm-settings/email'); + } + + configureSmtpSettings(smtpHost, smtpPort, smtpFrom) { + this.elements.smtpHostInput().clear().type(smtpHost); + this.elements.smtpPortInput().clear().type(smtpPort); + this.elements.emailFromInput().clear().type(smtpFrom); + + this.elements.testConnectionBtn().click(); + this.keycloak.elements.notification() + .should('contain', "Success! SMTP connection successful. E-mail was sent!"); + + this.elements.saveBtn().click(); + } +} + +module.exports = RealmSettingsEmailPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/realmSettingsLoginPage.js b/docker/e2e/cypress/e2e/realmSettingsLoginPage.js new file mode 100644 index 00000000..d8afb368 --- /dev/null +++ b/docker/e2e/cypress/e2e/realmSettingsLoginPage.js @@ -0,0 +1,22 @@ +class RealmSettingsLoginPage { + elements = { + forgotPasswordSwitch: () => cy.get('#kc-forgot-pw-switch') + } + + visit = () => { + cy.visit('/admin/master/console/#/master/realm-settings/login'); + } + + toggleForgotPasswordSwitchTo(state) { + this.elements.forgotPasswordSwitch().then(($checkbox) => { + if (!$checkbox.prop('checked')) { + cy.intercept('PUT', 'http://localhost:8024/admin/realms/master') + .as("saveForgotPassword"); + cy.wrap($checkbox).check({ force: state }); + cy.wait("@saveForgotPassword"); + } + }); + } +} + +module.exports = RealmSettingsLoginPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/resetPasswordEmail.js b/docker/e2e/cypress/e2e/resetPasswordEmail.js new file mode 100644 index 00000000..27d58618 --- /dev/null +++ b/docker/e2e/cypress/e2e/resetPasswordEmail.js @@ -0,0 +1,29 @@ +const quotedPrintable = require("quoted-printable"); +const RESET_PASSWORD_EMAIL_SUBJECT = 'Reset password'; + +class ResetPasswordEmail { + + /** + * @returns {Cypress.Chainable} + */ + open() { + return cy.mhGetMailsBySubject(RESET_PASSWORD_EMAIL_SUBJECT).mhFirst().mhGetBody(); + } + + getResetPasswordUrl() { + return this.open() + .then(bodyQuotedPrintable => { + const body = quotedPrintable.decode(bodyQuotedPrintable); + return this.getUrlFromLink(body, 'Link to reset credentials'); + }); + } + + getUrlFromLink(body, linkText) { + const linkPattern = new RegExp(''); + return linkPattern.exec(body)[1] + .toString() + .replace(/(\r\n|\n|\r)/gm, ""); + } +} + +module.exports = ResetPasswordEmail; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/resetPasswordPage.js b/docker/e2e/cypress/e2e/resetPasswordPage.js new file mode 100644 index 00000000..4f76357e --- /dev/null +++ b/docker/e2e/cypress/e2e/resetPasswordPage.js @@ -0,0 +1,15 @@ +class ResetPasswordPage { + elements = { + passwordInput: () => cy.get('#password-new'), + confirmPasswordInput: () => cy.get('#password-confirm'), + submitBtn: () => cy.get('input[type=submit]') + } + + chooseNewPassword(newPassword) { + this.elements.passwordInput().type(newPassword); + this.elements.confirmPasswordInput().type(newPassword); + this.elements.submitBtn().click(); + } +} + +module.exports = ResetPasswordPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/updateAccountInformationPage.js b/docker/e2e/cypress/e2e/updateAccountInformationPage.js new file mode 100644 index 00000000..e29336b5 --- /dev/null +++ b/docker/e2e/cypress/e2e/updateAccountInformationPage.js @@ -0,0 +1,12 @@ +class UpdateAccountInformationPage { + + elements = { + submitBtn: () => cy.get('input[type=submit]') + } + + confirmAccountInformation() { + this.elements.submitBtn().click(); + } +} + +module.exports = UpdateAccountInformationPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/updatePasswordPage.js b/docker/e2e/cypress/e2e/updatePasswordPage.js new file mode 100644 index 00000000..31e9cd11 --- /dev/null +++ b/docker/e2e/cypress/e2e/updatePasswordPage.js @@ -0,0 +1,15 @@ +class UpdatePasswordPage { + elements = { + passwordInput: () => cy.get('#password-new'), + confirmPasswordInput: () => cy.get('#password-confirm'), + submitBtn: () => cy.get('input[type=submit]') + } + + chooseNewPassword(newPassword) { + this.elements.passwordInput().type(newPassword); + this.elements.confirmPasswordInput().type(newPassword); + this.elements.submitBtn().click(); + } +} + +module.exports = UpdatePasswordPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/userDetailsPage.js b/docker/e2e/cypress/e2e/userDetailsPage.js new file mode 100644 index 00000000..ee657e55 --- /dev/null +++ b/docker/e2e/cypress/e2e/userDetailsPage.js @@ -0,0 +1,30 @@ +class UserDetailsPage { + + /** + * @param {UsersPage} usersPage + */ + constructor(usersPage) { + this.usersPage = usersPage; + } + + elements = { + emailInput: () => cy.get('#email'), + firstNameInput: () => cy.get('*[data-testid="firstName"]'), + lastNameInput: () => cy.get('*[data-testid="lastName"]') + } + + visit(userName) { + this.usersPage.visit(); + this.usersPage.goToUserDetails(userName) + } + + writePersonalInfo(email, username) { + this.elements.emailInput().clear(); + this.elements.emailInput().clear().type(email); + this.elements.firstNameInput().clear().type(username); + this.elements.lastNameInput().clear().type(username); + cy.get("form").submit(); + } +} + +module.exports = UserDetailsPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/userFederationPage.js b/docker/e2e/cypress/e2e/userFederationPage.js new file mode 100644 index 00000000..869adc49 --- /dev/null +++ b/docker/e2e/cypress/e2e/userFederationPage.js @@ -0,0 +1,58 @@ +class UserFederationPage { + + /** + * @param {Keycloak} keycloak + * @param {String} pluginName + */ + constructor(keycloak, pluginName) { + this.keycloak = keycloak; + this.pluginName = pluginName; + } + + elements = { + header: () => cy.get("h1"), + userMigrationProviderBtn: () => + cy.get('*[data-testid="User migration using a REST client-card"], ' + + 'div[class="pf-l-gallery pf-m-gutter"] *[data-testid="keycloak-card-title"] a'), + pluginDropdown: () => cy.get('[data-testid="' + this.pluginName + '-dropdown"]') + } + + visit() { + cy.intercept('/admin/realms/master/components*') + .as("components") + cy.visit('/admin/master/console/#/master/user-federation'); + cy.wait('@components'); + cy.wait(2000); // The initial page will always claim there are no components, so we must wait to make sure. + } + + removePluginIfExists() { + this.elements.pluginDropdown() + .should('have.length.gte', 0).then(userElement => { + if (!userElement.length) { + return; + } + this.elements.pluginDropdown().click(); + cy.get('.pf-c-dropdown__menu-item').contains("Delete").click(); + + cy.intercept('DELETE', 'admin/realms/master/components/*') + .as("deleteComponent"); + cy.get('#modal-confirm').click({force: true}); + cy.wait("@deleteComponent"); + this.keycloak.elements.notification() + .should('contain', 'The user federation provider has been deleted'); + this.removePluginIfExists(); + }); + } + + goToUserMigrationPluginPage() { + cy.intercept('/admin/realms/master/components*') + .as("components") + this.elements.userMigrationProviderBtn() + .first() + .click({force: true}); + cy.wait("@components"); + this.elements.header().should('contain', 'User migration using a REST client'); + } +} + +module.exports = UserFederationPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/userMigrationProviderPage.js b/docker/e2e/cypress/e2e/userMigrationProviderPage.js new file mode 100644 index 00000000..cd474cc8 --- /dev/null +++ b/docker/e2e/cypress/e2e/userMigrationProviderPage.js @@ -0,0 +1,36 @@ +class UserMigrationProviderPage { + + /** + * @param {Keycloak} keycloak + * @param {UserFederationPage} userFederationPage + * @param {String} pluginName + */ + constructor(keycloak, userFederationPage, pluginName) { + this.keycloak = keycloak; + this.userFederationPage = userFederationPage; + this.pluginName = pluginName; + } + + elements = { + header: () => cy.get("h1"), + uiDisplayName: () => cy.get('#kc-ui-display-name'), + restClientUri: () => cy.get('#URI'), + saveBtn: () => cy.get('button').contains('Save') + } + + visit() { + this.userFederationPage.visit(); + this.userFederationPage.goToUserMigrationPluginPage(); + } + + addPlugin(legacySystemUrl) { + this.elements.uiDisplayName() + .invoke('val', '') // clear() doesn't seem to work here for some reason + .type(this.pluginName); + this.elements.restClientUri().clear().type(legacySystemUrl); + this.elements.saveBtn().click() + this.keycloak.elements.notification().should('contain', "User federation provider successfully"); + } +} + +module.exports = UserMigrationProviderPage; \ No newline at end of file diff --git a/docker/e2e/cypress/e2e/usersPage.js b/docker/e2e/cypress/e2e/usersPage.js new file mode 100644 index 00000000..452bd544 --- /dev/null +++ b/docker/e2e/cypress/e2e/usersPage.js @@ -0,0 +1,73 @@ +const USER_ROW_SELECTOR = 'table td[data-label="Username"] a'; + +class UsersPage { + + /** + * @param {Keycloak} keycloak + */ + constructor(keycloak) { + this.keycloak = keycloak; + } + + elements = { + searchUserInput: () => cy.get('input[placeholder="Search user"'), + foundUserBtn: () => cy.get(USER_ROW_SELECTOR), + } + + visit() { + cy.intercept('/admin/realms/master/components*') + .as("components") + cy.visit('/admin/master/console/#/master/users'); + cy.wait('@components'); + } + + goToUserDetails(userName) { + this.findByName(userName) + .then(id => cy.visit("admin/master/console/#/master/users/" + id + "/settings")); + } + + findByName(userName) { + this.elements.searchUserInput().clear({force: true}); + this.elements.searchUserInput().type(userName); + cy.intercept("/admin/realms/master/ui-ext/brute-force-user*") + .as("findUsers"); + this.elements.searchUserInput().type("{enter}"); + return cy.wait("@findUsers") + .then(response => response.response?.body[0]?.id); + + } + + deleteUserIfExists(userName) { + this.findByName(userName); + + return cy.get(USER_ROW_SELECTOR).should('have.length.gte', 0).then(element => { + if (element.length < 1) { + return; + } + cy.log(`Found ${element.length} items`); + this.clickDeleteInUserDropdown(); + this.confirmUserDeletion(); + return this.assertUserWasDeleted(userName); + }); + } + + clickDeleteInUserDropdown() { + cy.get('table .pf-c-dropdown__toggle').click(); + cy.get('.pf-c-dropdown__menu-item').contains('Delete').click(); + } + + confirmUserDeletion() { + cy.intercept('DELETE', '/admin/realms/master/users/*') + .as("deleteUser"); + cy.get('#modal-confirm').click(); + cy.wait("@deleteUser"); + this.keycloak.elements.notification().should('contain', 'The user has been deleted'); + } + + assertUserWasDeleted(userName) { + this.findByName(userName); + return this.elements.foundUserBtn().should('not.exist'); + } +} + +module.exports = UsersPage; \ No newline at end of file