test: 💍 Add admin UI test for change passsword flow (#2920)

* 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
This commit is contained in:
Tony authored and GitHub committed 2025-07-10 11:26:52 -04:00
1 parent 8e31adb9db
commit aa7eb2e844
6 files changed
+236

No files matched your search

@@ -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);
}
}
},
);
+3
View File
@@ -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';
@@ -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<Serializable>}
*/
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);
}
@@ -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<Serializable>}
*/
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<Serializable>}
*/
export async function deleteAuthMethod(request, authMethodId) {
const response = await request.delete(`/v1/auth-methods/${authMethodId}`);
return checkResponse(response, true);
}
+18
View File
@@ -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<Serializable>}
*/
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);
}
+41
View File
@@ -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<Serializable>}
*/
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<Serializable>}
*/
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);
}