From aa7eb2e8448aa7b16ccbe9025569c509a9388531 Mon Sep 17 00:00:00 2001 From: Tony <52470376+wongtonyb@users.noreply.github.com> Date: Thu, 10 Jul 2025 11:26:52 -0400 Subject: [PATCH] =?UTF-8?q?test:=20=F0=9F=92=8D=20Add=20admin=20UI=20test?= =?UTF-8?q?=20for=20change=20passsword=20flow=20(#2920)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 💍 Add admin UI test for change passsword flow This PR adds a test to the Admin UI e2e test suite to validate the workflow of changing user password from settings menu ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17173 * refactor: 💡 create resources via REST API ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17173 * refactor: 💡 consistent code structure, comments ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17173 * refactor: 💡 code structure consistency ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17173 * refactor: 💡 removed version parameter from boundary-http funcs ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17173 * refactor: 💡 rm unneeded params from https makeAuthMethodPrimary ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17173 * refactor: 💡 rm ui login step ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17173 --- e2e-tests/admin/tests/change-password.spec.js | 104 ++++++++++++++++++ e2e-tests/helpers/boundary-http.js | 3 + e2e-tests/helpers/boundary-http/accounts.js | 34 ++++++ .../helpers/boundary-http/auth-methods.js | 36 ++++++ e2e-tests/helpers/boundary-http/scopes.js | 18 +++ e2e-tests/helpers/boundary-http/users.js | 41 +++++++ 6 files changed, 236 insertions(+) create mode 100644 e2e-tests/admin/tests/change-password.spec.js create mode 100644 e2e-tests/helpers/boundary-http/accounts.js create mode 100644 e2e-tests/helpers/boundary-http/auth-methods.js create mode 100644 e2e-tests/helpers/boundary-http/users.js diff --git a/e2e-tests/admin/tests/change-password.spec.js b/e2e-tests/admin/tests/change-password.spec.js new file mode 100644 index 000000000..aa03ff5ed --- /dev/null +++ b/e2e-tests/admin/tests/change-password.spec.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test } from '../../global-setup.js'; +import { expect } from '@playwright/test'; + +import * as boundaryCli from '../../helpers/boundary-cli'; +import * as boundaryHttp from '../../helpers/boundary-http.js'; +import { LoginPage } from '../pages/login.js'; + +// Reset storage state for this file to avoid being authenticated +test.use({ storageState: { cookies: [], origins: [] } }); + +test.beforeAll(async () => { + await boundaryCli.checkBoundaryCli(); +}); + +test( + 'Verify user can change password', + { tag: ['@ce', '@ent', '@aws', '@docker'] }, + async ({ page, request }) => { + await page.goto('/'); + let org; + let authMethod; + try { + let account; + let user; + + // Create a new org, password auth method, account, and user via http API + org = await boundaryHttp.createOrg(request); + + authMethod = await boundaryHttp.createPasswordAuthMethod(request, org.id); + const username = 'test-user'; + const password = 'password'; + account = await boundaryHttp.createPasswordAccount(request, { + authMethodId: authMethod.id, + username, + password, + }); + await boundaryHttp.makeAuthMethodPrimary(request, { + org, + authMethodId: authMethod.id, + }); + + user = await boundaryHttp.createUser(request, org.id); + await boundaryHttp.addAccountsToUser(request, { + user, + accountIds: [account.id], + }); + + // Log in using new account + await page.reload(); + await page.getByText('Choose a different scope').click(); + await page.getByRole('link', { name: org.name }).click(); + await page.getByRole('link', { name: authMethod.name }).click(); + const loginPage = new LoginPage(page); + await loginPage.login(username, password); + await expect( + page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText('Projects'), + ).toBeVisible(); + + // Change password for user + await page.getByRole('button', { name: username }).click(); + await page.getByRole('link', { name: 'Change Password' }).click(); + const newPassword = 'new-password'; + await page.getByLabel('Current Password').fill(password); + await page.getByLabel('New Password').fill(newPassword); + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + + // Confirm user cannot log in with old password + await loginPage.logout(username); + await page.getByText('Choose a different scope').click(); + await page.getByRole('link', { name: org.name }).click(); + await page.getByRole('link', { name: authMethod.name }).click(); + await page.getByLabel('Login Name').fill(username); + await page.getByLabel('Password', { exact: true }).fill(password); + await page.getByRole('button', { name: 'Sign In' }).click(); + await expect(page.getByRole('alert').getByText('Error')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible(); + + // Confirm user can log in with new password + await loginPage.login(username, newPassword); + await expect( + page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText('Projects'), + ).toBeVisible(); + } finally { + if (authMethod.id) { + await boundaryHttp.deleteAuthMethod(request, authMethod.id); + } + if (org.id) { + await boundaryHttp.deleteOrg(request, org.id); + } + } + }, +); diff --git a/e2e-tests/helpers/boundary-http.js b/e2e-tests/helpers/boundary-http.js index 264a6c531..15200d5bb 100644 --- a/e2e-tests/helpers/boundary-http.js +++ b/e2e-tests/helpers/boundary-http.js @@ -11,3 +11,6 @@ export * from './boundary-http/hosts.js'; export * from './boundary-http/credential-stores.js'; export * from './boundary-http/credentials.js'; export * from './boundary-http/credential-libraries.js'; +export * from './boundary-http/auth-methods.js'; +export * from './boundary-http/accounts.js'; +export * from './boundary-http/users.js'; diff --git a/e2e-tests/helpers/boundary-http/accounts.js b/e2e-tests/helpers/boundary-http/accounts.js new file mode 100644 index 000000000..91b8177b5 --- /dev/null +++ b/e2e-tests/helpers/boundary-http/accounts.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { nanoid } from 'nanoid'; +import { checkResponse } from './responseHelper.js'; + +/** + * Creates a new password auth account + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} authMethodId + * @param {string} username + * @param {string} password + * @returns {Promise} + */ +export async function createPasswordAccount( + request, + { authMethodId, username, password }, +) { + const response = await request.post(`/v1/accounts`, { + data: { + attributes: { + login_name: username, + password: password, + }, + auth_method_id: authMethodId, + description: 'Generated via API from automated test', + name: `Account ${nanoid()}`, + type: 'password', + }, + }); + return checkResponse(response); +} diff --git a/e2e-tests/helpers/boundary-http/auth-methods.js b/e2e-tests/helpers/boundary-http/auth-methods.js new file mode 100644 index 000000000..1b51839ff --- /dev/null +++ b/e2e-tests/helpers/boundary-http/auth-methods.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { nanoid } from 'nanoid'; +import { checkResponse } from './responseHelper.js'; + +/** + * Creates a new auth method + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} orgId + * @returns {Promise} + */ +export async function createPasswordAuthMethod(request, orgId) { + const response = await request.post(`/v1/auth-methods`, { + data: { + scope_id: orgId, + name: `Auth Method ${nanoid()}`, + description: 'Generated via API from automated test', + type: 'password', + }, + }); + return checkResponse(response); +} + +/** + * Deletes an auth method + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} authMethodId + * @returns {Promise} + */ +export async function deleteAuthMethod(request, authMethodId) { + const response = await request.delete(`/v1/auth-methods/${authMethodId}`); + return checkResponse(response, true); +} diff --git a/e2e-tests/helpers/boundary-http/scopes.js b/e2e-tests/helpers/boundary-http/scopes.js index 078e33abe..e850e6a3b 100644 --- a/e2e-tests/helpers/boundary-http/scopes.js +++ b/e2e-tests/helpers/boundary-http/scopes.js @@ -48,3 +48,21 @@ export async function createProject(request, scopeId) { }); return checkResponse(response); } + +/** + * Make Auth Method Primary + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} orgId + * @param {string} authMethodId + * @returns {Promise} + */ +export async function makeAuthMethodPrimary(request, { org, authMethodId }) { + const response = await request.patch(`v1/scopes/${org.id}`, { + data: { + primary_auth_method_id: authMethodId, + version: org.version, + }, + }); + + return checkResponse(response); +} diff --git a/e2e-tests/helpers/boundary-http/users.js b/e2e-tests/helpers/boundary-http/users.js new file mode 100644 index 000000000..b4d85ec66 --- /dev/null +++ b/e2e-tests/helpers/boundary-http/users.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { nanoid } from 'nanoid'; +import { checkResponse } from './responseHelper.js'; + +/** + * Create a new user + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} orgId + * @returns {Promise} + */ +export async function createUser(request, orgId) { + const response = await request.post(`/v1/users`, { + data: { + scope_id: orgId, + name: `User ${nanoid()}`, + description: 'Generated via API from automated test', + }, + }); + return checkResponse(response); +} + +/** + * Add account to user + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} userId + * @param {string} accountId + * @returns {Promise} + */ +export async function addAccountsToUser(request, { user, accountIds }) { + const response = await request.post(`/v1/users/${user.id}:add-accounts`, { + data: { + account_ids: accountIds, + version: user.version, + }, + }); + return checkResponse(response); +}