[UI][VAULT-42963] plugin management (#12973) (#13044)

* plugin management tests!

* Add general settings test

* Add configuration settings page

* Update configuration page tests..

* Use configuration page for pki tests

* Fix double comment

* Add configuration page tests

* Update keymgmt and pki tune tests

* Update secret engines to use the step helper

* Add transform configuration workflow test
This commit is contained in:
Kianna 2026-03-16 11:24:45 -07:00 committed by GitHub
parent 523654c7c8
commit f118b381ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 469 additions and 30 deletions

View file

@ -4,6 +4,7 @@
*/
import { Page } from '@playwright/test';
import { findEngineDisplayName } from './configuration-settings';
export class BasePage {
constructor(protected page: Page) {}
@ -25,6 +26,16 @@ export class BasePage {
}
}
async disableEngine(path: string) {
await this.page.goto('secrets-engines');
await this.page
.getByRole('row', { name: `Type of backend ${path}` })
.getByLabel('supported secrets engine menu')
.click();
await this.page.getByRole('button', { name: 'Delete' }).click();
await this.page.getByRole('button', { name: 'Confirm' }).click();
}
/**
* Enable a secrets engine with a dynamic path
* @param engineType - The type of engine to enable (e.g., 'KV', 'Transit', 'PKI Certificates')
@ -47,7 +58,7 @@ export class BasePage {
// Click "Enable new engine"
await this.page.getByRole('link', { name: 'Enable new engine' }).click();
await this.page.getByRole('heading', { name: engineType }).click();
await this.page.getByRole('heading', { name: findEngineDisplayName(engineType) }).click();
if (options?.external) {
// Prerequisite: mock plugin catalog endpoint in the test so the External plugin option is available.

View file

@ -0,0 +1,113 @@
/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
import { expect, type Page } from '@playwright/test';
import { ALL_ENGINES } from '../../lib/core/addon/utils/all-engines-metadata';
export const findEngineDisplayName = (engineType: string) => {
const engine = ALL_ENGINES.find((e) => e.type === engineType);
return engine ? engine.displayName : engineType;
};
const configurableEngines = ALL_ENGINES.filter((engine) => engine.isConfigurable).map(
(engine) => engine.type
);
export class ConfigurationSettingsPage {
constructor(protected page: Page) {}
async navigateToConfiguration(path: string) {
await this.page.getByRole('button', { name: 'Manage', exact: true }).click();
await this.page.getByRole('link', { name: 'Configure' }).click();
await expect(this.page.getByRole('heading', { name: `${path} configuration` })).toContainText(
`${path} configuration`
);
}
async assertPluginSettingsTabActive(engineType: string) {
const engineDisplayName = findEngineDisplayName(engineType);
await expect(this.page.getByRole('link', { name: 'General settings' })).not.toHaveClass(/active/);
await expect(this.page.getByRole('link', { name: `${engineDisplayName} settings` })).toHaveClass(
/active/
);
await expect(this.page.getByRole('link', { name: `${engineDisplayName} settings` })).toContainText(
`${engineDisplayName} settings`
);
}
async navigateToGeneralSettings(engineType: string) {
const engineDisplayName = findEngineDisplayName(engineType);
await this.page.getByRole('link', { name: 'General settings' }).click();
await expect(this.page.getByRole('link', { name: 'General settings' })).toHaveClass(/active/);
if (configurableEngines.includes(engineDisplayName)) {
await expect(
this.page.getByRole('link', {
name: `${engineDisplayName} settings`,
})
).not.toHaveClass(/active/);
}
await expect(this.page.getByRole('form', { name: 'general settings form' })).toBeVisible();
}
async editAndVerifyGeneralSettings(path: string, engineType: string, isExternalPlugin = false) {
await expect(this.page.getByText(`Engine type ${engineType}`)).toBeVisible();
await expect(this.page.getByText('Running version')).toContainText('Running version');
await expect(this.page.getByRole('group', { name: 'Path' })).toBeVisible();
await expect(this.page.getByRole('button', { name: `copy ${path}/` })).toContainText(`${path}/`);
await expect(this.page.getByRole('group', { name: 'Accessor' })).toBeVisible();
await expect(
this.page.getByText('Default time-to-live (TTL) How long secrets in this engine stay valid. seconds')
).toBeVisible();
if (!isExternalPlugin) {
await this.page.getByRole('textbox', { name: 'Description' }).fill('some description');
await this.page.getByRole('textbox', { name: 'Default time-to-live (TTL)time' }).fill('2');
await this.page.getByRole('button', { name: 'Save changes' }).click();
await expect(this.page.getByRole('alert', { name: 'Configuration saved' })).toBeVisible();
await expect(this.page.getByRole('textbox', { name: 'Description' })).toHaveValue('some description');
await expect(this.page.getByRole('textbox', { name: 'Default time-to-live (TTL)time' })).toHaveValue(
'2'
);
}
}
async verifyUnsavedChangesModalOnNavigateAway(path: string) {
// make a change to trigger the unsaved changes modal
await this.page.getByRole('textbox', { name: 'Description' }).fill('unsaved changes test');
// try to navigate away
this.page.getByRole('link', { name: 'Back to main navigation' }).click();
// verify the unsaved changes modal appears
const modal = this.page.getByRole('dialog', { name: 'Unsaved changes' });
await expect(modal).toBeVisible();
await expect(
this.page.getByText("You've made changes to the following: Description Would you like to apply them?")
).toBeVisible();
// save unsaved changes
await modal.getByRole('button', { name: 'Save changes' }).click();
// verify still on the configuration page and description was updated
await this.page.getByRole('link', { name: 'Secrets', exact: true }).click();
await this.page.getByRole('link', { name: path }).click();
await this.page.getByRole('button', { name: 'Manage' }).click();
await this.page.getByRole('link', { name: 'Configure' }).click();
await this.page.getByRole('link', { name: 'General settings' }).click();
await expect(this.page.getByRole('textbox', { name: 'Description' })).toHaveValue('unsaved changes test');
// make another change to trigger the unsaved changes modal
await this.page.getByRole('textbox', { name: 'Description' }).fill('unsaved changes test 2');
// try to navigate away again
this.page.getByRole('link', { name: 'Back to main navigation' }).click();
// dismiss the modal
await modal.getByRole('button', { name: 'Discard changes' }).click();
await expect(this.page.getByRole('link', { name: 'Dashboard' })).toBeVisible();
}
}

View file

@ -4,6 +4,7 @@
*/
import { expect, test } from '@playwright/test';
import { ConfigurationSettingsPage } from '../../pages/configuration-settings';
const PINNED_PLUGIN_DATA = {
data: {
@ -100,6 +101,8 @@ const UPDATED_KEYMGMT_EXTERNAL_MOUNT_DATA = {
};
test('tune external keymgmt workflow', async ({ page }) => {
const configurationSettingsPage = new ConfigurationSettingsPage(page);
await test.step('mock the keymgmt pinned version response', async () => {
await page.route('**v1/sys/plugins/pins/secret/vault-plugin-secrets-keymgmt', async (route) => {
if (route.request().method() === 'GET') {
@ -142,7 +145,7 @@ test('tune external keymgmt workflow', async ({ page }) => {
});
});
await test.step('mock the initial keymgmt external tunde response', async () => {
await test.step('mock the initial keymgmt external tune response', async () => {
await page.route('**/v1/sys/mounts/keymgmt-external/tune', async (route) => {
if (route.request().method() === 'POST') {
await route.fulfill({
@ -159,11 +162,10 @@ test('tune external keymgmt workflow', async ({ page }) => {
await test.step("navigate to the external keymgmt mount's general settings page", async () => {
await page.goto('secrets-engines/keymgmt-external/list');
await page.getByRole('button', { name: 'Manage' }).click();
await page.getByRole('link', { name: 'Configure' }).click();
await expect(page.getByRole('heading', { level: 1 })).toContainText('keymgmt-external configuration');
await expect(page.getByRole('link', { name: 'General settings' })).toBeVisible();
await expect(page.getByRole('paragraph').nth(2)).toContainText('vault-plugin-secrets-keymgmt');
const path = 'keymgmt-external';
const engineType = 'vault-plugin-secrets-keymgmt';
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType, true);
await expect(page.getByRole('paragraph').nth(3)).toContainText('v0.17.0+ent (Pinned)');
});

View file

@ -5,6 +5,7 @@
import { expect, test } from '@playwright/test';
import { BasePage } from '../../pages/base';
import { ConfigurationSettingsPage } from '../../pages/configuration-settings';
test('keymgmt workflow', async ({ page }) => {
const basePage = new BasePage(page);
@ -86,3 +87,49 @@ test('keymgmt workflow', async ({ page }) => {
await page.getByRole('button', { name: 'Dismiss' }).click();
});
});
test('keymgmt tune workflow', async ({ page }) => {
const basePage = new BasePage(page);
const configurationSettingsPage = new ConfigurationSettingsPage(page);
const path = 'keymgmt-tune';
const engineType = 'keymgmt';
await test.step('enable keymgmt secrets engine mount', async () => {
await basePage.enableEngine(engineType, path);
});
await test.step('navigate to configuration page from manage dropdown ', async () => {
await configurationSettingsPage.navigateToConfiguration(path);
});
// keymgmt does not have plugin settings, so we only need to test for general settings
await test.step('navigate and verify general settings form', async () => {
await configurationSettingsPage.navigateToGeneralSettings(engineType);
await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType);
await page.getByRole('link', { name: 'Exit configuration' }).click();
});
await test.step('ensure that we navigate back to the keymgmt overview page when Exit configuration is clicked', async () => {
await expect(
page
.locator('div')
.filter({ hasText: `${path} Manage Create provider` })
.nth(3)
).toBeVisible();
});
await test.step('verify unsaved changes modal works in general settings', async () => {
// Navigate back to general settings
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.navigateToGeneralSettings(engineType);
// Test Unsaved changes modal
await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path);
});
await test.step('clean up and disable engine', async () => {
await basePage.disableEngine(path);
});
});

View file

@ -5,6 +5,7 @@
import { test, expect } from '@playwright/test';
import { BasePage } from '../../pages/base';
import { ConfigurationSettingsPage } from '../../pages/configuration-settings';
test('kubernetes secrets workflow', async ({ page }) => {
const basePage = new BasePage(page);
@ -52,29 +53,6 @@ test('kubernetes secrets workflow', async ({ page }) => {
await basePage.dismissFlashMessages();
});
await test.step('edit kubernetes configuration', async () => {
await page.getByRole('button', { name: 'Manage' }).click();
await page.getByRole('link', { name: 'Configure' }).click();
await page.getByRole('link', { name: 'Edit configuration' }).click();
await expect(page.getByRole('textbox', { name: 'Service account JWT' })).toBeEmpty();
await page.getByRole('textbox', { name: 'Kubernetes host' }).fill('https://127.0.0.1:8443');
await page.getByRole('textbox', { name: 'Kubernetes CA Certificate' }).fill('-----NEW CERT-----');
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(
page.locator('.info-table-row').filter({
hasText: 'Kubernetes host https://127.0.0.1:8443',
})
).toBeVisible();
await expect(
page.locator('.info-table-row').filter({
hasText: 'Certificate PEM Format -----NEW CERT-----',
})
).toBeVisible();
await page.getByRole('link', { name: 'Exit configuration' }).click();
await basePage.dismissFlashMessages();
});
await test.step('create kubernetes role', async () => {
await page.getByRole('link', { name: 'Roles' }).click();
await expect(page.locator('section')).toContainText(
@ -206,4 +184,67 @@ test('kubernetes secrets workflow', async ({ page }) => {
'This will generate credentials using the role test-role.'
);
});
await test.step('clean up and disable engine', async () => {
await basePage.disableEngine('kubernetes');
});
});
test('kubernetes tune workflow', async ({ page }) => {
const basePage = new BasePage(page);
const configurationSettingsPage = new ConfigurationSettingsPage(page);
const path = 'kubernetes-tune';
const engineType = 'kubernetes';
await test.step('enable kubernetes secrets engine mount', async () => {
await basePage.enableEngine(engineType, path);
});
await test.step('navigate to configuration page from manage dropdown and ensure plugin settings tab is active', async () => {
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.assertPluginSettingsTabActive(engineType);
});
await test.step('configure kubernetes plugin settings', async () => {
await page.locator('div').filter({ hasText: 'Manual configuration Generate' }).nth(4).click();
await page.getByRole('textbox', { name: 'Kubernetes host' }).fill('https://192.168.99.100:8443');
await page.getByRole('textbox', { name: 'Service account JWT' }).fill('test-jwt');
await page.getByRole('textbox', { name: 'Kubernetes CA Certificate' }).fill('-----CERTIFICATE-----');
await page.getByRole('button', { name: 'Save' }).click();
});
await test.step('ensure kubernetes plugin settings was saved', async () => {
await expect(page.locator('section')).toContainText('Kubernetes host https://192.168.99.100:8443');
});
await test.step('edit plugin settings configuration', async () => {
await page.getByRole('link', { name: 'Edit configuration' }).click();
await page.getByRole('textbox', { name: 'Kubernetes host' }).fill('https://192.168.99.100:8448');
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.locator('section')).toContainText('Kubernetes host https://192.168.99.100:8448');
});
await test.step('navigate and verify general settings form', async () => {
await configurationSettingsPage.navigateToGeneralSettings(engineType);
await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType);
await page.getByRole('link', { name: 'Exit configuration' }).click();
});
await test.step('ensure that we navigate back to the kubernetes overview page when Exit configuration is clicked', async () => {
await expect(page.getByText(`Vault Secrets engines ${path} ${path} Kubernetes Manage`)).toBeVisible();
});
await test.step('verify unsaved changes modal works in general settings', async () => {
// Navigate back to general settings
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.navigateToGeneralSettings(engineType);
// Test Unsaved changes modal
await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path);
});
await test.step('clean up and disable engine', async () => {
await basePage.disableEngine(path);
});
});

View file

@ -5,6 +5,7 @@
import { test, expect } from '@playwright/test';
import { BasePage } from '../../pages/base';
import { ConfigurationSettingsPage } from '../../pages/configuration-settings';
test('kvv2 workflow', async ({ page }) => {
const basePage = new BasePage(page);
@ -134,3 +135,57 @@ test('kvv2 workflow', async ({ page }) => {
await page.getByRole('link', { name: 'View policy' }).click();
await expect(page.getByRole('heading', { name: 'foo-policy' })).toBeVisible();
});
test('kvv2 tune workflow', async ({ page }) => {
const basePage = new BasePage(page);
const configurationSettingsPage = new ConfigurationSettingsPage(page);
const path = 'kv-tune';
const engineType = 'kv';
await test.step('enable kvv2 secrets engine mount', async () => {
await basePage.enableEngine(engineType, path);
});
await test.step('navigate to configuration page from manage dropdown and ensure plugin settings tab is active', async () => {
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.assertPluginSettingsTabActive(engineType);
});
await test.step('edit plugin settings configuration', async () => {
await page.getByRole('link', { name: 'Edit configuration' }).click();
await page.getByRole('link', { name: 'Edit configuration' }).click();
await page.locator('#max_versions').fill('2');
await page.locator('#label-cas_required').check();
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Require check and set Yes')).toBeVisible();
});
await test.step('navigate and verify general settings form', async () => {
await configurationSettingsPage.navigateToGeneralSettings(engineType);
await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType);
await page.getByRole('link', { name: 'Exit configuration' }).click();
});
await test.step('ensure that we navigate back to the kv overview page when Exit configuration is clicked', async () => {
await expect(
page
.locator('div')
.filter({ hasText: `${path} version 2 KV Manage` })
.nth(3)
).toBeVisible();
});
await test.step('verify unsaved changes modal works in general settings', async () => {
// Navigate back to general settings
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.navigateToGeneralSettings(engineType);
// Test Unsaved changes modal
await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path);
});
await test.step('clean up and disable engine', async () => {
await basePage.disableEngine(path);
});
});

View file

@ -5,6 +5,7 @@
import { test, expect } from '@playwright/test';
import { BasePage } from '../../pages/base';
import { ConfigurationSettingsPage } from '../../pages/configuration-settings';
test('pki workflow', async ({ page }) => {
const basePage = new BasePage(page);
@ -144,3 +145,78 @@ test('pki workflow', async ({ page }) => {
await page.getByText('Type to find an issuer...').click();
await expect(page.getByRole('option').first()).toBeVisible();
});
test('pki tune workflow', async ({ page }) => {
const basePage = new BasePage(page);
const configurationSettingsPage = new ConfigurationSettingsPage(page);
const path = 'pki-tune';
const engineType = 'pki';
await test.step('enable pki secrets engine mount', async () => {
await basePage.enableEngine(engineType, path, {
defaultLeaseTtl: { unit: 5, option: 'm' },
maxLeaseTtl: { unit: 10, option: 'm' },
});
});
await test.step('navigate to configuration page from manage dropdown and ensure plugin settings tab is active', async () => {
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.assertPluginSettingsTabActive(engineType);
});
await test.step('configure pki plugin settings', async () => {
await page.locator('label').filter({ hasText: 'Generate root Generates a new' }).click();
await page.getByLabel('Type').selectOption('exported');
await page.getByRole('textbox', { name: 'Common name' }).click();
await page.getByRole('textbox', { name: 'Common name' }).fill('common-name-1');
await page.getByRole('textbox', { name: 'Issuer name' }).click();
await page.getByRole('textbox', { name: 'Issuer name' }).fill('issue-name-1');
await page.getByRole('button', { name: 'Done' }).click();
});
await test.step('ensure pki plugin settings was saved', async () => {
await expect(page.getByLabel('Next steps')).toContainText(
'Next steps The private_key is only available once. Make sure you copy and save it now.'
);
await expect(page.locator('section')).toContainText('Common name common-name-1');
await expect(page.locator('section')).toContainText('Issuer name issue-name-1');
await page.getByRole('button', { name: 'Done' }).click();
});
// Navigate back to plugin settings page
await configurationSettingsPage.navigateToConfiguration(path);
await test.step('edit plugin settings configuration', async () => {
await page.getByRole('link', { name: 'Edit configuration' }).click();
await expect(page.locator('form')).toContainText('Cluster Config');
await expect(page.getByText("Mount's API path Specifies")).toBeVisible();
await page.getByRole('button', { name: 'Cancel' }).click();
});
await test.step('navigate and verify general settings form', async () => {
// General settings
await configurationSettingsPage.navigateToGeneralSettings(engineType);
await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType);
await page.getByRole('link', { name: 'Exit configuration' }).click();
});
await test.step('ensure that we navigate back to the pki overview page when Exit configuration is clicked', async () => {
await expect(
page.getByText(`Vault Secrets engines ${path} ${path} Generate policy Manage`)
).toBeVisible();
});
await test.step('verify unsaved changes modal works in general settings', async () => {
// Navigate back to general settings
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.navigateToGeneralSettings(engineType);
// Test Unsaved changes modal
await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path);
});
await test.step('clean up and disable engine', async () => {
await basePage.disableEngine(path);
});
});

View file

@ -5,6 +5,7 @@
import { expect, test } from '@playwright/test';
import { BasePage } from '../../pages/base';
import { ConfigurationSettingsPage } from '../../pages/configuration-settings';
test('transform workflow', async ({ page }) => {
const basePage = new BasePage(page);
@ -91,3 +92,49 @@ test('transform workflow', async ({ page }) => {
await page.getByRole('button', { name: 'Dismiss' }).click();
});
});
test('transform tune workflow', async ({ page }) => {
const basePage = new BasePage(page);
const configurationSettingsPage = new ConfigurationSettingsPage(page);
const path = 'transform-tune';
const engineType = 'transform';
await test.step('enable transform secrets engine mount', async () => {
await basePage.enableEngine(engineType, path);
});
await test.step('navigate to configuration page from manage dropdown ', async () => {
await configurationSettingsPage.navigateToConfiguration(path);
});
// transform does not have plugin settings, so we only need to test for general settings
await test.step('navigate and verify general settings form', async () => {
await configurationSettingsPage.navigateToGeneralSettings(engineType);
await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType);
await page.getByRole('link', { name: 'Exit configuration' }).click();
});
await test.step('ensure that we navigate back to the transform overview page when Exit configuration is clicked', async () => {
await expect(
page
.locator('div')
.filter({ hasText: `${path} Manage Create transformation` })
.nth(3)
).toBeVisible();
});
await test.step('verify unsaved changes modal works in general settings', async () => {
// Navigate back to general settings
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.navigateToGeneralSettings(engineType);
// Test Unsaved changes modal
await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path);
});
await test.step('clean up and disable engine', async () => {
await basePage.disableEngine(path);
});
});

View file

@ -5,6 +5,7 @@
import { test, expect } from '@playwright/test';
import { BasePage } from '../../pages/base';
import { ConfigurationSettingsPage } from '../../pages/configuration-settings';
test('transit workflow', async ({ page }) => {
const basePage = new BasePage(page);
@ -82,3 +83,49 @@ test('transit workflow', async ({ page }) => {
await expect(page.getByText('Results Valid')).toBeVisible();
await page.getByRole('button', { name: 'Close' }).click();
});
test('transit tune workflow', async ({ page }) => {
const basePage = new BasePage(page);
const configurationSettingsPage = new ConfigurationSettingsPage(page);
const path = 'transit-tune';
const engineType = 'transit';
await test.step('enable Transit secrets engine mount', async () => {
await basePage.enableEngine(engineType, path);
});
await test.step('navigate to configuration page from manage dropdown ', async () => {
await configurationSettingsPage.navigateToConfiguration(path);
});
// Transit does not have plugin settings, so we only need to test for general settings
await test.step('navigate and verify general settings form', async () => {
await configurationSettingsPage.navigateToGeneralSettings(engineType);
await configurationSettingsPage.editAndVerifyGeneralSettings(path, engineType);
await page.getByRole('link', { name: 'Exit configuration' }).click();
});
await test.step('ensure that we navigate back to the transit overview page when Exit configuration is clicked', async () => {
await expect(
page
.locator('div')
.filter({ hasText: `${path} Manage Create key` })
.nth(3)
).toBeVisible();
});
await test.step('verify unsaved changes modal works in general settings', async () => {
// Navigate back to general settings
await configurationSettingsPage.navigateToConfiguration(path);
await configurationSettingsPage.navigateToGeneralSettings(engineType);
// Test Unsaved changes modal
await configurationSettingsPage.verifyUnsavedChangesModalOnNavigateAway(path);
});
await test.step('clean up and disable engine', async () => {
await basePage.disableEngine(path);
});
});