mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-26 00:51:08 +02:00
* Allow Managed clusters to see Secrets Sync Overview and Sidebar nav (#26649) * update badge text and allow hvd on secrets sync views * update logic in Secrets Sync overview and cta for hvd. * spacing * rearrange based on pr feedback * fix return on badgeText and cluster nav test * fix landing cta tests * update test to reflect new changes * moved call to feature-flags from application route to the service to match patterns * add managed test coverage on overview component test and remove premium feature so cta message appplies to both managed and non-managed clusters * missed service name and unskip admin test * clean up * fix tests * flags test fix * Rename isManaged and managedNamespaceRoot (#26697) * renames * lowercase HVD to match * missed some * test failure * [Secrets Sync] enable access to Sync clients page for HVD clusters (#26713) * feat: split client counts navbar into separate component * acceptance/clients/counts/overview-test: remove tests now covered by int tests * clients counts route: rename isSecretsSyncActivated to showSecretsSync * sync clients page: show unactivated state unless sync client history or feature is activated * client counts navbar: show sync tab only if client history or is /able to be/ activated * clients overview page: only show sync charts if activated * fix: rename isManaged to isHvd * acceptance/counts/overview-test: add HVD tests * acceptance/counts/overview-test: clean up unused cruft * aceptance/clients/counts/overview-test: ensure we dont get false negatives * chore: move Clients::Error to Clients::Counts::Error * chore: calculate showSecretSync in page component instead of route * chore: add copyright headers * acceptance/clients/counts/overview-test: stub activated flags to fix test * [Secrets sync] update sync test selectors (#26824) * acceptance/clients/counts/overview-test: use imported test selectors * general-selectors: add missing emptyStateSubtitle property * acceptance/clients/counts/sync: nest tests in top level module for easier test runs * Add permissions check to show/hide activate button (#26840) * add permissions check to flags service and consume in overview template * add back missing refresh * fix test failures * add test coverage * clean up * address flaky test * grr * address test failures * add changelog * try to fix test failure only on gh * fix fetch to match previous implementation of feature-flags * fix failing test * update comment --------- Co-authored-by: Noelle Daley <noelledaley@users.noreply.github.com> Co-authored-by: clairebontempo@gmail.com <clairebontempo@gmail.com>
384 lines
14 KiB
JavaScript
384 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
/* eslint-disable ember/no-settled-after-test-helper */
|
|
import { module, test } from 'qunit';
|
|
import { setupRenderingTest } from 'ember-qunit';
|
|
import { setupEngine } from 'ember-engines/test-support';
|
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
import { render, click, settled } from '@ember/test-helpers';
|
|
import hbs from 'htmlbars-inline-precompile';
|
|
import syncScenario from 'vault/mirage/scenarios/sync';
|
|
import syncHandlers from 'vault/mirage/handlers/sync';
|
|
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
|
|
import { Response } from 'miragejs';
|
|
import { dateFormat } from 'core/helpers/date-format';
|
|
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
|
|
|
const { title, tab, overviewCard, cta, overview, pagination, emptyStateTitle, emptyStateMessage } = PAGE;
|
|
|
|
module('Integration | Component | sync | Page::Overview', function (hooks) {
|
|
setupRenderingTest(hooks);
|
|
setupEngine(hooks, 'sync');
|
|
setupMirage(hooks);
|
|
|
|
hooks.beforeEach(async function () {
|
|
// allow capabilities as root by default to allow users to POST to the secrets-sync/activate endpoint
|
|
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
|
|
this.version = this.owner.lookup('service:version');
|
|
this.store = this.owner.lookup('service:store');
|
|
this.version.type = 'enterprise';
|
|
this.version.features = ['Secrets Sync'];
|
|
|
|
syncScenario(this.server);
|
|
syncHandlers(this.server);
|
|
|
|
this.destinations = await this.store.query('sync/destination', {});
|
|
this.isActivated = true;
|
|
this.licenseHasSecretsSync = true;
|
|
this.isHvdManaged = false;
|
|
|
|
this.renderComponent = () => {
|
|
return render(
|
|
hbs`<Secrets::Page::Overview @destinations={{this.destinations}} @totalVaultSecrets={{7}} @isActivated={{this.isActivated}} @licenseHasSecretsSync={{this.licenseHasSecretsSync}} @isHvdManaged={{this.isHvdManaged}} />`,
|
|
{
|
|
owner: this.engine,
|
|
}
|
|
);
|
|
};
|
|
});
|
|
|
|
test('it should render header, tabs and toolbar for overview state', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(title).hasText('Secrets Sync', 'Page title renders');
|
|
assert.dom(cta.button).doesNotExist('CTA does not render');
|
|
assert.dom(tab('Overview')).hasText('Overview', 'Overview tab renders');
|
|
assert.dom(tab('Destinations')).hasText('Destinations', 'Destinations tab renders');
|
|
assert.dom(overview.createDestination).hasText('Create new destination', 'Toolbar action renders');
|
|
});
|
|
|
|
module('community', function (hooks) {
|
|
hooks.beforeEach(function () {
|
|
this.version.type = 'community';
|
|
this.isActivated = false;
|
|
this.licenseHasSecretsSync = false;
|
|
this.destinations = [];
|
|
});
|
|
|
|
test('it should show an upsell CTA', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert
|
|
.dom(title)
|
|
.hasText('Secrets Sync Enterprise feature', 'page title indicates feature is only for Enterprise');
|
|
assert.dom(cta.button).doesNotExist();
|
|
assert.dom(cta.summary).exists();
|
|
});
|
|
});
|
|
|
|
module('ent', function (hooks) {
|
|
hooks.beforeEach(function () {
|
|
this.isActivated = false;
|
|
this.destinations = [];
|
|
});
|
|
|
|
test('it should show an upsell CTA if license does NOT have the secrets sync feature', async function (assert) {
|
|
this.version.features = [];
|
|
await this.renderComponent();
|
|
|
|
assert
|
|
.dom(title)
|
|
.hasText('Secrets Sync Premium feature', 'title indicates feature is only for Premium');
|
|
assert.dom(cta.button).doesNotExist();
|
|
assert.dom(cta.summary).exists();
|
|
});
|
|
|
|
test('it should show create CTA if license has the secrets sync feature', async function (assert) {
|
|
this.version.features = ['Secrets Sync'];
|
|
this.isActivated = true;
|
|
await this.renderComponent();
|
|
|
|
assert.dom(title).hasText('Secrets Sync');
|
|
assert.dom(cta.button).hasText('Create first destination', 'CTA action renders');
|
|
assert.dom(cta.summary).exists();
|
|
});
|
|
});
|
|
|
|
module('managed', function (hooks) {
|
|
hooks.beforeEach(function () {
|
|
this.isActivated = false;
|
|
this.isHvdManaged = true;
|
|
this.destinations = [];
|
|
});
|
|
|
|
test('it should show the opt-in banner if feature is not activated', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(overview.optInBanner).exists('Opt-in banner is shown');
|
|
});
|
|
|
|
test('it should not show the opt-in banner if feature is activated', async function (assert) {
|
|
this.isActivated = true;
|
|
await this.renderComponent();
|
|
|
|
assert.dom(overview.optInBanner).doesNotExist('Opt-in banner is not shown');
|
|
});
|
|
});
|
|
|
|
module('user does not have post permissions to activate', function (hooks) {
|
|
hooks.beforeEach(function () {
|
|
this.isActivated = false;
|
|
this.destinations = [];
|
|
this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub(['read']));
|
|
});
|
|
|
|
test('it should show the opt-in banner without the ability to activate', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert
|
|
.dom(overview.optInBannerDescription)
|
|
.hasText(
|
|
'To use this feature, specific activation is required. Please contact your administrator to activate.'
|
|
);
|
|
assert.dom(overview.optInBannerEnable).doesNotExist('Opt-in enable button does not show');
|
|
});
|
|
|
|
test('it should not show allow the user to dismiss the opt-in banner', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(overview.optInDismiss).doesNotExist('dismiss opt-in banner does not show');
|
|
});
|
|
});
|
|
|
|
module('secrets sync is not activated and license has secrets sync meep', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
this.isActivated = false;
|
|
});
|
|
|
|
test('it should show the opt-in banner with activate description', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(overview.optInBanner).exists('Opt-in banner is shown');
|
|
assert
|
|
.dom(overview.optInBannerDescription)
|
|
.hasText(
|
|
"To use this feature, specific activation is required. Please review the feature documentation and enable it. If you're upgrading from beta, your previous data will be accessible after activation."
|
|
);
|
|
});
|
|
|
|
test('it should show dismiss banner', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(overview.optInDismiss).exists('dismiss opt-in banner shows');
|
|
});
|
|
|
|
test('it should navigate to the opt-in modal', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
await click(overview.optInBannerEnable);
|
|
|
|
assert.dom(overview.optInModal).exists('Opt-in modal is shown');
|
|
assert.dom(overview.optInConfirm).isDisabled('Confirm button is disabled when checkbox is unchecked');
|
|
|
|
await click(overview.optInCheck);
|
|
assert.dom(overview.optInConfirm).isNotDisabled('confirm button is enabled once checkbox is checked');
|
|
});
|
|
|
|
test('it should make a POST to activate the feature', async function (assert) {
|
|
assert.expect(1);
|
|
|
|
await this.renderComponent();
|
|
|
|
this.server.post('/sys/activation-flags/secrets-sync/activate', () => {
|
|
assert.true(true, 'POST to secrets-sync/activate is called');
|
|
return {};
|
|
});
|
|
|
|
await this.renderComponent();
|
|
|
|
await click(overview.optInBannerEnable);
|
|
await click(overview.optInCheck);
|
|
await click(overview.optInConfirm);
|
|
});
|
|
|
|
test('it shows an error if activation fails', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
this.server.post('/sys/activation-flags/secrets-sync/activate', () => new Response(403));
|
|
|
|
await click(overview.optInBannerEnable);
|
|
await click(overview.optInCheck);
|
|
await click(overview.optInConfirm);
|
|
|
|
assert.dom(overview.optInError).exists('shows an error banner');
|
|
assert.dom(overview.optInBanner).exists('banner is visible so user can try to opt-in again');
|
|
});
|
|
});
|
|
|
|
module('secrets sync is not activated and license does not have secrets sync', function (hooks) {
|
|
hooks.beforeEach(async function () {
|
|
this.licenseHasSecretsSync = false;
|
|
});
|
|
|
|
test('it should hide the opt-in banner', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(overview.optInBanner).doesNotExist();
|
|
});
|
|
});
|
|
|
|
module('secrets sync is activated', function () {
|
|
test('it should hide the opt-in banner', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(overview.optInBanner).doesNotExist();
|
|
});
|
|
});
|
|
|
|
module('with no destinations', function (hooks) {
|
|
hooks.beforeEach(function () {
|
|
this.destinations = [];
|
|
});
|
|
|
|
test('it should show a CTA', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(cta.button).exists();
|
|
|
|
assert
|
|
.dom(overview.createDestination)
|
|
.doesNotExist('create new destination link is hidden if there are no pre-existing destinations');
|
|
});
|
|
|
|
test('it should hide the overview cards', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert
|
|
.dom(overviewCard.title('Secrets by destination'))
|
|
.doesNotExist('it does not show secrets by destination card if there are no destinations');
|
|
assert
|
|
.dom(overviewCard.title('Total destinations'))
|
|
.doesNotExist('it does not show total destinations card if there are no destinations');
|
|
assert
|
|
.dom(overviewCard.title('Total secrets'))
|
|
.doesNotExist('it does not show total secrets card if there are no destinations');
|
|
});
|
|
});
|
|
|
|
module('with destinations', function () {
|
|
test('it should show the overview cards', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
assert.dom(overviewCard.title('Secrets by destination')).exists();
|
|
assert.dom(overviewCard.title('Total destinations')).exists();
|
|
assert.dom(overviewCard.title('Total secrets')).exists();
|
|
});
|
|
|
|
module('Secrets by destination table', function () {
|
|
test('it should show the table with correct columns, data, badges, and actions', async function (assert) {
|
|
const { icon, name, badge, total, updated, actionToggle, action } = overview.table;
|
|
const updatedDate = dateFormat(
|
|
[new Date('2023-09-20T10:51:53.961861096-04:00'), 'MMMM do yyyy, h:mm:ss a'],
|
|
{}
|
|
);
|
|
|
|
await this.renderComponent();
|
|
|
|
assert
|
|
.dom(overviewCard.title('Secrets by destination'))
|
|
.hasText('Secrets by destination', 'Overview card title renders for table');
|
|
assert.dom(icon(0)).hasAttribute('data-test-icon', 'aws-color', 'Destination icon renders');
|
|
assert.dom(name(0)).hasText('destination-aws', 'Destination name renders');
|
|
|
|
assert.dom(badge(0)).hasText('All synced', 'All synced badge renders');
|
|
assert
|
|
.dom(badge(0))
|
|
.hasClass('hds-badge--color-success', 'Correct color renders for All synced badge');
|
|
|
|
assert
|
|
.dom(badge(1))
|
|
.doesNotExist('Status badge does not render for destination with no associations');
|
|
|
|
assert.dom(badge(2)).hasText('1 Unsynced', 'Unsynced badge renders');
|
|
assert.dom(badge(2)).hasClass('hds-badge--color-neutral', 'Correct color renders for unsynced badge');
|
|
|
|
assert.dom(total(0)).hasText('1', '# of external secrets renders');
|
|
assert.dom(updated(0)).hasText(updatedDate, 'Last updated datetime renders');
|
|
|
|
assert
|
|
.dom(total(1))
|
|
.hasText('0', '# of external secrets renders for destination with no associations');
|
|
assert
|
|
.dom(updated(1))
|
|
.hasText('—', 'Last updated placeholder renders for destination with no associations');
|
|
|
|
await click(actionToggle(0));
|
|
assert.dom(action('sync')).hasText('Sync secrets', 'Sync action renders');
|
|
assert.dom(action('details')).hasText('View synced secrets', 'View synced secrets action renders');
|
|
});
|
|
|
|
test('it should paginate the table', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
const { name, row } = overview.table;
|
|
assert.dom(row).exists({ count: 3 }, 'Correct number of table rows render based on page size');
|
|
assert.dom(name(0)).hasText('destination-aws', 'First destination renders on page 1');
|
|
|
|
await click(pagination.next);
|
|
await settled();
|
|
assert
|
|
.dom(overview.table.row)
|
|
.exists({ count: 3 }, 'New items are fetched and rendered on page change');
|
|
assert.dom(name(0)).hasText('destination-gcp', 'First destination renders on page 2');
|
|
});
|
|
|
|
test('it should show an empty state if there is an error fetching associations', async function (assert) {
|
|
this.server.get('/sys/sync/destinations/:type/:name/associations', () => {
|
|
return new Response(403, {}, { errors: ['Permission denied'] });
|
|
});
|
|
|
|
await this.renderComponent();
|
|
|
|
assert.dom(emptyStateTitle).hasText('Error fetching information', 'Empty state title renders');
|
|
assert
|
|
.dom(emptyStateMessage)
|
|
.hasText(
|
|
'Ensure that the policy has access to read sync associations.',
|
|
'Empty state message renders'
|
|
);
|
|
});
|
|
});
|
|
|
|
test('it should show the Totals cards', async function (assert) {
|
|
await this.renderComponent();
|
|
|
|
const { title, description, action, content } = overviewCard;
|
|
const cardData = [
|
|
{
|
|
cardTitle: 'Total destinations',
|
|
subText: 'The total number of connected destinations',
|
|
actionText: 'Create new',
|
|
count: '6',
|
|
},
|
|
{
|
|
cardTitle: 'Total secrets',
|
|
subText:
|
|
'The total number of secrets that have been synced from Vault over time. One secret will be counted as one sync client.',
|
|
actionText: 'View billing',
|
|
count: '7',
|
|
},
|
|
];
|
|
|
|
cardData.forEach(({ cardTitle, subText, actionText, count }) => {
|
|
assert.dom(title(cardTitle)).hasText(cardTitle, `${cardTitle} card title renders`);
|
|
assert.dom(description(cardTitle)).hasText(subText, ` ${cardTitle} card description renders`);
|
|
assert.dom(content(cardTitle)).hasText(count, 'Total count renders');
|
|
assert.dom(action(cardTitle)).hasText(actionText, 'Card action renders');
|
|
});
|
|
});
|
|
});
|
|
});
|