ClusterRoleBinding
diff --git a/ui/mirage/handlers/base.js b/ui/mirage/handlers/base.js
index 99e5eea7db..f057dea4bb 100644
--- a/ui/mirage/handlers/base.js
+++ b/ui/mirage/handlers/base.js
@@ -94,12 +94,4 @@ export default function (server) {
},
};
});
-
- server.get('sys/internal/ui/namespaces', function () {
- return {
- data: {
- keys: ['ns1/', 'ns2/', 'ns3/', 'ns4/', 'ns5/', 'ns6/', 'ns7/', 'ns8/', 'ns9/', 'ns10/'],
- },
- };
- });
}
diff --git a/ui/tests/acceptance/access/namespaces/index-test.js b/ui/tests/acceptance/access/namespaces/index-test.js
index 854cb6f00a..a02a51b59f 100644
--- a/ui/tests/acceptance/access/namespaces/index-test.js
+++ b/ui/tests/acceptance/access/namespaces/index-test.js
@@ -3,27 +3,27 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import { currentRouteName, visit, click, fillIn, currentURL } from '@ember/test-helpers';
+import { currentRouteName, visit, click, fillIn, currentURL, findAll } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
-import { setupMirage } from 'ember-cli-mirage/test-support';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import { createNSFromPaths, deleteNSFromPaths } from 'vault/tests/helpers/commands';
+import { NAMESPACE_PICKER_SELECTORS } from 'vault/tests/helpers/namespace-picker';
module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
setupApplicationTest(hooks);
- setupMirage(hooks);
- const searchInput = GENERAL.filterInputExplicit;
- const searchButton = GENERAL.filterInputExplicitSearch;
-
- hooks.beforeEach(function () {
- return login();
+ hooks.beforeEach(async () => {
+ await login();
});
test('it navigates to namespaces page', async function (assert) {
assert.expect(1);
+
+ // Go to the manage namespaces page
await visit('/vault/access/namespaces');
+
assert.strictEqual(
currentRouteName(),
'vault.cluster.access.namespaces.index',
@@ -32,52 +32,159 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
});
test('it displays the breadcrumb trail', async function (assert) {
+ // Go to the manage namespaces page
await visit('/vault/access/namespaces');
+
assert.dom(GENERAL.breadcrumb).exists({ count: 1 }, 'Only one breadcrumb is displayed');
assert.dom(GENERAL.breadcrumb).hasText('Namespaces', 'Breadcrumb trail is displayed correctly');
});
test('it should render correct number of namespaces', async function (assert) {
+ // Setup: Create namespace(s) via the CLI
+ const namespaces = [
+ 'ns1',
+ 'ns2',
+ 'ns3',
+ 'ns4',
+ 'ns5',
+ 'ns6',
+ 'ns7',
+ 'ns8',
+ 'ns9',
+ 'ns10',
+ 'ns11',
+ 'ns12',
+ 'ns13',
+ 'ns14',
+ 'ns15',
+ 'ns16',
+ 'ns17',
+ 'ns18',
+ ];
+ await createNSFromPaths(namespaces);
+
assert.expect(3);
+
+ // Go to the manage namespaces page
await visit('/vault/access/namespaces');
+
const store = this.owner.lookup('service:store');
+
// Default page size is 15
assert.strictEqual(store.peekAll('namespace').length, 15, 'Store has 15 namespaces records');
assert.dom('.list-item-row').exists({ count: 15 }, 'Should display 15 namespaces');
assert.dom('.hds-pagination').exists();
+
+ // Cleanup: Delete namespace(s) via the CLI
+ await deleteNSFromPaths(namespaces);
});
test('it should show button to refresh namespace list', async function (assert) {
- let refreshNetworkRequestTriggered;
- const refreshNamespaceButton = GENERAL.button('refresh-namespace-list');
+ const testNS = 'test-refresh-ns';
- this.server.get('/sys/internal/ui/namespaces', () => {
- refreshNetworkRequestTriggered = true;
- return;
- });
+ // Setup: Create namespace via the CLI
+ const namespaces = [testNS];
+ await createNSFromPaths(namespaces);
+ // Go to the manage namespaces page
await visit('/vault/access/namespaces');
- assert.dom(refreshNamespaceButton).hasText('Refresh list', 'Refresh button is rendered correctly');
+ // Open the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
- refreshNetworkRequestTriggered = false;
- await click(refreshNamespaceButton);
- assert.true(
- refreshNetworkRequestTriggered,
- 'Get namespaces network request was made when refresh button was clicked'
+ // Verify the search input field exists
+ assert.dom(NAMESPACE_PICKER_SELECTORS.searchInput).exists('The namespace search field exists');
+
+ // Verify 0 namespaces are displayed after searching for "test-refresh-ns"
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, testNS);
+ assert.strictEqual(
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 0,
+ `No namespaces are displayed after searching for "${testNS}"`
);
+
+ // Close the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Click the refresh list button
+ assert
+ .dom(GENERAL.button('refresh-namespace-list'))
+ .hasText('Refresh list', 'Refresh button is rendered correctly');
+ await click(GENERAL.button('refresh-namespace-list'));
+
+ // Open the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Verify the search input field exists
+ assert.dom(NAMESPACE_PICKER_SELECTORS.searchInput).exists('The namespace search field exists');
+
+ // Verify 1 namespace is displayed after searching for "test-refresh-ns"
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, testNS);
+ assert.strictEqual(
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 1,
+ `1 namespace is displayed after searching for "${testNS}"`
+ );
+
+ // Close the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Cleanup: Delete namespace via the CLI
+ await deleteNSFromPaths(namespaces);
+
+ // Go to the manage namespaces page
+ await visit('/vault/access/namespaces');
+
+ // Open the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Verify the search input field exists
+ assert.dom(NAMESPACE_PICKER_SELECTORS.searchInput).exists('The namespace search field exists');
+
+ // Verify 1 namespace is displayed after searching for "test-refresh-ns"
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, testNS);
+ assert.strictEqual(
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 1,
+ `1 namespace is displayed after searching for "${testNS}"`
+ );
+
+ // Close the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Click the refresh list button
+ assert
+ .dom(GENERAL.button('refresh-namespace-list'))
+ .hasText('Refresh list', 'Refresh button is rendered correctly');
+ await click(GENERAL.button('refresh-namespace-list'));
+
+ // Open the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Verify the search input field exists
+ assert.dom(NAMESPACE_PICKER_SELECTORS.searchInput).exists('The namespace search field exists');
+
+ // Verify 0 namespaces are displayed after searching for "test-refresh-ns"
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, testNS);
+ assert.strictEqual(
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 0,
+ `No namespaces are displayed after searching for "${testNS}"`
+ );
+
+ // Close the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
});
test('it should show button to create new namespace', async function (assert) {
- const createNamespaceLink = GENERAL.linkTo('create-namespace');
-
+ // Go to the manage namespaces page
await visit('/vault/access/namespaces');
assert
- .dom(createNamespaceLink)
+ .dom(GENERAL.linkTo('create-namespace'))
.hasText('Create namespace', 'Create namespace button is rendered correctly');
assert
- .dom(createNamespaceLink)
+ .dom(GENERAL.linkTo('create-namespace'))
.hasAttribute(
'href',
'/ui/vault/access/namespaces/create',
@@ -85,44 +192,143 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
);
});
+ test('it should update namespace list after create/delete without manual refresh', async function (assert) {
+ const testNS = 'test-create-ns';
+
+ // Go to the manage namespaces page
+ await visit('/vault/access/namespaces');
+
+ // Verify test-create-ns does not exist in the Manage Namespace page
+ await fillIn(GENERAL.filterInputExplicit, testNS);
+ await click(GENERAL.filterInputExplicitSearch);
+ assert.dom('.list-item-row').exists({ count: 0 }, `"${testNS}" namespace is not displayed on the page`);
+
+ // Verify test-create-ns does not exist in the Namespace Picker
+ await click(GENERAL.toggleInput('namespace-id'));
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, testNS);
+ assert.strictEqual(
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 0,
+ `"${testNS}" is not displayed in the namespace picker`
+ );
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Create a new namespace
+ assert
+ .dom(GENERAL.linkTo('create-namespace'))
+ .hasText('Create namespace', 'Create namespace button is displayed');
+ await click(GENERAL.linkTo('create-namespace'));
+ assert.dom(GENERAL.inputByAttr('path')).exists('Create namespace input field is displayed');
+ await fillIn(GENERAL.inputByAttr('path'), testNS);
+ assert.dom('[data-test-edit-form-submit]').exists('Save button is displayed');
+ await click('[data-test-edit-form-submit]');
+
+ // Verify test-create-ns does not exist in the Manage Namespace page
+ await fillIn(GENERAL.filterInputExplicit, testNS);
+ await click(GENERAL.filterInputExplicitSearch);
+ assert.dom('.list-item-row').exists({ count: 1 }, `"${testNS}" namespace is displayed on the page`);
+
+ // Verify test-create-ns exists in the Namespace Picker without refresh
+ await click(GENERAL.toggleInput('namespace-id'));
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, testNS);
+ assert.strictEqual(
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 1,
+ `"${testNS}" is displayed in the namespace picker`
+ );
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Delete the created namespace
+ assert.dom(GENERAL.menuTrigger).exists('Namespace options menu is displayed');
+ await click(GENERAL.menuTrigger);
+ assert
+ .dom('.hds-dropdown-list-item:nth-of-type(2)')
+ .hasText('Delete', 'Delete namespace option is displayed');
+ await click('.hds-dropdown-list-item:nth-of-type(2) button');
+ assert.dom(GENERAL.confirmButton).hasText('Confirm', 'Confirm namespace deletion button is shown');
+ await click(GENERAL.confirmButton);
+
+ // Verify test-create-ns does not exist in the Manage Namespace page
+ await fillIn(GENERAL.filterInputExplicit, testNS);
+ await click(GENERAL.filterInputExplicitSearch);
+ assert.dom('.list-item-row').exists({ count: 0 }, `"${testNS}" namespace is not displayed on the page`);
+
+ // Verify test-create-ns does not exist in the Namespace Picker
+ await click(GENERAL.toggleInput('namespace-id'));
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, testNS);
+ assert.strictEqual(
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 0,
+ `"${testNS}" is not displayed in the namespace picker`
+ );
+ await click(GENERAL.toggleInput('namespace-id'));
+ });
+
test('it should filter namespaces based on search input', async function (assert) {
+ // Setup: Create namespace(s) via the CLI
+ const namespaces = ['parent', 'other-parent'];
+ await createNSFromPaths(namespaces);
+
+ // Go to the manage namespaces page
await visit('/vault/access/namespaces');
// Enter search text
- await fillIn(searchInput, 'ns4');
- assert.dom(searchInput).hasValue('ns4', 'Search input contains the entered text');
+ await fillIn(GENERAL.filterInputExplicit, 'other');
+ assert.dom(GENERAL.filterInputExplicit).hasValue('other', 'Search input contains the entered text');
// Click the search button
- await click(searchButton);
+ await click(GENERAL.filterInputExplicitSearch);
// Verify the filtered results
assert.dom('.list-item-row').exists({ count: 1 }, 'Filtered results are displayed correctly');
- assert.dom('.list-item-row').hasText('ns4', 'Correct namespace is displayed in the filtered results');
+ assert
+ .dom('.list-item-row')
+ .hasText('other-parent', 'Correct namespace is displayed in the filtered results');
// Verify the URL query param is updated
assert.strictEqual(
currentURL(),
- '/vault/access/namespaces?page=1&pageFilter=ns4',
+ '/vault/access/namespaces?page=1&pageFilter=other',
'URL query param is updated to reflect the search field as pageFilter'
);
// Clear the search input
- await fillIn(searchInput, '');
- await click(searchButton);
- assert.dom(searchInput).hasValue('', 'Search input is cleared');
+ await fillIn(GENERAL.filterInputExplicit, '');
+ await click(GENERAL.filterInputExplicitSearch);
+
+ assert.dom(GENERAL.filterInputExplicit).hasValue('', 'Search input is cleared');
assert
.dom('.list-item-row')
- .exists({ count: 15 }, 'All namespaces are displayed after clearing the search input');
+ .exists({ count: 2 }, 'All namespaces are displayed after clearing the search input');
assert.strictEqual(
currentURL(),
'/vault/access/namespaces?page=1',
'URL query param is updated to remove pageFilter'
);
+
+ // Cleanup: Delete namespace(s) via the CLI
+ await deleteNSFromPaths(namespaces);
});
test('it should show options menu for each namespace', async function (assert) {
+ // Setup: Create namespace(s) via the CLI
+ const namespace = 'asdf';
+ await createNSFromPaths([namespace]);
+
+ // Go to the manage namespaces page
await visit('/vault/access/namespaces');
- assert.dom(GENERAL.menuTrigger).exists();
+
+ // Hack: Trigger refresh internal namespaces endpoint
+ await click(GENERAL.toggleInput('namespace-id'));
+ await click(GENERAL.button('Refresh list'));
+
+ // Enter search text
+ await fillIn(GENERAL.filterInputExplicit, namespace);
+ await click(GENERAL.filterInputExplicitSearch);
+
+ await click(GENERAL.button('refresh-namespace-list'));
+
+ assert.dom(GENERAL.menuTrigger).exists('Namespace options menu is displayed');
await click(GENERAL.menuTrigger);
assert.dom('.hds-dropdown-list-item').exists({ count: 2 }, 'Should display 2 options in the menu.');
@@ -135,30 +341,16 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
.dom(`${switchNamespaceButton} a`)
.hasAttribute(
'href',
- 'http://localhost:7357/ui/vault/dashboard?namespace=ns1',
+ `http://localhost:7357/ui/vault/dashboard?namespace=${namespace}`,
'Switch namespace button has the correct href attribute'
);
// Verify that the user can delete the namespace
- const deleteNamespaceButton = '.hds-dropdown-list-item:nth-of-type(2)';
- assert.dom(deleteNamespaceButton).hasText('Delete', 'Allow users to delete the namespace');
- });
+ assert
+ .dom('.hds-dropdown-list-item:nth-of-type(2)')
+ .hasText('Delete', 'Delete namespace option is displayed');
- test('it should hide the switch to namespace option for unaccessible namespaces', async function (assert) {
- await visit('/vault/access/namespaces');
-
- // Search for a namespace that is not accessible
- await fillIn(searchInput, 'ns12');
- await click(searchButton);
-
- assert.dom(GENERAL.menuTrigger).exists();
- await click(GENERAL.menuTrigger);
-
- // Verify that only the delete option is available for the unaccessible namespace
- assert.dom('.hds-dropdown-list-item').exists({ count: 1 }, 'Should display 1 option in the menu.');
-
- // Verify that the user can delete the namespace
- const deleteNamespaceButton = '.hds-dropdown-list-item:nth-of-type(1)';
- assert.dom(deleteNamespaceButton).hasText('Delete', 'Allow users to delete the namespace');
+ // Cleanup: Delete namespace(s) via the CLI
+ await deleteNSFromPaths([namespace]);
});
});
diff --git a/ui/tests/acceptance/enterprise-namespaces-test.js b/ui/tests/acceptance/enterprise-namespaces-test.js
index 034255c939..e5194559ff 100644
--- a/ui/tests/acceptance/enterprise-namespaces-test.js
+++ b/ui/tests/acceptance/enterprise-namespaces-test.js
@@ -15,67 +15,21 @@ import {
} from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
-import { runCmd, createNS, deleteNS } from 'vault/tests/helpers/commands';
+import { runCmd, createNSFromPaths, deleteNSFromPaths } from 'vault/tests/helpers/commands';
import { login, loginNs, logout } from 'vault/tests/helpers/auth/auth-helpers';
import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { NAMESPACE_PICKER_SELECTORS } from '../helpers/namespace-picker';
-import sinon from 'sinon';
-
-async function createNamespaces(namespaces) {
- for (const ns of namespaces) {
- // Note: iterate through the namespace parts to create the full namespace path
- const parts = ns.split('/');
- let currentPath = '';
-
- for (const part of parts) {
- // Visit the parent namespace
- const url = `/vault/dashboard${currentPath && `?namespace=${currentPath.replaceAll('/', '%2F')}`}`;
- await visit(url);
-
- currentPath = currentPath ? `${currentPath}/${part}` : part;
-
- // Create the current namespace
- await runCmd(createNS(part), false);
- await settled();
- }
-
- // Reset to the root namespace
- const url = '/vault/dashboard';
- await visit(url);
- }
-}
-
-async function deleteNamespaces(namespaces) {
- // Reset to the root namespace
- const url = '/vault/dashboard';
- await visit(url);
-
- for (const ns of namespaces) {
- // Note: delete the parent namespace to delete all child namespaces
- const part = ns.split('/')[0];
- await runCmd(deleteNS(part), false);
- await settled();
- }
-}
-
module('Acceptance | Enterprise | namespaces', function (hooks) {
setupApplicationTest(hooks);
- let fetchSpy;
-
- hooks.beforeEach(() => {
- fetchSpy = sinon.spy(window, 'fetch');
- return login();
- });
-
- hooks.afterEach(() => {
- fetchSpy.restore();
+ hooks.beforeEach(async () => {
+ await login();
});
test('it focuses the search input field when user toggles namespace picker', async function (assert) {
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
// Verify that the search input field is focused
const searchInput = find(NAMESPACE_PICKER_SELECTORS.searchInput);
@@ -87,11 +41,11 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
});
test('it navigates to the matching namespace when Enter is pressed', async function (assert) {
- // Test Setup
+ // Setup: Create namespace(s) via the CLI
const namespaces = ['beep/boop'];
- await createNamespaces(namespaces);
+ await createNSFromPaths(namespaces);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
await click(GENERAL.button('Refresh list'));
assert.dom(NAMESPACE_PICKER_SELECTORS.searchInput).exists('The namespace search field exists');
@@ -112,16 +66,16 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
'Navigates to the correct namespace when Enter is pressed'
);
- // Test Cleanup
- await deleteNamespaces(namespaces);
+ // Cleanup: Delete namespace(s) via the CLI
+ await deleteNSFromPaths(namespaces);
});
test('it filters namespaces based on search input', async function (assert) {
- // Test Setup
+ // Setup: Create namespace(s) via the CLI
const namespaces = ['beep/boop/bop'];
- await createNamespaces(namespaces);
+ await createNSFromPaths(namespaces);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
await click(GENERAL.button('Refresh list'));
// Verify all namespaces are displayed initially
@@ -163,51 +117,85 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
'All namespaces are displayed after clearing search input'
);
- // Test Cleanup
- await deleteNamespaces(namespaces);
+ // Cleanup: Delete namespace(s) via the CLI
+ await deleteNSFromPaths(namespaces);
});
test('it updates the namespace list after clicking "Refresh list"', async function (assert) {
- // Test Setup
- const namespaces = ['beep'];
- await createNamespaces(namespaces);
+ // Open the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ // Verify the search input field exists
+ assert.dom(NAMESPACE_PICKER_SELECTORS.searchInput).exists('The namespace search field exists');
- // Verify that the namespace list was fetched on load
- let listNamespaceRequests = fetchSpy
- .getCalls()
- .filter((call) => call.args[0].includes('/v1/sys/internal/ui/namespaces'));
+ // Verify 0 namespaces are displayed after searching for "beep"
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, 'beep');
assert.strictEqual(
- listNamespaceRequests.length,
- 1,
- 'The network call to the specific endpoint was made twice (once on load, once on refresh)'
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 0,
+ 'No namespaces are displayed after searching for "beep"'
);
+ // Close the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Create 'beep' namespace via the CLI
+ const namespaces = ['beep'];
+ await createNSFromPaths(namespaces);
+
+ // Open the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
// Refresh the list of namespaces
assert.dom(GENERAL.button('Refresh list')).exists('Refresh list button exists');
await click(GENERAL.button('Refresh list'));
- // Verify that the namespace list was fetched on refresh
- listNamespaceRequests = fetchSpy
- .getCalls()
- .filter((call) => call.args[0].includes('/v1/sys/internal/ui/namespaces'));
+ // Verify the search input field exists
+ assert.dom(NAMESPACE_PICKER_SELECTORS.searchInput).exists('The namespace search field exists');
+
+ // Verify 1 namespace is displayed after searching for "beep"
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, 'beep');
assert.strictEqual(
- listNamespaceRequests.length,
- 2,
- 'The network call to the specific endpoint was made twice (once on load, once on refresh)'
+ findAll(NAMESPACE_PICKER_SELECTORS.link('beep')).length,
+ 1,
+ '1 namespace is displayed after searching for "beep"'
);
- // Test Cleanup
- await deleteNamespaces(namespaces);
+ // Close the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Delete the 'beep' namespace via the CLI
+ await deleteNSFromPaths(namespaces);
+
+ // Open the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Refresh the list of namespaces
+ assert.dom(GENERAL.button('Refresh list')).exists('Refresh list button exists');
+ await click(GENERAL.button('Refresh list'));
+
+ // Verify the search input field exists
+ assert.dom(NAMESPACE_PICKER_SELECTORS.searchInput).exists('The namespace search field exists');
+
+ // Verify 0 namespaces are displayed after searching for "beep"
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, 'beep');
+ assert.strictEqual(
+ findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
+ 0,
+ 'No namespaces are displayed after searching for "beep"'
+ );
+
+ // Close the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
});
test('it displays the "Manage" button with the correct URL', async function (assert) {
- // Test Setup
+ // Setup: Create namespace(s) via the CLI
const namespaces = ['beep'];
- await createNamespaces(namespaces);
+ await createNSFromPaths(namespaces);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ // Open the namespace picker & refresh the list of namespaces
+ await click(GENERAL.toggleInput('namespace-id'));
await click(GENERAL.button('Refresh list'));
// Verify the "Manage" button is rendered and has the correct URL
@@ -215,37 +203,54 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
.dom('[href="/ui/vault/access/namespaces"]')
.exists('The "Manage" button is displayed with the correct URL');
- // Test Cleanup
- await deleteNamespaces(namespaces);
+ // Cleanup: Delete namespace(s) via the CLI
+ await deleteNSFromPaths(namespaces);
});
// This test originated from this PR: https://github.com/hashicorp/vault/pull/7186
test('it clears namespaces when you log out', async function (assert) {
// Test Setup
- const namespaces = ['foo'];
- await createNamespaces(namespaces);
+ const namespace = 'foo';
+ await createNSFromPaths([namespace]);
- const ns = 'foo';
- await runCmd(createNS(ns), false);
const token = await runCmd(`write -field=client_token auth/token/create policies=default`);
await login(token);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+
+ // Open the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Verify that the root namespace is selected by default
assert.dom(NAMESPACE_PICKER_SELECTORS.link()).hasText('root', 'root renders as current namespace');
assert
.dom(`${NAMESPACE_PICKER_SELECTORS.link()} svg${GENERAL.icon('check')}`)
.exists('The root namespace is selected');
- // Test Cleanup
- await deleteNamespaces(namespaces);
+ // Verify that the foo namespace does not exist in the namespace picker
+ assert
+ .dom(NAMESPACE_PICKER_SELECTORS.link(namespace))
+ .exists({ count: 0 }, 'foo should not exist in the namespace picker');
+
+ // Logout and log back into root
+ await logout();
+ await login();
+
+ // Open the namespace picker & verify that the foo namespace does exist
+ await click(GENERAL.toggleInput('namespace-id'));
+ assert
+ .dom(NAMESPACE_PICKER_SELECTORS.link(namespace))
+ .exists({ count: 1 }, 'foo should exist in the namespace picker');
+
+ // Cleanup: Delete namespace(s) via the CLI
+ await deleteNSFromPaths([namespace]);
});
// This test originated from this PR: https://github.com/hashicorp/vault/pull/7186
test('it displays namespaces whether you log in with a namespace prefixed with / or not', async function (assert) {
- // Test Setup
+ // Setup: Create namespace(s) via the CLI
const namespaces = ['beep/boop/bop'];
- await createNamespaces(namespaces);
+ await createNSFromPaths(namespaces);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
await click(GENERAL.button('Refresh list'));
// Login with a namespace prefixed with /
@@ -253,11 +258,11 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
await settled();
assert
- .dom(NAMESPACE_PICKER_SELECTORS.toggle)
+ .dom(GENERAL.toggleInput('namespace-id'))
.hasText('boop', `shows the namespace 'boop' in the toggle component`);
// Open the namespace picker & wait for it to render
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
assert.dom(`svg${GENERAL.icon('check')}`).exists('The check icon is rendered');
// Find the selected element with the check icon & ensure it exists
@@ -275,8 +280,8 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
'The current namespace does not begin or end with /'
);
- // Test Cleanup
- await deleteNamespaces(namespaces);
+ // Cleanup: Delete namespace(s) via the CLI
+ await deleteNSFromPaths(namespaces);
});
test('it shows the regular namespace toolbar when not managed', async function (assert) {
@@ -295,37 +300,56 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
});
test('it should allow the user to delete a namespace', async function (assert) {
- // Test Setup
- const namespaces = ['test-delete-me'];
- await createNamespaces(namespaces);
+ // Setup: Create namespace(s) via the CLI
+ const namespace = 'test-delete-me';
+ await createNSFromPaths([namespace]);
await visit('/vault/access/namespaces');
- const searchInput = GENERAL.filterInputExplicit;
- const searchButton = GENERAL.filterInputExplicitSearch;
+ // Verify that the namespace exists in the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+ await click(GENERAL.button('Refresh list'));
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, namespace);
- await fillIn(searchInput, 'test-delete-me');
- await click(searchButton);
+ assert
+ .dom(NAMESPACE_PICKER_SELECTORS.link(namespace))
+ .exists({ count: 1 }, 'Namespace exists in the namespace picker');
+
+ // Close the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+
+ // Verify that the namespace exists in the manage namespaces page
+ await fillIn(GENERAL.filterInputExplicit, namespace);
+ await click(GENERAL.filterInputExplicitSearch);
assert.dom(GENERAL.menuTrigger).exists();
await click(GENERAL.menuTrigger);
- // Verify that the user can delete the namespace
- const deleteNamespaceButton = '.hds-dropdown-list-item:nth-of-type(1)';
- assert.dom(deleteNamespaceButton).hasText('Delete', 'Allow users to delete the namespace');
+ // Delete the namespace
+ const deleteNamespaceButton = '.hds-dropdown-list-item:nth-of-type(2)';
+ assert.dom(deleteNamespaceButton).hasText('Delete', 'Delete namespace button exists');
await click(`${deleteNamespaceButton} button`);
- assert.dom(GENERAL.confirmButton).hasText('Confirm', 'Allow users to delete the namespace');
+ assert.dom(GENERAL.confirmButton).hasText('Confirm', 'Confirm namespace deletion button is shown');
await click(GENERAL.confirmButton);
+ // Verify that the namespace does not exist in the nmanage namespace page
assert.strictEqual(
currentURL(),
- '/vault/access/namespaces?page=1&pageFilter=test-delete-me',
+ `/vault/access/namespaces?page=1&pageFilter=${namespace}`,
'Should remain on the manage namespaces page after deletion'
);
assert
.dom('.list-item-row')
.exists({ count: 0 }, 'Namespace should be deleted and not displayed in the list');
+
+ // Verify that the namespace does not exist in the namespace picker
+ await click(GENERAL.toggleInput('namespace-id'));
+ await click(GENERAL.button('Refresh list'));
+ await fillIn(NAMESPACE_PICKER_SELECTORS.searchInput, namespace);
+ assert
+ .dom(NAMESPACE_PICKER_SELECTORS.link())
+ .exists({ count: 0 }, 'Deleted namespace does not exist in the namespace picker');
});
});
diff --git a/ui/tests/acceptance/enterprise-replication-test.js b/ui/tests/acceptance/enterprise-replication-test.js
index 7282b0ba00..51b5afcbe1 100644
--- a/ui/tests/acceptance/enterprise-replication-test.js
+++ b/ui/tests/acceptance/enterprise-replication-test.js
@@ -23,6 +23,7 @@ import { create } from 'ember-cli-page-object';
import flashMessage from 'vault/tests/pages/components/flash-message';
import ss from 'vault/tests/pages/components/search-select';
import { disableReplication } from 'vault/tests/helpers/replication';
+import { GENERAL } from '../helpers/general-selectors';
const searchSelect = create(ss);
const flash = create(flashMessage);
@@ -272,7 +273,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await click('[data-test-secondary-add]');
await fillIn('[data-test-replication-secondary-id]', secondaryNameSecond);
- await click('[data-test-toggle-input]');
+ await click(GENERAL.toggleInput('Time to Live (TTL) for generated secondary token'));
await fillIn('[data-test-ttl-value]', 3);
await click('[data-test-secondary-add]');
diff --git a/ui/tests/acceptance/secrets/backend/kubernetes/credentials-test.js b/ui/tests/acceptance/secrets/backend/kubernetes/credentials-test.js
index dcb523b60a..5fdf61afcd 100644
--- a/ui/tests/acceptance/secrets/backend/kubernetes/credentials-test.js
+++ b/ui/tests/acceptance/secrets/backend/kubernetes/credentials-test.js
@@ -10,6 +10,7 @@ import kubernetesScenario from 'vault/mirage/scenarios/kubernetes';
import kubernetesHandlers from 'vault/mirage/handlers/kubernetes';
import { login } from 'vault/tests/helpers/auth/auth-helpers';
import { fillIn, visit, click, currentRouteName } from '@ember/test-helpers';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Acceptance | kubernetes | credentials', function (hooks) {
setupApplicationTest(hooks);
@@ -66,8 +67,8 @@ module('Acceptance | kubernetes | credentials', function (hooks) {
};
});
await fillIn('[data-test-kubernetes-namespace]', 'kubernetes-test');
- await click('[data-test-toggle-input]');
- await click('[data-test-toggle-input="Time-to-Live (TTL)"]');
+ await click(GENERAL.toggleInput('kubernetes-clusterRoleBinding'));
+ await click(GENERAL.toggleInput('Time-to-Live (TTL)'));
await fillIn('[data-test-ttl-value="Time-to-Live (TTL)"]', 2);
await click('[data-test-generate-credentials-button]');
await click('[data-test-generate-credentials-done]');
diff --git a/ui/tests/helpers/commands.js b/ui/tests/helpers/commands.js
index a585b4d0ef..e1f1db346e 100644
--- a/ui/tests/helpers/commands.js
+++ b/ui/tests/helpers/commands.js
@@ -3,8 +3,9 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import { click, fillIn, findAll, triggerKeyEvent } from '@ember/test-helpers';
+import { click, fillIn, findAll, triggerKeyEvent, visit } from '@ember/test-helpers';
+// REPL selectors
const REPL = {
toggle: '[data-test-console-toggle]',
consoleInput: '[data-test-component="console/command-input"] input',
@@ -13,49 +14,33 @@ const REPL = {
/**
* Helper functions to run common commands in the consoleComponent during tests.
- * Please note that a user must be logged in during the test context for the commands to run.
- * By default runCmd throws an error if the last log includes "Error". To override this,
- * pass boolean false to run the commands and not throw errors
+ * Note: A user must be logged in during the test context for the commands to run.
*
* Example:
- *
* import { v4 as uuidv4 } from 'uuid';
* import { runCmd, mountEngineCmd } from 'vault/tests/helpers/commands';
*
- *
- * async function mountEngineExitOnError() {
- * const backend = `pki-${uuidv4()}`;
- * await runCmd(mountEngineCmd('pki', backend));
- * return backend;
- * }
- *
- * async function mountEngineSquashErrors() {
- * const backend = `pki-${uuidv4()}`;
- * await runCmd(mountEngineCmd('pki', backend), false);
- * return backend;
+ * async function mountEngine() {
+ * const backend = `pki-${uuidv4()}`;
+ * await runCmd(mountEngineCmd('pki', backend));
+ * return backend;
* }
*/
-/**
- * runCmd is used to run commands and throw an error if the output includes "Error"
- * @param {string || string[]} commands array of commands that should run
- * @param {boolean} throwErrors
- * @returns the last log output. Throws an error if it includes an error
- */
+// Command execution helpers
export const runCmd = async (commands, throwErrors = true) => {
- if (!commands) {
- throw new Error('runCmd requires commands array passed in');
- }
- if (!Array.isArray(commands)) {
- commands = [commands];
- }
+ if (!commands) throw new Error('runCmd requires commands array passed in');
+ if (!Array.isArray(commands)) commands = [commands];
+
await click(REPL.toggle);
await enterCommands(commands);
const lastOutput = await lastLogOutput();
await click(REPL.toggle);
+
if (throwErrors && lastOutput.includes('Error')) {
throw new Error(`Error occurred while running commands: "${commands.join('; ')}" - ${lastOutput}`);
}
+
return lastOutput;
};
@@ -69,54 +54,93 @@ export const enterCommands = async (commands) => {
export const lastLogOutput = async () => {
const items = findAll(REPL.logOutputItems);
- const count = items.length;
- if (count === 0) {
- // If no logOutput items are found, we can assume the response is empty
- return '';
- }
- const outputItemText = items[count - 1].innerText;
- return outputItemText;
+ if (!items.length) return '';
+ return items[items.length - 1].innerText;
};
-// Common commands
-export function mountEngineCmd(type, customName = '') {
+// Command builders
+export const mountEngineCmd = (type, customName = '') => {
const name = customName || type;
- if (type === 'kv-v2') {
- return `write sys/mounts/${name} type=kv options=version=2`;
- }
- return `write sys/mounts/${name} type=${type}`;
-}
+ return type === 'kv-v2'
+ ? `write sys/mounts/${name} type=kv options=version=2`
+ : `write sys/mounts/${name} type=${type}`;
+};
-export function deleteEngineCmd(name) {
- return `delete sys/mounts/${name}`;
-}
+export const deleteEngineCmd = (name) => `delete sys/mounts/${name}`;
-export function mountAuthCmd(type, customName = '') {
+export const mountAuthCmd = (type, customName = '') => {
const name = customName || type;
return `write sys/auth/${name} type=${type}`;
-}
-
-export function deleteAuthCmd(name) {
- return `delete sys/auth/${name}`;
-}
-
-export function createPolicyCmd(name, contents) {
- const policyContent = window.btoa(contents);
- return `write sys/policies/acl/${name} policy=${policyContent}`;
-}
-
-export function createTokenCmd(policyName = 'default') {
- return `write -field=client_token auth/token/create policies=${policyName} ttl=1h`;
-}
-
-export const tokenWithPolicyCmd = function (name, policy) {
- return [createPolicyCmd(name, policy), createTokenCmd(name)];
};
-export function createNS(namespace) {
- return `write sys/namespaces/${namespace} -f`;
-}
+export const deleteAuthCmd = (name) => `delete sys/auth/${name}`;
-export function deleteNS(namespace) {
- return `delete sys/namespaces/${namespace} -f`;
-}
+export const createPolicyCmd = (name, contents) => {
+ const policyContent = window.btoa(contents);
+ return `write sys/policies/acl/${name} policy=${policyContent}`;
+};
+
+export const createTokenCmd = (policyName = 'default') =>
+ `write -field=client_token auth/token/create policies=${policyName} ttl=1h`;
+
+export const tokenWithPolicyCmd = (name, policy) => [createPolicyCmd(name, policy), createTokenCmd(name)];
+
+export const createNS = (namespace) => `write sys/namespaces/${namespace} -f`;
+
+export const deleteNS = (namespace) => `delete sys/namespaces/${namespace} -f`;
+
+/**
+ * @description
+ * Iterates over an array of namespace paths and ensures each nested level is created.
+ * It visits the root namespace before attempting to create the next segment.
+ *
+ * @example input: ['foo/bar', 'baz/qux/quux']
+ * This will create: foo, foo/bar, baz, baz/qux, baz/qux/quux
+ *
+ * @param {string[]} namespaces - Array of strings of namespace paths (containing backslashes)
+ */
+export const createNSFromPaths = async (namespaces) => {
+ for (const ns of namespaces) {
+ const parts = ns.split('/');
+ let currentPath = '';
+
+ for (const part of parts) {
+ const url = `/vault/dashboard${currentPath && `?namespace=${currentPath.replaceAll('/', '%2F')}`}`;
+ await visit(url);
+
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
+ await runCmd(createNS(part), false);
+ }
+
+ // Reset to root namespace after creating each path
+ await visit('/vault/dashboard');
+ }
+};
+
+/**
+ * @description
+ * Deletes namespaces by removing each segment of the path from deepest to top.
+ *
+ * @example input: ['foo/bar', 'baz/qux']
+ * This will delete: foo, baz
+ *
+ * @param {string[]} namespaces - Array of strings of namespace paths (containing backslashes)
+ */
+export const deleteNSFromPaths = async (namespaces) => {
+ for (const ns of namespaces) {
+ const parts = ns.split('/');
+ // Work from deepest child up to the top-level namespace
+ for (let i = parts.length - 1; i >= 0; i--) {
+ const parentPath = parts.slice(0, i).join('/');
+ const toDelete = parts[i];
+ // Build the URL for the parent namespace (or root if none)
+ const url = parentPath
+ ? `/vault/dashboard?namespace=${parentPath.replaceAll('/', '%2F')}`
+ : '/vault/dashboard';
+ await visit(url);
+ await runCmd(deleteNS(toDelete), false);
+ }
+ // Reset to root namespace after deleting each path
+ await visit('/vault/dashboard');
+ }
+};
diff --git a/ui/tests/helpers/namespace-picker.js b/ui/tests/helpers/namespace-picker.js
index d69c54e1c6..bc41ed376e 100644
--- a/ui/tests/helpers/namespace-picker.js
+++ b/ui/tests/helpers/namespace-picker.js
@@ -5,6 +5,5 @@
export const NAMESPACE_PICKER_SELECTORS = {
link: (link) => (link ? `[data-test-namespace-link="${link}"]` : '[data-test-namespace-link]'),
- toggle: '[data-test-namespace-toggle]',
searchInput: 'input[type="search"]',
};
diff --git a/ui/tests/integration/components/form-field-test.js b/ui/tests/integration/components/form-field-test.js
index 691fc6b1b8..4a6877b950 100644
--- a/ui/tests/integration/components/form-field-test.js
+++ b/ui/tests/integration/components/form-field-test.js
@@ -178,7 +178,7 @@ module('Integration | Component | form field', function (hooks) {
})
);
assert.ok(component.hasToggleButton, 'renders a toggle button');
- assert.dom('[data-test-toggle-input]').isNotChecked();
+ assert.dom(GENERAL.toggleInput('toggle-foobar')).isNotChecked();
assert.dom('[data-test-toggle-subtext]').hasText('Toggled off');
await component.fields.objectAt(0).toggleButton();
diff --git a/ui/tests/integration/components/kubernetes/page/credentials-test.js b/ui/tests/integration/components/kubernetes/page/credentials-test.js
index 83c29f2093..d966ff00f3 100644
--- a/ui/tests/integration/components/kubernetes/page/credentials-test.js
+++ b/ui/tests/integration/components/kubernetes/page/credentials-test.js
@@ -109,9 +109,8 @@ module('Integration | Component | kubernetes | Page::Credentials', function (hoo
await this.renderComponent();
await fillIn('[data-test-kubernetes-namespace]', 'kubernetes-test');
assert.dom('[data-test-kubernetes-namespace]').hasValue('kubernetes-test', 'kubernetes-test');
-
- await click('[data-test-toggle-input]');
- await click('[data-test-toggle-input="Time-to-Live (TTL)"]');
+ await click(GENERAL.toggleInput('kubernetes-clusterRoleBinding'));
+ await click(GENERAL.toggleInput('Time-to-Live (TTL)'));
await fillIn('[data-test-ttl-value="Time-to-Live (TTL)"]', 2);
await click('[data-test-generate-credentials-button]');
diff --git a/ui/tests/integration/components/namespace-picker-test.js b/ui/tests/integration/components/namespace-picker-test.js
index 6b81eb626d..ba0798e016 100644
--- a/ui/tests/integration/components/namespace-picker-test.js
+++ b/ui/tests/integration/components/namespace-picker-test.js
@@ -62,7 +62,7 @@ module('Integration | Component | namespace-picker', function (hooks) {
test('it focuses the search input field when the component is loaded', async function (assert) {
await render(hbs``);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
// Verify that the search input field is focused
const searchInput = find(NAMESPACE_PICKER_SELECTORS.searchInput);
@@ -75,7 +75,7 @@ module('Integration | Component | namespace-picker', function (hooks) {
test('it filters namespace options based on search input', async function (assert) {
await render(hbs``);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
// Verify all namespaces are displayed initially
await waitFor(NAMESPACE_PICKER_SELECTORS.link());
@@ -117,7 +117,7 @@ module('Integration | Component | namespace-picker', function (hooks) {
});
await render(hbs``);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
// Verify that the "Refresh List" button is visible
assert.dom(GENERAL.button('Refresh list')).exists('Refresh List button is visible');
@@ -134,7 +134,7 @@ module('Integration | Component | namespace-picker', function (hooks) {
});
await render(hbs``);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
// Verify that the buttons are hidden
assert.dom(GENERAL.button('Refresh list')).doesNotExist('Refresh List button is hidden');
@@ -148,7 +148,7 @@ module('Integration | Component | namespace-picker', function (hooks) {
});
await render(hbs``);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
// Verify that the buttons are hidden
assert.dom(GENERAL.button('Refresh list')).doesNotExist('Refresh List button is hidden');
@@ -167,7 +167,7 @@ module('Integration | Component | namespace-picker', function (hooks) {
});
await render(hbs``);
- await click(NAMESPACE_PICKER_SELECTORS.toggle);
+ await click(GENERAL.toggleInput('namespace-id'));
// Dynamically modify the `findNamespacesForUser.perform` method for this test
const namespaceService = this.owner.lookup('service:namespace');
diff --git a/ui/tests/integration/components/sidebar/frame-test.js b/ui/tests/integration/components/sidebar/frame-test.js
index 70d7835875..caf9fab0a6 100644
--- a/ui/tests/integration/components/sidebar/frame-test.js
+++ b/ui/tests/integration/components/sidebar/frame-test.js
@@ -9,7 +9,7 @@ import { render, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { setRunOptions } from 'ember-a11y-testing/test-support';
-import { NAMESPACE_PICKER_SELECTORS } from 'vault/tests/helpers/namespace-picker';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | sidebar-frame', function (hooks) {
setupRenderingTest(hooks);
@@ -89,6 +89,6 @@ module('Integration | Component | sidebar-frame', function (hooks) {
`);
- assert.dom(NAMESPACE_PICKER_SELECTORS.toggle).exists('Namespace picker renders in sidebar footer');
+ assert.dom(GENERAL.toggleInput('namespace-id')).exists('Namespace picker renders in sidebar footer');
});
});