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); +}