From 6fadc0c041ac9e8ca5f04fc98a0d16bb48865ff5 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Wed, 9 Oct 2024 03:03:08 -0600 Subject: [PATCH] [MM-47312] Migrate /enterprise/saml e2e tests to TypeScript (#28367) --- ...{okta_login_spec.js => okta_login_spec.ts} | 3 +- ...tomated_spec.js => saml_automated_spec.ts} | 3 +- ...mber_spec.js => saml_guest_member_spec.ts} | 4 +- ...metadata_spec.js => saml_metadata_spec.ts} | 4 +- .../tests/support/keycloak_commands.d.ts | 179 -------- .../tests/support/keycloak_commands.js | 236 ---------- .../tests/support/keycloak_commands.ts | 425 ++++++++++++++++++ .../cypress/tests/support/okta_commands.ts | 10 +- .../cypress/tests/support/saml_commands.ts | 10 +- 9 files changed, 444 insertions(+), 430 deletions(-) rename e2e-tests/cypress/tests/integration/channels/enterprise/saml/{okta_login_spec.js => okta_login_spec.ts} (99%) rename e2e-tests/cypress/tests/integration/channels/enterprise/saml/{saml_automated_spec.js => saml_automated_spec.ts} (98%) rename e2e-tests/cypress/tests/integration/channels/enterprise/saml/{saml_guest_member_spec.js => saml_guest_member_spec.ts} (98%) rename e2e-tests/cypress/tests/integration/channels/enterprise/saml/{saml_metadata_spec.js => saml_metadata_spec.ts} (97%) delete mode 100644 e2e-tests/cypress/tests/support/keycloak_commands.d.ts delete mode 100644 e2e-tests/cypress/tests/support/keycloak_commands.js create mode 100644 e2e-tests/cypress/tests/support/keycloak_commands.ts diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/saml/okta_login_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/saml/okta_login_spec.ts similarity index 99% rename from e2e-tests/cypress/tests/integration/channels/enterprise/saml/okta_login_spec.js rename to e2e-tests/cypress/tests/integration/channels/enterprise/saml/okta_login_spec.ts index c37fabb6e7..a409b34a15 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/saml/okta_login_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/saml/okta_login_spec.ts @@ -11,6 +11,7 @@ // Group: @channels @enterprise @saml // Skip: @headless @electron @firefox // run on Chrome (headed) only +import {UserCollection} from 'tests/support/okta_commands'; import users from '../../../../fixtures/saml_users.json'; //Manual Setup required: Follow the instructions mentioned in the mattermost/platform-private/config/saml-okta-setup.txt file @@ -92,7 +93,7 @@ context('Okta', () => { // # Check SAML metadata if working properly cy.apiGetMetadataFromIdp(idpMetadataUrl); - cy.oktaAddUsers(users); + cy.oktaAddUsers(users as unknown as UserCollection); cy.apiUpdateConfig(newConfig).then(({config}) => { cy.setTestSettings(loginButtonText, config).then((_response) => { testSettings = _response; diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_automated_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_automated_spec.ts similarity index 98% rename from e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_automated_spec.js rename to e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_automated_spec.ts index 70d8950c5e..ebac0b3f21 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_automated_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_automated_spec.ts @@ -11,6 +11,7 @@ // Group: @channels @enterprise @saml // Skip: @headless @electron @firefox // run on Chrome (headed) only +import {UserCollection} from 'tests/support/okta_commands'; import users from '../../../../fixtures/saml_users.json'; //Manual Setup required: Follow the instructions mentioned in the mattermost/platform-private/config/saml-okta-setup.txt file @@ -88,7 +89,7 @@ context('LDAP SAML - Automated Tests (SAML TESTS)', () => { // # Check SAML metadata if working properly cy.apiGetMetadataFromIdp(idpMetadataUrl); - cy.oktaAddUsers(users); + cy.oktaAddUsers(users as unknown as UserCollection); cy.apiUpdateConfig(newConfig).then(({config}) => { cy.setTestSettings(loginButtonText, config).then((_response) => { testSettings = _response; diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_guest_member_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_guest_member_spec.ts similarity index 98% rename from e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_guest_member_spec.js rename to e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_guest_member_spec.ts index 05aaf967c1..c4b5b1e0b1 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_guest_member_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_guest_member_spec.ts @@ -82,7 +82,7 @@ describe('SAML Guest', () => { cy.apiUpdateConfig(newConfig).then(({config}) => { cy.setTestSettings(loginButtonText, config).then((_response) => { testSettings = _response; - cy.keycloakResetUsers({guestUser}); + cy.keycloakResetUsers([guestUser]); }); }); }); @@ -147,7 +147,7 @@ describe('SAML Guest', () => { const testConfig = { ...newConfig, GuestAccountsSettings: { - ...newConfig.GuestAccountSettings, + ...newConfig.GuestAccountsSettings, Enable: true, }, SamlSettings: { diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_metadata_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_metadata_spec.ts similarity index 97% rename from e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_metadata_spec.js rename to e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_metadata_spec.ts index e0053d5309..324a21e359 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_metadata_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/saml/saml_metadata_spec.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {AdminConfig} from '@mattermost/types/config'; + // *************************************************************** // - [#] indicates a test step (e.g. # Go to a page) // - [*] indicates an assertion (e.g. * Check the title) @@ -18,7 +20,7 @@ const testIdpURL = 'http://test_idp_url'; const testIdpDescriptorURL = 'http://test_idp_descriptor_url'; const getSamlMetadataErrorMessage = 'SAML Metadata URL did not connect and pull data successfully'; -let config; +let config: AdminConfig; describe('SystemConsole->SAML 2.0 - Get Metadata from Idp Flow', () => { before(() => { diff --git a/e2e-tests/cypress/tests/support/keycloak_commands.d.ts b/e2e-tests/cypress/tests/support/keycloak_commands.d.ts deleted file mode 100644 index 34c485ba08..0000000000 --- a/e2e-tests/cypress/tests/support/keycloak_commands.d.ts +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/// - -// *************************************************************** -// Each command should be properly documented using JSDoc. -// See https://jsdoc.app/index.html for reference. -// Basic requirements for documentation are the following: -// - Meaningful description -// - Each parameter with `@params` -// - Return value with `@returns` -// - Example usage with `@example` -// Custom command should follow naming convention of having `keycloak` prefix, e.g. `keycloakActivateUser`. -// *************************************************************** - -declare namespace Cypress { - interface Chainable { - - /** - * keycloakGetAccessTokenAPI is a task wrapped as command with post-verification - * that an Access Token is successfully retrieved - * @returns {string} - access token - */ - keycloakGetAccessTokenAPI(): Chainable; - - /** - * keycloakCreateUserAPI is a task wrapped as command with post-verification - * that a user is successfully created in keycloak - * @param {string} accessToken - a valid access token - * @param {object} user - a keycloak user object to create - * - * @example - * cy.keycloakCreateUserAPI('abcde', {firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true,}); - */ - keycloakCreateUserAPI(accessToken: string, user: any): Chainable; - - /** - * keycloakResetPasswordAPI is a task wrapped as command with post-verification - * that a user password is successfully reset in keycloak - * @param {string} accessToken - a valid access token - * @param {string} userId - a keycloak userId - * @param {string} password - new password to set - * - * @example - * cy.keycloakResetPasswordAPI('abcde', '12345', 'password'); - */ - keycloakResetPasswordAPI(accessToken: string, userId: string, password: string): Chainable; - - /** - * keycloakGetUserAPI is a task wrapped as command with post-verification - * that a user is successfully found in keycloak - * @param {string} accessToken - a valid access token - * @param {string} email - an email to query - * @returns {string} - keycloak userId if found - * - * @example - * cy.keycloakGetUserAPI('abcde', 'test@mm.com'); - */ - keycloakGetUserAPI(accessToken: string, email: string): Chainable; - - /** - * keycloakDeleteUserAPI is a task wrapped as command with post-verification - * that a user is successfully deleted in keycloak - * @param {string} accessToken - a valid access token - * @param {string} userId - keycloak user id to delete - * - * @example - * cy.keycloakDeleteUserAPI('abcde', '12345'); - */ - keycloakDeleteUserAPI(accessToken: string, userId: string): Chainable; - - /** - * keycloakUpdateUserAPI is a task wrapped as command with post-verification - * that a user is successfully updated in keycloak - * @param {string} accessToken - a valid access token - * @param {string} userId - keycloak user id to delete - * @param {object} data - keycloak user object - * - * @example - * cy.keycloakUpdateUserAPI('abcde', '12345', {'enabled': false}}); - */ - keycloakUpdateUserAPI(accessToken: string, userId: string, data: any): Chainable; - - /** - * keycloakDeleteSessionAPI is a task wrapped as command with post-verification - * that a users session is successfully deleted in keycloak - * @param {string} accessToken - a valid access token - * @param {string} sessionId- keycloak session id to delete - * - * @example - * cy.keycloakDeleteSessionAPI('abcde', '12345'); - */ - keycloakDeleteSessionAPI(accessToken: string, sessionId: string): Chainable; - - /** - * keycloakGetUserSessionsAPI is a task wrapped as command with post-verification - * that a users sessions are successfully found - * @param {string} accessToken - a valid access token - * @param {string} userId - keycloak user id to find sessions - * @returns {string[]} - array of keycloak session ids - * - * @example - * cy.keycloakGetUserSessionsAPI('abcde', '12345'); - */ - keycloakGetUserSessionsAPI(accessToken: string, userId: string): Chainable ; - - /** - * keycloakDeleteUserSessions is a command that finds a user's sessions - * and deletes them. - * @param {string} accessToken - a valid access token - * @param {string} userId- keycloak user id to delete sessions - * - * @example - * cy.keycloakDeleteUserSessions('abcde', '12345'); - */ - keycloakDeleteUserSessions(accessToken: string, userId: string): Chainable; - - /** - * keycloakResetUsers is a command that "resets" (deletes and re-creates) the users. - * @param {object[]} users - an array of user objects - * - * @example - * cy.keycloakResetUsers([{firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true}]); - */ - keycloakResetUsers(users: any[]): Chainable; - - /** - * keycloakCreateUser is a command that creates a keycloak user. - * @param {User} user - a user object - * - * @example - * cy.keycloakCreateUser({firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true}); - */ - keycloakCreateUser(user: any): Chainable; - - /** - * keycloakSuspendUser is a command that suspends a user (enabled=false) - * @param {string} userEmail - email of keycloak user - * - * @example - * cy.keycloakSuspendUser('user@test.com'); - */ - keycloakSuspendUser(userEmail: string): Chainable; - - /** - * keycloakUnsuspendUser is a command that re-activates a user (enabled=true) - * @param {string} userEmail - email of keycloak user - * - * @example - * cy.keycloakUnsuspendUser('user@test.com'); - */ - keycloakUnsuspendUser(userEmail: string): Chainable; - - /** - * checkKeycloakLoginPage is a command that verifies the keycloak login page is displayed - * - * @example - * cy.checkKeycloakLoginPage(); - */ - checkKeycloakLoginPage(): Chainable; - - /** - * doKeycloakLogin is a command that attempts to log a user into keycloak. - * - * @example - * cy.doKeycloakLogin(); - */ - doKeycloakLogin(user): Chainable; - - /** - * verifyKeycloakLoginFailed is a command that verifies a keycloak login failed. - * - * @example - * cy.verifyKeycloakLoginFailed(); - */ - verifyKeycloakLoginFailed(): Chainable; - } -} diff --git a/e2e-tests/cypress/tests/support/keycloak_commands.js b/e2e-tests/cypress/tests/support/keycloak_commands.js deleted file mode 100644 index 41beba58ef..0000000000 --- a/e2e-tests/cypress/tests/support/keycloak_commands.js +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import * as TIMEOUTS from '../fixtures/timeouts'; - -const { - keycloakBaseUrl, - keycloakAppName, -} = Cypress.env(); - -const baseUrl = `${keycloakBaseUrl}/auth/admin/realms/${keycloakAppName}`; -const loginUrl = `${keycloakBaseUrl}/auth/realms/master/protocol/openid-connect/token`; - -function buildProfile(user) { - return { - firstName: user.firstname, - lastName: user.lastname, - email: user.email, - username: user.username, - enabled: true, - }; -} - -Cypress.Commands.add('keycloakGetAccessTokenAPI', () => { - return cy.task('keycloakRequest', { - baseUrl: loginUrl, - path: '', - method: 'post', - headers: {'Content-type': 'application/x-www-form-urlencoded'}, - data: 'grant_type=password&username=mmuser&password=mostest&client_id=admin-cli', - }).then((response) => { - expect(response.status).to.equal(200); - const token = response.data.access_token; - return cy.wrap(token); - }); -}); - -Cypress.Commands.add('keycloakCreateUserAPI', (accessToken, user = {}) => { - const profile = buildProfile(user); - return cy.task('keycloakRequest', { - baseUrl, - path: 'users', - method: 'post', - data: profile, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }).then((response) => { - expect(response.status).to.equal(201); - }); -}); - -Cypress.Commands.add('keycloakResetPasswordAPI', (accessToken, userId, password) => { - return cy.task('keycloakRequest', { - baseUrl, - path: `users/${userId}/reset-password`, - method: 'put', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - data: {type: 'password', temporary: false, value: password}, - }).then((response) => { - if (response.status === 200 && response.data.length > 0) { - return cy.wrap(response.data[0].id); - } - return null; - }); -}); - -Cypress.Commands.add('keycloakGetUserAPI', (accessToken, email) => { - return cy.task('keycloakRequest', { - baseUrl, - path: 'users?email=' + email, - method: 'get', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }).then((response) => { - if (response.status === 200 && response.data.length > 0) { - return cy.wrap(response.data[0].id); - } - return null; - }); -}); - -Cypress.Commands.add('keycloakDeleteUserAPI', (accessToken, userId) => { - return cy.task('keycloakRequest', { - baseUrl, - path: `users/${userId}`, - method: 'delete', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }).then((response) => { - expect(response.status).to.equal(204); - expect(response.data).is.empty; - }); -}); - -Cypress.Commands.add('keycloakUpdateUserAPI', (accessToken, userId, data) => { - return cy.task('keycloakRequest', { - baseUrl, - path: 'users/' + userId, - method: 'put', - headers: { - Authorization: `Bearer ${accessToken}`, - }, - data, - }).then((response) => { - expect(response.status).to.equal(204); - expect(response.data).is.empty; - }); -}); - -Cypress.Commands.add('keycloakDeleteSessionAPI', (accessToken, sessionId) => { - return cy.task('keycloakRequest', { - baseUrl, - path: `sessions/${sessionId}`, - method: 'delete', - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }).then((delResponse) => { - expect(delResponse.status).to.equal(204); - expect(delResponse.data).is.empty; - }); -}); - -Cypress.Commands.add('keycloakGetUserSessionsAPI', (accessToken, userId) => { - return cy.task('keycloakRequest', { - baseUrl, - path: `users/${userId}/sessions`, - method: 'get', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }, - }).then((response) => { - expect(response.status).to.equal(200); - expect(response.data); - return cy.wrap(response.data); - }); -}); - -Cypress.Commands.add('keycloakDeleteUserSessions', (accessToken, userId) => { - return cy.keycloakGetUserSessionsAPI(accessToken, userId).then((responseData) => { - if (responseData.length > 0) { - Object.values(responseData).forEach((data) => { - const sessionId = data.id; - cy.keycloakDeleteSession(accessToken, sessionId); - }); - - // Ensure we clear out these specific cookies - ['JSESSIONID'].forEach((cookie) => { - cy.clearCookie(cookie); - }); - } - }); -}); - -Cypress.Commands.add('keycloakResetUsers', (users) => { - return cy.keycloakGetAccessTokenAPI().then((accessToken) => { - Object.values(users).forEach((_user) => { - cy.keycloakGetUserAPI(accessToken, _user.email).then((userId) => { - if (userId) { - cy.keycloakDeleteUserAPI(accessToken, userId); - } - }).then(() => { - cy.keycloakCreateUser(accessToken, _user).then((_id) => { - _user.keycloakId = _id; - }); - }); - }); - }); -}); - -Cypress.Commands.add('keycloakCreateUser', (accessToken, user) => { - return cy.keycloakCreateUserAPI(accessToken, user).then(() => { - cy.keycloakGetUserAPI(accessToken, user.email).then((newId) => { - cy.keycloakResetPasswordAPI(accessToken, newId, user.password).then(() => { - cy.keycloakDeleteUserSessions(accessToken, newId).then(() => { - return cy.wrap(newId); - }); - }); - }); - }); -}); - -Cypress.Commands.add('keycloakCreateUsers', (users = []) => { - return cy.keycloakGetAccessTokenAPI().then((accessToken) => { - return users.forEach((user) => { - return cy.keycloakCreateUser(accessToken, user); - }); - }); -}); - -Cypress.Commands.add('keycloakUpdateUser', (userEmail, data) => { - return cy.keycloakGetAccessTokenAPI().then((accessToken) => { - return cy.keycloakGetUserAPI(accessToken, userEmail).then((userId) => { - return cy.keycloakUpdateUserAPI(accessToken, userId, data); - }); - }); -}); - -Cypress.Commands.add('keycloakSuspendUser', (userEmail) => { - const data = {enabled: false}; - cy.keycloakUpdateUser(userEmail, data); -}); - -Cypress.Commands.add('keycloakUnsuspendUser', (userEmail) => { - const data = {enabled: true}; - cy.keycloakUpdateUser(userEmail, data); -}); - -Cypress.Commands.add('checkKeycloakLoginPage', () => { - cy.findByText('Username or email', {timeout: TIMEOUTS.ONE_SEC}).should('be.visible'); - cy.findByText('Password').should('be.visible'); - cy.findAllByText('Log In').should('be.visible'); -}); - -Cypress.Commands.add('doKeycloakLogin', (user) => { - cy.apiLogout(); - cy.visit('/login'); - cy.findByText('SAML').click(); - cy.findByText('Username or email').type(user.email); - cy.findByText('Password').type(user.password); - cy.findAllByText('Log In').last().click(); -}); - -Cypress.Commands.add('verifyKeycloakLoginFailed', () => { - cy.findAllByText('Account is disabled, contact your administrator.').should('be.visible'); -}); diff --git a/e2e-tests/cypress/tests/support/keycloak_commands.ts b/e2e-tests/cypress/tests/support/keycloak_commands.ts new file mode 100644 index 0000000000..0b896cc9d3 --- /dev/null +++ b/e2e-tests/cypress/tests/support/keycloak_commands.ts @@ -0,0 +1,425 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ChainableT} from 'tests/types'; +import * as TIMEOUTS from '../fixtures/timeouts'; + +const { + keycloakBaseUrl, + keycloakAppName, +} = Cypress.env(); + +const baseUrl = `${keycloakBaseUrl}/auth/admin/realms/${keycloakAppName}`; +const loginUrl = `${keycloakBaseUrl}/auth/realms/master/protocol/openid-connect/token`; + +function buildProfile(user) { + return { + firstName: user.firstname, + lastName: user.lastname, + email: user.email, + username: user.username, + enabled: true, + }; +} + +/** +* keycloakGetAccessTokenAPI is a task wrapped as command with post-verification +* that an Access Token is successfully retrieved +* @returns {string} - access token +*/ +function keycloakGetAccessTokenAPI(): ChainableT { + return cy.task('keycloakRequest', { + baseUrl: loginUrl, + path: '', + method: 'post', + headers: {'Content-type': 'application/x-www-form-urlencoded'}, + data: 'grant_type=password&username=mmuser&password=mostest&client_id=admin-cli', + }).then((response: any) => { + expect(response.status).to.equal(200); + const token = response.data.access_token; + return cy.wrap(token as string); + }); +} + +Cypress.Commands.add('keycloakGetAccessTokenAPI', keycloakGetAccessTokenAPI); + +/** +* keycloakCreateUserAPI is a task wrapped as command with post-verification +* that a user is successfully created in keycloak +* @param {string} accessToken - a valid access token +* @param {object} user - a keycloak user object to create +* +* @example +* cy.keycloakCreateUserAPI('abcde', {firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true,}); +*/ +function keycloakCreateUserAPI(accessToken: string, user: any = {}) { + const profile = buildProfile(user); + return cy.task('keycloakRequest', { + baseUrl, + path: 'users', + method: 'post', + data: profile, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }).then((response: Response) => { + expect(response.status).to.equal(201); + }); +} + +Cypress.Commands.add('keycloakCreateUserAPI', keycloakCreateUserAPI); + +/** +* keycloakResetPasswordAPI is a task wrapped as command with post-verification +* that a user password is successfully reset in keycloak +* @param {string} accessToken - a valid access token +* @param {string} userId - a keycloak userId +* @param {string} password - new password to set +* +* @example +* cy.keycloakResetPasswordAPI('abcde', '12345', 'password'); +*/ +function keycloakResetPasswordAPI(accessToken: string, userId: string, password: string): ChainableT { + return cy.task('keycloakRequest', { + baseUrl, + path: `users/${userId}/reset-password`, + method: 'put', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + data: {type: 'password', temporary: false, value: password}, + }).then((response: any) => { + if (response.status === 200 && response.data.length > 0) { + return cy.wrap(response.data[0].id); + } + return null; + }); +} + +Cypress.Commands.add('keycloakResetPasswordAPI', keycloakResetPasswordAPI); + +/** +* keycloakGetUserAPI is a task wrapped as command with post-verification +* that a user is successfully found in keycloak +* @param {string} accessToken - a valid access token +* @param {string} email - an email to query +* @returns {string} - keycloak userId if found +* +* @example +* cy.keycloakGetUserAPI('abcde', 'test@mm.com'); +*/ +function keycloakGetUserAPI(accessToken: string, email: string): ChainableT { + return cy.task('keycloakRequest', { + baseUrl, + path: 'users?email=' + email, + method: 'get', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }).then((response: any) => { + if (response.status === 200 && response.data.length > 0) { + return cy.wrap(response.data[0].id); + } + return null; + }); +} + +Cypress.Commands.add('keycloakGetUserAPI', keycloakGetUserAPI); + +/** +* keycloakDeleteUserAPI is a task wrapped as command with post-verification +* that a user is successfully deleted in keycloak +* @param {string} accessToken - a valid access token +* @param {string} userId - keycloak user id to delete +* +* @example +* cy.keycloakDeleteUserAPI('abcde', '12345'); +*/ +function keycloakDeleteUserAPI(accessToken: string, userId: string): ChainableT { + return cy.task('keycloakRequest', { + baseUrl, + path: `users/${userId}`, + method: 'delete', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }).then((response: any) => { + expect(response.status).to.equal(204); + expect(response.data).is.empty; + }); +} + +Cypress.Commands.add('keycloakDeleteUserAPI', keycloakDeleteUserAPI); + +/** +* keycloakUpdateUserAPI is a task wrapped as command with post-verification +* that a user is successfully updated in keycloak +* @param {string} accessToken - a valid access token +* @param {string} userId - keycloak user id to delete +* @param {object} data - keycloak user object +* +* @example +* cy.keycloakUpdateUserAPI('abcde', '12345', {'enabled': false}}); +*/ +function keycloakUpdateUserAPI(accessToken: string, userId: string, data: any): ChainableT { + return cy.task('keycloakRequest', { + baseUrl, + path: 'users/' + userId, + method: 'put', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + data, + }).then((response: any) => { + expect(response.status).to.equal(204); + expect(response.data).is.empty; + }); +} + +Cypress.Commands.add('keycloakUpdateUserAPI', keycloakUpdateUserAPI); + +/** +* keycloakDeleteSessionAPI is a task wrapped as command with post-verification +* that a users session is successfully deleted in keycloak +* @param {string} accessToken - a valid access token +* @param {string} sessionId- keycloak session id to delete +* +* @example +* cy.keycloakDeleteSessionAPI('abcde', '12345'); +*/ +function keycloakDeleteSessionAPI(accessToken: string, sessionId: string): ChainableT { + return cy.task('keycloakRequest', { + baseUrl, + path: `sessions/${sessionId}`, + method: 'delete', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }).then((delResponse: any) => { + expect(delResponse.status).to.equal(204); + expect(delResponse.data).is.empty; + }); +} + +Cypress.Commands.add('keycloakDeleteSessionAPI', keycloakDeleteSessionAPI); + +/** +* keycloakGetUserSessionsAPI is a task wrapped as command with post-verification +* that a users sessions are successfully found +* @param {string} accessToken - a valid access token +* @param {string} userId - keycloak user id to find sessions +* @returns {string[]} - array of keycloak session ids +* +* @example +* cy.keycloakGetUserSessionsAPI('abcde', '12345'); +*/ +function keycloakGetUserSessionsAPI(accessToken: string, userId: string): ChainableT { + return cy.task('keycloakRequest', { + baseUrl, + path: `users/${userId}/sessions`, + method: 'get', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }).then((response: any) => { + expect(response.status).to.equal(200); + expect(response.data); + return cy.wrap(response.data); + }); +} + +Cypress.Commands.add('keycloakGetUserSessionsAPI', keycloakGetUserSessionsAPI); + +/** +* keycloakDeleteUserSessions is a command that finds a user's sessions +* and deletes them. +* @param {string} accessToken - a valid access token +* @param {string} userId- keycloak user id to delete sessions +* +* @example +* cy.keycloakDeleteUserSessions('abcde', '12345'); +*/ +function keycloakDeleteUserSessions(accessToken: string, userId: string): ChainableT { + return cy.keycloakGetUserSessionsAPI(accessToken, userId).then((responseData) => { + if (responseData.length > 0) { + responseData.forEach((sessionId) => { + cy.keycloakDeleteSessionAPI(accessToken, sessionId); + }); + + // Ensure we clear out these specific cookies + ['JSESSIONID'].forEach((cookie) => { + cy.clearCookie(cookie); + }); + } + }); +} + +Cypress.Commands.add('keycloakDeleteUserSessions', keycloakDeleteUserSessions); + +/** +* keycloakResetUsers is a command that "resets" (deletes and re-creates) the users. +* @param {object[]} users - an array of user objects +* +* @example +* cy.keycloakResetUsers([{firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true}]); +*/ +function keycloakResetUsers(users: any[]): ChainableT { + return cy.keycloakGetAccessTokenAPI().then((accessToken) => { + users.forEach((_user) => { + cy.keycloakGetUserAPI(accessToken, _user.email).then((userId) => { + if (userId) { + cy.keycloakDeleteUserAPI(accessToken, userId); + } + }).then(() => { + cy.keycloakCreateUser(accessToken, _user).then((_id) => { + _user.keycloakId = _id; + }); + }); + }); + }); +} + +Cypress.Commands.add('keycloakResetUsers', keycloakResetUsers); + +/** +* keycloakCreateUser is a command that creates a keycloak user. +* @param {User} user - a user object +* +* @example +* cy.keycloakCreateUser({firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true}); +*/ +function keycloakCreateUser(accessToken, user: any): ChainableT { + return cy.keycloakCreateUserAPI(accessToken, user).then(() => { + cy.keycloakGetUserAPI(accessToken, user.email).then((newId) => { + cy.keycloakResetPasswordAPI(accessToken, newId, user.password).then(() => { + cy.keycloakDeleteUserSessions(accessToken, newId).then(() => { + return cy.wrap(newId); + }); + }); + }); + }); +} + +Cypress.Commands.add('keycloakCreateUser', keycloakCreateUser); + +function keycloakCreateUsers(users = []) { + return cy.keycloakGetAccessTokenAPI().then((accessToken) => { + return users.forEach((user) => { + return cy.keycloakCreateUser(accessToken, user); + }); + }); +} + +Cypress.Commands.add('keycloakCreateUsers', keycloakCreateUsers); + +function keycloakUpdateUser(userEmail, data): ChainableT { + return cy.keycloakGetAccessTokenAPI().then((accessToken) => { + return cy.keycloakGetUserAPI(accessToken, userEmail).then((userId) => { + return cy.keycloakUpdateUserAPI(accessToken, userId, data); + }); + }); +} + +Cypress.Commands.add('keycloakUpdateUser', keycloakUpdateUser); + +/** +* keycloakSuspendUser is a command that suspends a user (enabled=false) +* @param {string} userEmail - email of keycloak user +* +* @example +* cy.keycloakSuspendUser('user@test.com'); +*/ +function keycloakSuspendUser(userEmail: string) { + const data = {enabled: false}; + cy.keycloakUpdateUser(userEmail, data); +} + +Cypress.Commands.add('keycloakSuspendUser', keycloakSuspendUser); + +/** +* keycloakUnsuspendUser is a command that re-activates a user (enabled=true) +* @param {string} userEmail - email of keycloak user +* +* @example +* cy.keycloakUnsuspendUser('user@test.com'); +*/ +function keycloakUnsuspendUser(userEmail: string): ChainableT { + const data = {enabled: true}; + return cy.keycloakUpdateUser(userEmail, data); +} + +Cypress.Commands.add('keycloakUnsuspendUser', keycloakUnsuspendUser); + +/** +* checkKeycloakLoginPage is a command that verifies the keycloak login page is displayed +* +* @example +* cy.checkKeycloakLoginPage(); +*/ +function checkKeycloakLoginPage() { + cy.findByText('Username or email', {timeout: TIMEOUTS.ONE_SEC}).should('be.visible'); + cy.findByText('Password').should('be.visible'); + cy.findAllByText('Log In').should('be.visible'); +} + +Cypress.Commands.add('checkKeycloakLoginPage', checkKeycloakLoginPage); + +/** +* doKeycloakLogin is a command that attempts to log a user into keycloak. +* +* @example +* cy.doKeycloakLogin(); +*/ +function doKeycloakLogin(user) { + cy.apiLogout(); + cy.visit('/login'); + cy.findByText('SAML').click(); + cy.findByText('Username or email').type(user.email); + cy.findByText('Password').type(user.password); + cy.findAllByText('Log In').last().click(); +} + +Cypress.Commands.add('doKeycloakLogin', doKeycloakLogin); + +/** +* verifyKeycloakLoginFailed is a command that verifies a keycloak login failed. +* +* @example +* cy.verifyKeycloakLoginFailed(); +*/ +function verifyKeycloakLoginFailed() { + cy.findAllByText('Account is disabled, contact your administrator.').should('be.visible'); +} + +Cypress.Commands.add('verifyKeycloakLoginFailed', verifyKeycloakLoginFailed); + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + keycloakCreateUsers: typeof keycloakCreateUsers; + keycloakUpdateUser: typeof keycloakUpdateUser; + keycloakGetAccessTokenAPI: typeof keycloakGetAccessTokenAPI; + keycloakCreateUserAPI: typeof keycloakCreateUserAPI; + keycloakResetPasswordAPI: typeof keycloakResetPasswordAPI; + keycloakGetUserAPI: typeof keycloakGetUserAPI; + keycloakDeleteUserAPI: typeof keycloakDeleteUserAPI; + keycloakUpdateUserAPI: typeof keycloakUpdateUserAPI; + keycloakDeleteSessionAPI: typeof keycloakDeleteSessionAPI; + keycloakGetUserSessionsAPI: typeof keycloakGetUserSessionsAPI; + keycloakDeleteUserSessions: typeof keycloakDeleteUserSessions; + keycloakResetUsers: typeof keycloakResetUsers; + keycloakCreateUser: typeof keycloakCreateUser; + keycloakSuspendUser(userEmail: string): ChainableT; + keycloakUnsuspendUser: typeof keycloakUnsuspendUser; + checkKeycloakLoginPage: typeof checkKeycloakLoginPage; + doKeycloakLogin(user): ChainableT; + verifyKeycloakLoginFailed: typeof verifyKeycloakLoginFailed; + } + } +} diff --git a/e2e-tests/cypress/tests/support/okta_commands.ts b/e2e-tests/cypress/tests/support/okta_commands.ts index 19de9c8100..f281f1f0b5 100644 --- a/e2e-tests/cypress/tests/support/okta_commands.ts +++ b/e2e-tests/cypress/tests/support/okta_commands.ts @@ -29,10 +29,10 @@ interface User { password: string; } -interface UserCollection { - admins: User[]; - guests: User[]; - regulars: User[]; +export interface UserCollection { + admins: Record>; + guests: Record>; + regulars: Record>; } interface OktaResponse { @@ -362,7 +362,7 @@ declare global { oktaAddUsers: typeof oktaAddUsers; oktaRemoveUsers: typeof oktaRemoveUsers; checkOktaLoginPage: typeof checkOktaLoginPage; - doOktaLogin: typeof doOktaLogin; + doOktaLogin(user: User): ChainableT; } } } diff --git a/e2e-tests/cypress/tests/support/saml_commands.ts b/e2e-tests/cypress/tests/support/saml_commands.ts index 03786f53a6..17a4e2ec06 100644 --- a/e2e-tests/cypress/tests/support/saml_commands.ts +++ b/e2e-tests/cypress/tests/support/saml_commands.ts @@ -51,7 +51,7 @@ function doSamlLogin(settings) { cy.checkLoginPage(settings); //click the login button - cy.findByText(settings.loginButtonText).should('be.visible').click().wait(TIMEOUTS.ONE_SEC); + return cy.findByText(settings.loginButtonText).should('be.visible').click().wait(TIMEOUTS.ONE_SEC); } Cypress.Commands.add('doSamlLogin', doSamlLogin); @@ -65,7 +65,7 @@ function doSamlLogout(settings) { // # Logout then check login page cy.uiLogout(); - cy.checkLoginPage(settings); + return cy.checkLoginPage(settings); } Cypress.Commands.add('doSamlLogout', doSamlLogout); @@ -99,14 +99,14 @@ Cypress.Commands.add('getInvitePeopleLink', getInvitePeopleLink); * @param {AdminConfig} config - The config object * @returns {TestSettings} - The settings to use for SAML tests */ -function setTestSettings(loginButtonText: string, config: AdminConfig): TestSettings { - return { +function setTestSettings(loginButtonText: string, config: AdminConfig): ChainableT { + return cy.wrap({ loginButtonText, siteName: config.TeamSettings.SiteName, siteUrl: config.ServiceSettings.SiteURL, teamName: '', user: null, - }; + }); } Cypress.Commands.add('setTestSettings', setTestSettings);