mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
[VAULT-35715] UI: Namespace picker is updated after add/delete namespace (#30737)
* [VAULT-35715] UI: Namespace picker is updated after add/delete namespace * + changelog * pairing session updates * fixing tests (wip) * fix tests! * remove meep lol * fix one more test * add missing test coverage * address PR comments, enterprise tests are passing again!! * fix lint issue
This commit is contained in:
parent
4c5876ec81
commit
b54aba9fd8
15 changed files with 529 additions and 273 deletions
3
changelog/30737.txt
Normal file
3
changelog/30737.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: Automatically refresh namespace list inside the namespace picker after creating or deleting a namespace in the UI.
|
||||
```
|
||||
|
|
@ -6,7 +6,13 @@
|
|||
<div class="namespace-picker side-padding-4" ...attributes>
|
||||
<Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
|
||||
|
||||
<D.ToggleButton @icon="org" @text={{or this.selected.id "-"}} @isFullWidth={{true}} data-test-namespace-toggle />
|
||||
<D.ToggleButton
|
||||
@icon="org"
|
||||
@text={{or this.selectedNamespace.id "-"}}
|
||||
@isFullWidth={{true}}
|
||||
data-test-toggle-input="namespace-id"
|
||||
{{on "click" this.toggleNamespacePicker}}
|
||||
/>
|
||||
|
||||
{{#if this.errorLoadingNamespaces}}
|
||||
|
||||
|
|
@ -52,7 +58,7 @@
|
|||
<div class="is-overflow-y-auto is-max-drawer-height" {{did-insert this.setupScrollListener}}>
|
||||
{{#each this.visibleNamespaceOptions as |option|}}
|
||||
<D.Checkmark
|
||||
@selected={{eq option.id this.selected.id}}
|
||||
@selected={{eq option.id this.selectedNamespace.id}}
|
||||
{{on "click" (fn this.onChange option)}}
|
||||
data-test-namespace-link={{option.path}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export default class NamespacePicker extends Component {
|
|||
// Load 200 namespaces in the namespace picker at a time
|
||||
@tracked batchSize = 200;
|
||||
|
||||
@tracked allNamespaces: NamespaceOption[] = [];
|
||||
@tracked canManageNamespaces = false; // Show/hide manage namespaces button
|
||||
@tracked canRefreshNamespaces = false; // Show/hide refresh list button
|
||||
@tracked errorLoadingNamespaces = '';
|
||||
|
|
@ -47,13 +46,24 @@ export default class NamespacePicker extends Component {
|
|||
@tracked searchInput = '';
|
||||
@tracked searchInputHelpText =
|
||||
"Enter a full path in the search bar and hit the 'Enter' ↵ key to navigate faster.";
|
||||
@tracked selected: NamespaceOption | null = null;
|
||||
|
||||
constructor(owner: unknown, args: Record<string, never>) {
|
||||
super(owner, args);
|
||||
this.loadOptions();
|
||||
}
|
||||
|
||||
get allNamespaces(): NamespaceOption[] {
|
||||
return this.getOptions(
|
||||
this.namespace?.accessibleNamespaces,
|
||||
this.namespace?.currentNamespace,
|
||||
this.namespace?.path
|
||||
);
|
||||
}
|
||||
|
||||
get selectedNamespace(): NamespaceOption | null {
|
||||
return this.getSelected(this.allNamespaces, this.namespace?.path) ?? null;
|
||||
}
|
||||
|
||||
private matchesPath(option: NamespaceOption, currentPath: string): boolean {
|
||||
return option?.path === currentPath;
|
||||
}
|
||||
|
|
@ -62,7 +72,11 @@ export default class NamespacePicker extends Component {
|
|||
return options.find((option) => this.matchesPath(option, currentPath));
|
||||
}
|
||||
|
||||
private getOptions(namespace: NamespaceService): NamespaceOption[] {
|
||||
private getOptions(
|
||||
accessibleNamespaces: string[],
|
||||
currentNamespace: string,
|
||||
path: string
|
||||
): NamespaceOption[] {
|
||||
/* Each namespace option has 3 properties: { id, path, and label }
|
||||
* - id: node / namespace name (displayed when the namespace picker is closed)
|
||||
* - path: full namespace path (used to navigate to the namespace)
|
||||
|
|
@ -76,7 +90,7 @@ export default class NamespacePicker extends Component {
|
|||
* | 'child' | 'parent/child' | 'parent/child' |
|
||||
*/
|
||||
const options = [
|
||||
...(namespace?.accessibleNamespaces || []).map((ns: string) => {
|
||||
...(accessibleNamespaces || []).map((ns: string) => {
|
||||
const parts = ns.split('/');
|
||||
return { id: parts[parts.length - 1] || '', path: ns, label: ns };
|
||||
}),
|
||||
|
|
@ -91,9 +105,9 @@ export default class NamespacePicker extends Component {
|
|||
// to the list of options. This is a fallback for when the user has access to a single namespace.
|
||||
if (options.length === 0) {
|
||||
options.push({
|
||||
id: namespace.currentNamespace,
|
||||
path: namespace.path,
|
||||
label: namespace.path,
|
||||
id: currentNamespace,
|
||||
path: path,
|
||||
label: path,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -187,9 +201,6 @@ export default class NamespacePicker extends Component {
|
|||
this.errorLoadingNamespaces = errorMessage(error);
|
||||
}
|
||||
|
||||
this.allNamespaces = this.getOptions(this.namespace);
|
||||
this.selected = this.getSelected(this.allNamespaces, this.namespace?.path) ?? null;
|
||||
|
||||
await this.fetchListCapability();
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +227,6 @@ export default class NamespacePicker extends Component {
|
|||
|
||||
@action
|
||||
async onChange(selected: NamespaceOption): Promise<void> {
|
||||
this.selected = selected;
|
||||
this.searchInput = '';
|
||||
this.router.transitionTo('vault.cluster.dashboard', { queryParams: { namespace: selected.path } });
|
||||
}
|
||||
|
|
@ -227,7 +237,6 @@ export default class NamespacePicker extends Component {
|
|||
const matchingNamespace = this.allNamespaces.find((ns) => ns.label === this.searchInput.trim());
|
||||
|
||||
if (matchingNamespace) {
|
||||
this.selected = matchingNamespace;
|
||||
this.searchInput = '';
|
||||
this.router.transitionTo('vault.cluster.dashboard', {
|
||||
queryParams: { namespace: matchingNamespace.path },
|
||||
|
|
@ -247,4 +256,10 @@ export default class NamespacePicker extends Component {
|
|||
this.searchInput = '';
|
||||
await this.loadOptions();
|
||||
}
|
||||
|
||||
@action
|
||||
toggleNamespacePicker() {
|
||||
// Reset the search input when the dropdown is toggled
|
||||
this.searchInput = '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@
|
|||
<Toggle
|
||||
@checked={{this.clusterRoleBinding}}
|
||||
@onChange={{(toggle-action "clusterRoleBinding" this)}}
|
||||
data-test-kubernetes-clusterRoleBinding
|
||||
data-test-toggle-input="kubernetes-clusterRoleBinding"
|
||||
>
|
||||
<h3 class="title is-7 is-marginless">ClusterRoleBinding</h3>
|
||||
<div class="description has-text-grey">
|
||||
|
|
|
|||
|
|
@ -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/'],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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]');
|
||||
|
|
|
|||
|
|
@ -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]');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"]',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]');
|
||||
|
||||
|
|
|
|||
|
|
@ -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`<NamespacePicker />`);
|
||||
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`<NamespacePicker/>`);
|
||||
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`<NamespacePicker />`);
|
||||
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`<NamespacePicker />`);
|
||||
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`<NamespacePicker />`);
|
||||
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`<NamespacePicker />`);
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<Sidebar::Frame @showSidebar={{true}} />
|
||||
`);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue