diff --git a/changelog/26841.txt b/changelog/26841.txt new file mode 100644 index 000000000000..a65e5affbef9 --- /dev/null +++ b/changelog/26841.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui (enterprise): Allow HVD users to access Secrets Sync. +``` \ No newline at end of file diff --git a/ui/app/components/auth-jwt.js b/ui/app/components/auth-jwt.js index c8cab780946d..a373ee86cfb0 100644 --- a/ui/app/components/auth-jwt.js +++ b/ui/app/components/auth-jwt.js @@ -133,7 +133,7 @@ export default Component.extend({ // The namespace can be either be passed as a query parameter, or be embedded // in the state param in the format `,ns=`. So if // `namespace` is empty, check for namespace in state as well. - if (namespace === '' || this.flagsService.managedNamespaceRoot) { + if (namespace === '' || this.flagsService.hvdManagedNamespaceRoot) { const i = state.indexOf(',ns='); if (i >= 0) { // ",ns=" is 4 characters diff --git a/ui/app/components/clients/activity.ts b/ui/app/components/clients/activity.ts index ea25b3b0bc38..f22134b94a43 100644 --- a/ui/app/components/clients/activity.ts +++ b/ui/app/components/clients/activity.ts @@ -22,7 +22,6 @@ import type { } from 'core/utils/client-count-utils'; interface Args { - isSecretsSyncActivated?: boolean; activity: ClientsActivityModel; versionHistory: ClientsVersionHistoryModel[]; startTimestamp: number; diff --git a/ui/app/components/clients/attribution.js b/ui/app/components/clients/attribution.js index 928661afcfa1..ed8bde49f30d 100644 --- a/ui/app/components/clients/attribution.js +++ b/ui/app/components/clients/attribution.js @@ -39,6 +39,7 @@ import { format, isSameMonth } from 'date-fns'; * @param {string} responseTimestamp - ISO timestamp created in serializer to timestamp the response, renders in bottom left corner below attribution chart * @param {boolean} isHistoricalMonth - when true data is from a single, historical month so side-by-side charts should display for attribution data * @param {array} upgradesDuringActivity - array of objects containing version history upgrade data + * @param {boolean} isSecretsSyncActivated - boolean to determine if secrets sync is activated */ export default class Attribution extends Component { diff --git a/ui/app/components/clients/error.hbs b/ui/app/components/clients/counts/error.hbs similarity index 100% rename from ui/app/components/clients/error.hbs rename to ui/app/components/clients/counts/error.hbs diff --git a/ui/app/components/clients/counts/nav-bar.hbs b/ui/app/components/clients/counts/nav-bar.hbs new file mode 100644 index 000000000000..04c6e028c22f --- /dev/null +++ b/ui/app/components/clients/counts/nav-bar.hbs @@ -0,0 +1,31 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + + \ No newline at end of file diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index 9da5164761c3..512d729b9148 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -51,7 +51,7 @@ {{#if (eq @activity.id "no-data")}} {{else if @activityError}} - + {{else}} {{#if (eq @config.enabled "Off")}} @@ -151,32 +151,7 @@ {{/if}} - + {{! CLIENT COUNT PAGE COMPONENTS RENDER HERE }} {{yield}} diff --git a/ui/app/components/clients/page/counts.ts b/ui/app/components/clients/page/counts.ts index 1f8d308850b9..28fb5ae7b05d 100644 --- a/ui/app/components/clients/page/counts.ts +++ b/ui/app/components/clients/page/counts.ts @@ -12,6 +12,7 @@ import { filterVersionHistory, formatDateObject } from 'core/utils/client-count- import timestamp from 'core/utils/timestamp'; import type AdapterError from '@ember-data/adapter'; +import type FlagsService from 'vault/services/flags'; import type StoreService from 'vault/services/store'; import type VersionService from 'vault/services/version'; import type ClientsActivityModel from 'vault/models/clients/activity'; @@ -30,6 +31,7 @@ interface Args { } export default class ClientsCountsPageComponent extends Component { + @service declare readonly flags: FlagsService; @service declare readonly version: VersionService; @service declare readonly store: StoreService; @@ -165,6 +167,22 @@ export default class ClientsCountsPageComponent extends Component { return activity?.total; } + get showSecretsSync(): boolean { + const { activity } = this.args; + // if there is any sync client data, show it + if (activity && activity?.total?.secret_syncs > 0) return true; + + // otherwise, show the tab based on the cluster type and license + if (this.version.isCommunity) return false; + + const isHvd = this.flags.isHvdManaged; + const onLicense = this.version.hasSecretsSync; + + // we can't tell if HVD clusters have the feature or not, so we show it by default + // if the cluster is not HVD, show the tab if the feature is on the license + return isHvd || onLicense; + } + @action onDateChange(dateObject: { dateType: string; monthIdx: number; year: number }) { const { dateType, monthIdx, year } = dateObject; diff --git a/ui/app/components/clients/page/overview.hbs b/ui/app/components/clients/page/overview.hbs index 0923f56aeac3..f069423df279 100644 --- a/ui/app/components/clients/page/overview.hbs +++ b/ui/app/components/clients/page/overview.hbs @@ -4,7 +4,7 @@ ~}} {{#if this.hasAttributionData}} - {{#if this.showSync}} - - {{/if}} + {{#if (has-permission "access")}} 1) { @@ -42,8 +42,8 @@ export default Controller.extend({ fullNamespaceFromInput(value) { const strippedNs = sanitizePath(value); - if (this.managedNamespaceRoot) { - return `${this.managedNamespaceRoot}/${strippedNs}`; + if (this.hvdManagedNamespaceRoot) { + return `${this.hvdManagedNamespaceRoot}/${strippedNs}`; } return strippedNs; }, diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index 95014f40ed82..4916222c4685 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -65,14 +65,7 @@ export default Route.extend({ }, }, - async beforeModel() { - const result = await fetch('/v1/sys/internal/ui/feature-flags', { - method: 'GET', - }); - if (result.status === 200) { - const body = await result.json(); - const flags = body.feature_flags || []; - this.flagsService.setFeatureFlags(flags); - } + beforeModel() { + return this.flagsService.fetchFeatureFlags(); }, }); diff --git a/ui/app/routes/vault/cluster.js b/ui/app/routes/vault/cluster.js index 8ca0e27b5128..6620b0447212 100644 --- a/ui/app/routes/vault/cluster.js +++ b/ui/app/routes/vault/cluster.js @@ -58,7 +58,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, { const params = this.paramsFor(this.routeName); let namespace = params.namespaceQueryParam; const currentTokenName = this.auth.currentTokenName; - const managedRoot = this.flagsService.managedNamespaceRoot; + const managedRoot = this.flagsService.hvdManagedNamespaceRoot; assert( 'Cannot use VAULT_CLOUD_ADMIN_NAMESPACE flag with non-enterprise Vault version', !(managedRoot && this.version.isCommunity) diff --git a/ui/app/routes/vault/cluster/clients/counts.ts b/ui/app/routes/vault/cluster/clients/counts.ts index e5be73f97e67..3c87c2922e11 100644 --- a/ui/app/routes/vault/cluster/clients/counts.ts +++ b/ui/app/routes/vault/cluster/clients/counts.ts @@ -5,16 +5,16 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import { DEBUG } from '@glimmer/env'; import timestamp from 'core/utils/timestamp'; import { getUnixTime } from 'date-fns'; +import type FlagsService from 'vault/services/flags'; import type StoreService from 'vault/services/store'; import type VersionService from 'vault/services/version'; - import type { ModelFrom } from 'vault/vault/route'; import type ClientsRoute from '../clients'; import type ClientsActivityModel from 'vault/models/clients/activity'; +import type ClientsConfigModel from 'vault/models/clients/config'; import type ClientsCountsController from 'vault/controllers/vault/cluster/clients/counts'; import { setStartTimeQuery } from 'core/utils/client-count-utils'; @@ -25,14 +25,8 @@ export interface ClientsCountsRouteParams { mountPath?: string | undefined; } -interface ActivationFlagsResponse { - data: { - activated: Array; - unactivated: Array; - }; -} - export default class ClientsCountsRoute extends Route { + @service declare readonly flags: FlagsService; @service declare readonly store: StoreService; @service declare readonly version: VersionService; @@ -43,6 +37,10 @@ export default class ClientsCountsRoute extends Route { mountPath: { refreshModel: false, replace: true }, }; + beforeModel() { + return this.flags.fetchActivatedFlags(); + } + async getActivity(start_time: number | null, end_time: number) { let activity, activityError; // if there is no start_time we want the user to manually choose a date @@ -60,30 +58,6 @@ export default class ClientsCountsRoute extends Route { return { activity, activityError }; } - async getActivatedFeatures() { - try { - const resp: ActivationFlagsResponse = await this.store - .adapterFor('application') - .ajax('/v1/sys/activation-flags', 'GET', { unauthenticated: true, namespace: null }); - return resp.data?.activated; - } catch (error) { - if (DEBUG) console.error(error); // eslint-disable-line no-console - return []; - } - } - - async isSecretsSyncActivated(activity: ClientsActivityModel | undefined) { - // if there are secrets, the feature is activated - if (activity && activity.total?.secret_syncs > 0) return true; - - // if feature is not in license, it's definitely not activated - if (!this.version.hasSecretsSync) return false; - - // otherwise check explicitly if the feature has been activated - const activatedFeatures = await this.getActivatedFeatures(); - return activatedFeatures.includes('secrets-sync'); - } - async model(params: ClientsCountsRouteParams) { const { config, versionHistory } = this.modelFor('vault.cluster.clients') as ModelFrom; // only enterprise versions will have a relevant billing start date, if null users must select initial start time @@ -93,14 +67,11 @@ export default class ClientsCountsRoute extends Route { const endTimestamp = Number(params.end_time) || getUnixTime(timestamp.now()); const { activity, activityError } = await this.getActivity(startTimestamp, endTimestamp); - const isSecretsSyncActivated = await this.isSecretsSyncActivated(activity); - return { activity, activityError, config, endTimestamp, - isSecretsSyncActivated, startTimestamp, versionHistory, }; diff --git a/ui/app/services/flags.ts b/ui/app/services/flags.ts index a89a2928decd..272bf9a9c52d 100644 --- a/ui/app/services/flags.ts +++ b/ui/app/services/flags.ts @@ -7,6 +7,7 @@ import Service, { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { keepLatestTask } from 'ember-concurrency'; import { DEBUG } from '@glimmer/env'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; import type StoreService from 'vault/services/store'; import type VersionService from 'vault/services/version'; @@ -24,22 +25,37 @@ export default class flagsService extends Service { @service declare readonly version: VersionService; @service declare readonly store: StoreService; - @tracked flags: string[] = []; @tracked activatedFlags: string[] = []; + @tracked featureFlags: string[] = []; - setFeatureFlags(flags: string[]) { - this.flags = flags; + get isHvdManaged(): boolean { + return this.featureFlags?.includes(FLAGS.vaultCloudNamespace); } - get managedNamespaceRoot() { - if (this.flags && this.flags.includes(FLAGS.vaultCloudNamespace)) { - return 'admin'; + get hvdManagedNamespaceRoot(): string | null { + return this.isHvdManaged ? 'admin' : null; + } + + getFeatureFlags = keepLatestTask(async () => { + try { + const result = await fetch('/v1/sys/internal/ui/feature-flags', { + method: 'GET', + }); + + if (result.status === 200) { + const body = await result.json(); + this.featureFlags = body.feature_flags || []; + } + } catch (error) { + if (DEBUG) console.error(error); // eslint-disable-line no-console } - return null; + }); + + fetchFeatureFlags() { + return this.getFeatureFlags.perform(); } - // TODO getter will be used in the upcoming persona service - get secretsSyncIsActivated() { + get secretsSyncIsActivated(): boolean { return this.activatedFlags.includes('secrets-sync'); } @@ -61,4 +77,13 @@ export default class flagsService extends Service { fetchActivatedFlags() { return this.getActivatedFlags.perform(); } + + @lazyCapabilities(apiPath`sys/activation-flags/secrets-sync/activate`) secretsSyncActivatePath; + + get canActivateSecretsSync() { + return ( + this.secretsSyncActivatePath.get('canCreate') !== false || + this.secretsSyncActivatePath.get('canUpdate') !== false + ); + } } diff --git a/ui/app/services/version.js b/ui/app/services/version.js index 7b02d8547a4a..fcb4871fbb5c 100644 --- a/ui/app/services/version.js +++ b/ui/app/services/version.js @@ -48,8 +48,6 @@ export default class VersionService extends Service { } get hasSecretsSync() { - // TODO remove this conditional when we allow secrets sync in managed clusters - if (this.flags.managedNamespaceRoot !== null) return false; return this.features.includes('Secrets Sync'); } diff --git a/ui/app/templates/vault/cluster/auth.hbs b/ui/app/templates/vault/cluster/auth.hbs index 13d291f2a24f..cb4027416878 100644 --- a/ui/app/templates/vault/cluster/auth.hbs +++ b/ui/app/templates/vault/cluster/auth.hbs @@ -62,9 +62,9 @@
- {{#if this.managedNamespaceRoot}} + {{#if this.hvdManagedNamespaceRoot}}
- /{{this.managedNamespaceRoot}} + /{{this.hvdManagedNamespaceRoot}}
{{/if}}
@@ -73,7 +73,7 @@
{{#if @subTitle}} -

+

{{@subTitle}}

{{/if}} @@ -22,7 +22,7 @@ {{@title}}

{{#if @subTitle}} -

+

{{@subTitle}}

{{/if}} diff --git a/ui/lib/sync/addon/components/secrets/landing-cta.hbs b/ui/lib/sync/addon/components/secrets/landing-cta.hbs index b77c376d7913..9750a534871f 100644 --- a/ui/lib/sync/addon/components/secrets/landing-cta.hbs +++ b/ui/lib/sync/addon/components/secrets/landing-cta.hbs @@ -5,38 +5,27 @@ <:actions> - {{! Only allow users to create a destination if secrets-sync is activated and they have the secrets sync feature }} - {{#if (and @hasSecretsSync @isActivated)}} + {{! Only allow users who have activated the feature to create a destination. }} + {{#if @isActivated}} {{/if}}
- {{#if @hasSecretsSync}} -

- Sync secrets to platforms and tools across your stack to get secrets when and where you need them. - -

- {{else}} -

- This premium enterprise feature allows you to sync secrets to platforms and tools across your stack to get secrets when - and where you need them. - -

- {{/if}} + {{! One cta message regardless of OSS vs Enterprise with/without secrets sync or managed cluster. }} +

+ This feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need + them. + +

diff --git a/ui/lib/sync/addon/components/secrets/page/overview.hbs b/ui/lib/sync/addon/components/secrets/page/overview.hbs index 1763ff71c899..1afd64e50b11 100644 --- a/ui/lib/sync/addon/components/secrets/page/overview.hbs +++ b/ui/lib/sync/addon/components/secrets/page/overview.hbs @@ -2,28 +2,37 @@ Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: BUSL-1.1 ~}} - -{{#if (and this.version.hasSecretsSync (not @isActivated))}} - {{#unless this.hideOptIn}} - - Enable Secrets Sync feature - 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. - - - {{/unless}} -{{/if}} +{{#unless @isActivated}} + {{#if (or @licenseHasSecretsSync @isHvdManaged)}} + {{#unless this.hideOptIn}} + {{! Allows users to dismiss activation banner if they have permissions to activate. }} + + Enable Secrets Sync feature + To use this feature, specific activation is required. + {{if + this.flags.canActivateSecretsSync + "Please review the feature documentation and + enable it. If you're upgrading from beta, your previous data will be accessible after activation." + "Please contact your administrator to activate." + }} + {{#if this.flags.canActivateSecretsSync}} + + {{/if}} + + {{/unless}} + {{/if}} +{{/unless}} {{! error message if post to activated endpoint fails }} {{#if this.error}} @@ -185,7 +194,7 @@
{{else}} - + {{/if}} {{#if this.showActivateSecretsSyncModal}} diff --git a/ui/lib/sync/addon/components/secrets/page/overview.ts b/ui/lib/sync/addon/components/secrets/page/overview.ts index 9b9723d0edab..75e2778a49df 100644 --- a/ui/lib/sync/addon/components/secrets/page/overview.ts +++ b/ui/lib/sync/addon/components/secrets/page/overview.ts @@ -16,13 +16,16 @@ import type FlashMessageService from 'vault/services/flash-messages'; import type StoreService from 'vault/services/store'; import type RouterService from '@ember/routing/router-service'; import type VersionService from 'vault/services/version'; +import type FlagsService from 'vault/services/flags'; import type { SyncDestinationAssociationMetrics } from 'vault/vault/adapters/sync/association'; import type SyncDestinationModel from 'vault/vault/models/sync/destination'; interface Args { destinations: Array; totalVaultSecrets: number; - activatedFeatures: Array; + isActivated: boolean; + licenseHasSecretsSync: boolean; + isHvdManaged: boolean; } export default class SyncSecretsDestinationsPageComponent extends Component { @@ -30,6 +33,7 @@ export default class SyncSecretsDestinationsPageComponent extends Component { @service declare readonly version: VersionService; + @service declare readonly flags: FlagsService; get badgeText() { - return this.version.hasSecretsSync - ? '' - : this.version.isCommunity - ? 'Enterprise feature' - : 'Premium feature'; + const isHvdManaged = this.flags.isHvdManaged; + const onLicense = this.version.hasSecretsSync; + const isEnterprise = this.version.isEnterprise; + + if (isHvdManaged) return 'Plus feature'; + if (isEnterprise && !onLicense) return 'Premium feature'; + if (!isEnterprise) return 'Enterprise feature'; + // no badge for Enterprise clusters with Secrets Sync on their license--the only remaining option. + return ''; } } diff --git a/ui/lib/sync/addon/routes/secrets/overview.ts b/ui/lib/sync/addon/routes/secrets/overview.ts index f4d0069f98b1..52a81d1074bb 100644 --- a/ui/lib/sync/addon/routes/secrets/overview.ts +++ b/ui/lib/sync/addon/routes/secrets/overview.ts @@ -7,28 +7,24 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; import { hash } from 'rsvp'; -import type RouterService from '@ember/routing/router-service'; import type FlagsService from 'vault/services/flags'; import type StoreService from 'vault/services/store'; +import type VersionService from 'vault/services/version'; export default class SyncSecretsOverviewRoute extends Route { - @service declare readonly router: RouterService; @service declare readonly store: StoreService; @service declare readonly flags: FlagsService; - - beforeModel(): void | Promise { - if (this.flags.managedNamespaceRoot !== null) { - this.router.transitionTo('vault.cluster.dashboard'); - } - } + @service declare readonly version: VersionService; async model() { - const { activatedFeatures } = this.modelFor('secrets') as { - activatedFeatures: Array; - }; - const isActivated = activatedFeatures.includes('secrets-sync'); + const isActivated = this.flags.secretsSyncIsActivated; + const licenseHasSecretsSync = this.version.hasSecretsSync; + const isHvdManaged = this.flags.isHvdManaged; + return hash({ + licenseHasSecretsSync, isActivated, + isHvdManaged, destinations: isActivated ? this.store.query('sync/destination', {}).catch(() => []) : [], associations: isActivated ? this.store diff --git a/ui/lib/sync/addon/templates/secrets/overview.hbs b/ui/lib/sync/addon/templates/secrets/overview.hbs index cdb2c3fe6ceb..a24c1d9eafcf 100644 --- a/ui/lib/sync/addon/templates/secrets/overview.hbs +++ b/ui/lib/sync/addon/templates/secrets/overview.hbs @@ -7,4 +7,6 @@ @destinations={{this.model.destinations}} @totalVaultSecrets={{this.model.associations.total_secrets}} @isActivated={{this.model.isActivated}} + @licenseHasSecretsSync={{this.model.licenseHasSecretsSync}} + @isHvdManaged={{this.model.isHvdManaged}} /> \ No newline at end of file diff --git a/ui/tests/acceptance/clients/counts/overview-test.js b/ui/tests/acceptance/clients/counts/overview-test.js index 9e7fdf61f1a4..3bf134effa73 100644 --- a/ui/tests/acceptance/clients/counts/overview-test.js +++ b/ui/tests/acceptance/clients/counts/overview-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import clientsHandler, { STATIC_NOW, LICENSE_START, UPGRADE_DATE } from 'vault/mirage/handlers/clients'; +import syncHandler from 'vault/mirage/handlers/sync'; import sinon from 'sinon'; import { visit, click, findAll, settled } from '@ember/test-helpers'; import authPage from 'vault/tests/pages/auth'; @@ -33,12 +34,6 @@ module('Acceptance | clients | overview', function (hooks) { return visit('/vault/clients/counts/overview'); }); - test('it should render the correct tabs', async function (assert) { - assert.dom(GENERAL.tab('overview')).exists(); - assert.dom(GENERAL.tab('token')).exists(); - assert.dom(GENERAL.tab('acme')).exists(); - }); - test('it should render charts', async function (assert) { assert .dom(CLIENT_COUNT.counts.startMonth) @@ -59,9 +54,11 @@ module('Acceptance | clients | overview', function (hooks) { test('it should update charts when querying date ranges', async function (assert) { // query for single, historical month with no new counts (July 2023) await click(CLIENT_COUNT.rangeDropdown); - await click('[data-test-show-calendar]'); - await click('[data-test-previous-year]'); - await click(`[data-test-calendar-month=${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}]`); + await click(CLIENT_COUNT.calendarWidget.customEndMonth); + await click(CLIENT_COUNT.calendarWidget.previousYear); + + const month = ARRAY_OF_MONTHS[LICENSE_START.getMonth()]; + await click(CLIENT_COUNT.calendarWidget.calendarMonth(month)); assert .dom(CLIENT_COUNT.usageStats('Vault client counts')) .doesNotExist('running total single month stat boxes do not show'); @@ -70,24 +67,24 @@ module('Acceptance | clients | overview', function (hooks) { .doesNotExist('running total month over month charts do not show'); assert.dom(CLIENT_COUNT.attributionBlock).exists('attribution area shows'); assert - .dom('[data-test-chart-container="new-clients"] [data-test-component="empty-state"]') + .dom(`${CHARTS.container('new-clients')} ${GENERAL.emptyStateTitle}`) .exists('new client attribution has empty state'); assert - .dom('[data-test-empty-state-subtext]') + .dom(GENERAL.emptyStateSubtitle) .hasText('There are no new clients for this namespace during this time period. '); - assert.dom('[data-test-chart-container="total-clients"]').exists('total client attribution chart shows'); + assert.dom(CHARTS.container('total-clients')).exists('total client attribution chart shows'); // reset to billing period await click(CLIENT_COUNT.rangeDropdown); - await click('[data-test-current-billing-period]'); + await click(CLIENT_COUNT.currentBillingPeriod); // change billing start to month/year of upgrade to 1.10 await click(CLIENT_COUNT.counts.startEdit); await click(CLIENT_COUNT.monthDropdown); - await click(`[data-test-dropdown-month="${ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()]}"]`); + await click(CLIENT_COUNT.dateDropdown.selectMonth(ARRAY_OF_MONTHS[UPGRADE_DATE.getMonth()])); await click(CLIENT_COUNT.yearDropdown); - await click(`[data-test-dropdown-year="${UPGRADE_DATE.getFullYear()}"]`); - await click('[data-test-date-dropdown-submit]'); + await click(CLIENT_COUNT.dateDropdown.selectYear(UPGRADE_DATE.getFullYear())); + await click(CLIENT_COUNT.dateDropdown.submit); assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area'); assert .dom(CHARTS.container('Vault client counts')) @@ -99,10 +96,10 @@ module('Acceptance | clients | overview', function (hooks) { // query for single, historical month (upgrade month) await click(CLIENT_COUNT.rangeDropdown); - await click('[data-test-show-calendar]'); - assert.dom('[data-test-display-year]').hasText('2024'); - await click('[data-test-previous-year]'); - await click('[data-test-calendar-month="September"]'); + await click(CLIENT_COUNT.calendarWidget.customEndMonth); + assert.dom(CLIENT_COUNT.calendarWidget.displayYear).hasText('2024'); + await click(CLIENT_COUNT.calendarWidget.previousYear); + await click(CLIENT_COUNT.calendarWidget.calendarMonth('September')); assert .dom(CLIENT_COUNT.usageStats('Vault client counts')) .exists('running total single month usage stats show'); @@ -115,8 +112,8 @@ module('Acceptance | clients | overview', function (hooks) { // query historical date range (from September 2023 to December 2023) await click(CLIENT_COUNT.rangeDropdown); - await click('[data-test-show-calendar]'); - await click('[data-test-calendar-month="December"]'); + await click(CLIENT_COUNT.calendarWidget.customEndMonth); + await click(CLIENT_COUNT.calendarWidget.calendarMonth('December')); assert.dom(CLIENT_COUNT.attributionBlock).exists('Shows attribution area'); assert @@ -130,14 +127,14 @@ module('Acceptance | clients | overview', function (hooks) { // reset to billing period await click(CLIENT_COUNT.rangeDropdown); - await click('[data-test-current-billing-period]'); + await click(CLIENT_COUNT.currentBillingPeriod); // query month older than count start date await click(CLIENT_COUNT.counts.startEdit); await click(CLIENT_COUNT.monthDropdown); - await click(`[data-test-dropdown-month="${ARRAY_OF_MONTHS[LICENSE_START.getMonth()]}"]`); + await click(CLIENT_COUNT.dateDropdown.selectMonth(ARRAY_OF_MONTHS[LICENSE_START.getMonth()])); await click(CLIENT_COUNT.yearDropdown); - await click(`[data-test-dropdown-year="${LICENSE_START.getFullYear() - 3}"]`); - await click('[data-test-date-dropdown-submit]'); + await click(CLIENT_COUNT.dateDropdown.selectYear(LICENSE_START.getFullYear() - 3)); + await click(CLIENT_COUNT.dateDropdown.submit); assert .dom(CLIENT_COUNT.counts.startDiscrepancy) .hasTextContaining( @@ -147,6 +144,9 @@ module('Acceptance | clients | overview', function (hooks) { }); test('totals filter correctly with full data', async function (assert) { + // stub secrets sync being activated + this.owner.lookup('service:flags').activatedFlags = ['secrets-sync']; + assert .dom(CHARTS.container('Vault client counts')) .exists('Shows running totals with monthly breakdown charts'); @@ -228,15 +228,8 @@ module('Acceptance | clients | overview | sync in license, activated', function hooks.beforeEach(async function () { sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - clientsHandler(this.server); - this.store = this.owner.lookup('service:store'); - // add feature to license - this.server.get('/sys/license/features', () => ({ features: ['Secrets Sync'] })); - // activate feature - this.server.get('/sys/activation-flags', () => ({ - data: { activated: ['secrets-sync'], unactivated: [] }, - })); + syncHandler(this.server); await authPage.login(); return visit('/vault/clients/counts/overview'); @@ -246,12 +239,16 @@ module('Acceptance | clients | overview | sync in license, activated', function assert.dom(GENERAL.tab('sync')).exists(); }); - test('it should show secrets sync data in overview and tab', async function (assert) { + test('it should show secrets sync stats', async function (assert) { assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists('shows secret sync data on overview'); + }); + + test('it should navigate to secrets sync page', async function (assert) { await click(GENERAL.tab('sync')); assert.dom(GENERAL.tab('sync')).hasClass('active'); assert.dom(GENERAL.emptyStateTitle).doesNotExist(); + assert .dom(CHARTS.chart('Secrets sync usage')) .exists('chart is shown because feature is active and has data'); @@ -263,7 +260,6 @@ module('Acceptance | clients | overview | sync in license, not activated', funct setupMirage(hooks); hooks.beforeEach(async function () { - this.store = this.owner.lookup('service:store'); this.server.get('/sys/license/features', () => ({ features: ['Secrets Sync'] })); await authPage.login(); @@ -274,12 +270,11 @@ module('Acceptance | clients | overview | sync in license, not activated', funct assert.dom(GENERAL.tab('sync')).exists('sync tab is shown because feature is in license'); }); - test('it should hide secrets sync charts', async function (assert) { + test('it should hide secrets sync stats', async function (assert) { assert - .dom(CHARTS.chart('Secrets sync usage')) - .doesNotExist('chart is hidden because feature is not activated'); - - assert.dom('[data-test-stat-text="secret-syncs"]').doesNotExist(); + .dom(CLIENT_COUNT.statTextValue('Secret sync')) + .doesNotExist('stat is hidden because feature is not activated'); + assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists('other stats are still visible'); }); }); @@ -288,7 +283,6 @@ module('Acceptance | clients | overview | sync not in license', function (hooks) setupMirage(hooks); hooks.beforeEach(async function () { - this.store = this.owner.lookup('service:store'); // mocks endpoint for no additional license modules this.server.get('/sys/license/features', () => ({ features: [] })); @@ -300,9 +294,30 @@ module('Acceptance | clients | overview | sync not in license', function (hooks) assert.dom(GENERAL.tab('sync')).doesNotExist(); }); - test('it should hide secrets sync charts', async function (assert) { - assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist(); + test('it should hide secrets sync stats', async function (assert) { + assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).doesNotExist(); + assert.dom(CLIENT_COUNT.statTextValue('Entity')).exists('other stats are still visible'); + }); +}); + +module('Acceptance | clients | overview | HVD', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); + syncHandler(this.server); + this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; + + await authPage.login(); + return visit('/vault/clients/counts/overview'); + }); + + test('it should show the secrets sync tab', async function (assert) { + assert.dom(GENERAL.tab('sync')).exists(); + }); - assert.dom('[data-test-stat-text="secret-syncs"]').doesNotExist(); + test('it should show secrets sync stats', async function (assert) { + assert.dom(CLIENT_COUNT.statTextValue('Secret sync')).exists(); }); }); diff --git a/ui/tests/acceptance/clients/counts/sync-test.js b/ui/tests/acceptance/clients/counts/sync-test.js index 36b2fa285891..6dfb2a823815 100644 --- a/ui/tests/acceptance/clients/counts/sync-test.js +++ b/ui/tests/acceptance/clients/counts/sync-test.js @@ -15,71 +15,57 @@ import authPage from 'vault/tests/pages/auth'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors'; -module('Acceptance | clients | sync | activated', function (hooks) { +module('Acceptance | clients | sync', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + module('sync activated', function (hooks) { + hooks.beforeEach(async function () { + sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); + syncHandler(this.server); + await authPage.login(); + return visit('/vault/clients/counts/sync'); + }); - hooks.beforeEach(async function () { - sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - syncHandler(this.server); - await authPage.login(); - return visit('/vault/clients/counts/sync'); + test('it should render charts when secrets sync is activated', async function (assert) { + syncHandler(this.server); + assert.dom(CHARTS.chart('Secrets sync usage')).exists('Secrets sync usage chart is rendered'); + assert.dom(CLIENT_COUNT.statText('Total sync clients')).exists('Total sync clients chart is rendered'); + assert.dom(GENERAL.emptyStateTitle).doesNotExist(); + }); }); - test('it should render charts when secrets sync is activated', async function (assert) { - syncHandler(this.server); - assert.dom(CHARTS.chart('Secrets sync usage')).exists('Secrets sync usage chart is rendered'); - assert.dom(CLIENT_COUNT.statText('Total sync clients')).exists('Total sync clients chart is rendered'); - assert.dom(GENERAL.emptyStateTitle).doesNotExist(); - }); -}); - -module('Acceptance | clients | sync | not activated', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - this.server.get('/sys/internal/counters/config', function () { - return CONFIG_RESPONSE; + module('sync not activated', function (hooks) { + hooks.beforeEach(async function () { + this.server.get('/sys/internal/counters/config', function () { + return CONFIG_RESPONSE; + }); + sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); + await authPage.login(); + return visit('/vault/clients/counts/sync'); }); - sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - await authPage.login(); - return visit('/vault/clients/counts/sync'); - }); - test('it should show an empty state when secrets sync is not activated', async function (assert) { - assert.expect(3); + test('it should show an empty state when secrets sync is not activated', async function (assert) { + assert.expect(3); + + this.server.get('/sys/activation-flags', () => { + assert.true(true, '/sys/activation-flags/ is called to check if secrets-sync is activated'); - // ensure secret_syncs clients activity is 0 - this.server.get('/sys/internal/counters/activity', () => { - // return only the things that determine whether to show/hide secrets sync - return { - data: { - total: { - secret_syncs: 0, + return { + data: { + activated: [], + unactivated: ['secrets-sync'], }, - }, - }; - }); + }; + }); - this.server.get('/sys/activation-flags', () => { - assert.true(true, '/sys/activation-flags/ is called to check if secrets-sync is activated'); + assert.dom(GENERAL.emptyStateTitle).exists('Shows empty state when secrets-sync is not activated'); - return { - data: { - activated: [], - unactivated: ['secrets-sync'], - }, - }; + await click(`${GENERAL.emptyStateActions} .hds-link-standalone`); + assert.strictEqual( + currentURL(), + '/vault/sync/secrets/overview', + 'action button navigates to secrets sync overview page' + ); }); - - assert.dom(GENERAL.emptyStateTitle).exists('Shows empty state when secrets-sync is not activated'); - - await click(`${GENERAL.emptyStateActions} .hds-link-standalone`); - assert.strictEqual( - currentURL(), - '/vault/sync/secrets/overview', - 'action button navigates to secrets sync overview page' - ); }); }); diff --git a/ui/tests/acceptance/managed-namespace-test.js b/ui/tests/acceptance/managed-namespace-test.js index 3bf8581211d1..c38480e630f7 100644 --- a/ui/tests/acceptance/managed-namespace-test.js +++ b/ui/tests/acceptance/managed-namespace-test.js @@ -9,10 +9,6 @@ import { setupApplicationTest } from 'ember-qunit'; import Pretender from 'pretender'; import { getManagedNamespace } from 'vault/routes/vault/cluster'; -const FEATURE_FLAGS_RESPONSE = { - feature_flags: ['VAULT_CLOUD_ADMIN_NAMESPACE'], -}; - module('Acceptance | Enterprise | Managed namespace root', function (hooks) { setupApplicationTest(hooks); @@ -22,10 +18,9 @@ module('Acceptance | Enterprise | Managed namespace root', function (hooks) { * we have to populate them on the beforeEach hook because * the fetch won't trigger again within the tests */ + + this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; this.server = new Pretender(function () { - this.get('/v1/sys/internal/ui/feature-flags', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify(FEATURE_FLAGS_RESPONSE)]; - }); this.get('/v1/sys/health', this.passthrough); this.get('/v1/sys/seal-status', this.passthrough); this.get('/v1/sys/license/features', this.passthrough); diff --git a/ui/tests/acceptance/mfa-login-test.js b/ui/tests/acceptance/mfa-login-test.js index 5154a6647b39..d2dc5018135a 100644 --- a/ui/tests/acceptance/mfa-login-test.js +++ b/ui/tests/acceptance/mfa-login-test.js @@ -8,6 +8,7 @@ import { setupApplicationTest } from 'ember-qunit'; import { click, currentRouteName, fillIn, visit, waitUntil, find, waitFor } from '@ember/test-helpers'; import { setupMirage } from 'ember-cli-mirage/test-support'; import mfaLoginHandler, { validationHandler } from '../../mirage/handlers/mfa-login'; +import { GENERAL } from '../helpers/general-selectors'; module('Acceptance | mfa-login', function (hooks) { setupApplicationTest(hooks); @@ -181,7 +182,7 @@ module('Acceptance | mfa-login', function (hooks) { assert.dom('[data-test-auth-form]').doesNotExist('Auth form hidden when mfa fails'); assert.dom('[data-test-empty-state-title]').hasText('Unauthorized', 'Error title renders'); assert - .dom('[data-test-empty-state-subText]') + .dom(GENERAL.emptyStateSubtitle) .hasText('PingId MFA validation failed', 'Error message from server renders'); assert .dom('[data-test-empty-state-message]') diff --git a/ui/tests/acceptance/sync/secrets/overview-test.js b/ui/tests/acceptance/sync/secrets/overview-test.js index 0f49d6838ea5..1cc210a364de 100644 --- a/ui/tests/acceptance/sync/secrets/overview-test.js +++ b/ui/tests/acceptance/sync/secrets/overview-test.js @@ -163,7 +163,7 @@ module('Acceptance | sync | overview', function (hooks) { assert.strictEqual( req.requestHeaders['X-Vault-Namespace'], undefined, - 'Request is made to root namespace' + 'Request is made to undefined namespace' ); return {}; }); @@ -177,10 +177,10 @@ module('Acceptance | sync | overview', function (hooks) { await click(ts.overview.optInConfirm); }); - test.skip('it should make activation-flag requests to correct namespace when managed', async function (assert) { - // TODO: unskip for 1.16.1 when managed is supported - assert.expect(3); - this.owner.lookup('service:flags').setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']); + test('it should make activation-flag requests to correct namespace when managed', async function (assert) { + assert.expect(4); + // should call GET activation-flags twice because we need an updated response after activating the feature + this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; this.server.get('/sys/activation-flags', (_, req) => { assert.deepEqual(req.requestHeaders, {}, 'Request is unauthenticated and in root namespace'); @@ -195,7 +195,7 @@ module('Acceptance | sync | overview', function (hooks) { assert.strictEqual( req.requestHeaders['X-Vault-Namespace'], 'admin', - 'Request is made to admin namespace' + 'Request is made to the admin namespace' ); return {}; }); diff --git a/ui/tests/helpers/clients/client-count-selectors.ts b/ui/tests/helpers/clients/client-count-selectors.ts index 9870da1cf942..4d329ff619cb 100644 --- a/ui/tests/helpers/clients/client-count-selectors.ts +++ b/ui/tests/helpers/clients/client-count-selectors.ts @@ -41,6 +41,7 @@ export const CLIENT_COUNT = { customEndMonth: '[data-test-show-calendar]', previousYear: '[data-test-previous-year]', nextYear: '[data-test-next-year]', + displayYear: '[data-test-display-year]', calendarMonth: (month: string) => `[data-test-calendar-month="${month}"]`, }, selectedAuthMount: 'div#mounts-search-select [data-test-selected-option] div', diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index a4cc9bbe51ad..49260cb17b64 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -24,6 +24,7 @@ export const GENERAL = { confirmButton: '[data-test-confirm-button]', confirmTrigger: '[data-test-confirm-action-trigger]', emptyStateTitle: '[data-test-empty-state-title]', + emptyStateSubtitle: '[data-test-empty-state-subtitle]', emptyStateMessage: '[data-test-empty-state-message]', emptyStateActions: '[data-test-empty-state-actions]', menuTrigger: '[data-test-popup-menu-trigger]', diff --git a/ui/tests/helpers/sync/sync-selectors.js b/ui/tests/helpers/sync/sync-selectors.js index 9a7123b45bd9..db3b37cbe302 100644 --- a/ui/tests/helpers/sync/sync-selectors.js +++ b/ui/tests/helpers/sync/sync-selectors.js @@ -53,6 +53,8 @@ export const PAGE = { overview: { optInBanner: '[data-test-secrets-sync-opt-in-banner]', optInBannerEnable: '[data-test-secrets-sync-opt-in-banner-enable]', + optInBannerDescription: '[data-test-secrets-sync-opt-in-banner-description]', + optInDismiss: '[data-test-secrets-sync-opt-in-banner] [data-test-icon="x"]', optInModal: '[data-test-secrets-sync-opt-in-modal]', optInCheck: '[data-test-opt-in-check]', optInConfirm: '[data-test-opt-in-confirm]', @@ -70,7 +72,7 @@ export const PAGE = { action: (name) => `[data-test-overview-table-action="${name}"]`, }, }, - syncBadge: { + badgeText: { icon: (name) => `[data-test-icon="${name}"]`, text: '.hds-badge__text', }, diff --git a/ui/tests/integration/components/clients/counts/nav-bar-test.js b/ui/tests/integration/components/clients/counts/nav-bar-test.js new file mode 100644 index 000000000000..eed377b88fe0 --- /dev/null +++ b/ui/tests/integration/components/clients/counts/nav-bar-test.js @@ -0,0 +1,44 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; + +module('Integration | Component | clients/counts/nav-bar', function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.showSecretsSync = false; + + this.renderComponent = async () => { + await render(hbs``); + }; + }); + + test('it renders default tabs', async function (assert) { + await this.renderComponent(); + + assert.dom(GENERAL.tab('overview')).hasText('Overview'); + assert.dom(GENERAL.tab('token')).hasText('Entity/Non-entity clients'); + assert.dom(GENERAL.tab('acme')).hasText('ACME clients'); + }); + + test('it shows secrets sync tab if showSecretsSync is true', async function (assert) { + this.showSecretsSync = true; + await this.renderComponent(); + + assert.dom(GENERAL.tab('sync')).exists(); + }); + + test('it should not show secrets sync tab if showSecretsSync is false', async function (assert) { + this.showSecretsSync = false; + await this.renderComponent(); + + assert.dom(GENERAL.tab('sync')).doesNotExist(); + }); +}); diff --git a/ui/tests/integration/components/clients/line-chart-test.js b/ui/tests/integration/components/clients/line-chart-test.js index 586f4f8e254e..a6f1e9a74761 100644 --- a/ui/tests/integration/components/clients/line-chart-test.js +++ b/ui/tests/integration/components/clients/line-chart-test.js @@ -10,6 +10,7 @@ import { find, render, findAll } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { format, formatRFC3339, subMonths } from 'date-fns'; import timestamp from 'core/utils/timestamp'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; module('Integration | Component | clients/line-chart', function (hooks) { setupRenderingTest(hooks); @@ -270,7 +271,7 @@ module('Integration | Component | clients/line-chart', function (hooks) { assert.dom('[data-test-component="empty-state"]').exists('renders empty state when no data'); assert - .dom('[data-test-empty-state-subtext]') + .dom(GENERAL.emptyStateSubtitle) .hasText( `this is a custom message to explain why you're not seeing a line chart`, 'custom message renders' diff --git a/ui/tests/integration/components/clients/page/sync-test.js b/ui/tests/integration/components/clients/page/sync-test.js index 921021e092cd..da6bb1de6249 100644 --- a/ui/tests/integration/components/clients/page/sync-test.js +++ b/ui/tests/integration/components/clients/page/sync-test.js @@ -8,7 +8,8 @@ import { setupRenderingTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, findAll } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; -import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; +import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; +import syncHandler from 'vault/mirage/handlers/sync'; import { getUnixTime } from 'date-fns'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { CLIENT_COUNT, CHARTS } from 'vault/tests/helpers/clients/client-count-selectors'; @@ -26,22 +27,9 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook setupMirage(hooks); hooks.beforeEach(async function () { - clientsHandler(this.server); - this.store = this.owner.lookup('service:store'); - const activityQuery = { - start_time: { timestamp: START_TIME }, - end_time: { timestamp: END_TIME }, - }; - // set this to 0 - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - this.startTimestamp = START_TIME; - this.endTimestamp = END_TIME; - this.isSecretsSyncActivated = true; - this.renderComponent = () => render(hbs` m?.new_clients), - 'secret_syncs' - ), - ]); - await this.renderComponent(); - assert - .dom(statText('Total sync clients')) - .hasText( - `Total sync clients The total number of secrets synced from Vault to other destinations during this date range. ${expectedTotal}`, - `renders correct total sync stat ${expectedTotal}` - ); - assert - .dom(statText('Average sync clients per month')) - .hasText( - `Average sync clients per month ${expectedAvg}`, - `renders correct average sync stat ${expectedAvg}` - ); - assert.dom(statText('Average new sync clients per month')).hasTextContaining(`${expectedNewAvg}`); - - const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], { - withTimeZone: true, - }); - assert.dom(CHARTS.timestamp).hasText(`Updated ${formattedTimestamp}`, 'renders response timestamp'); + module('with secrets sync not activated', function () { + test('it should render an empty state', async function (assert) { + await this.renderComponent(); - assertBarChart(assert, 'Secrets sync usage', this.activity.byMonth); - assertBarChart(assert, 'Monthly new', this.activity.byMonth); - }); + assert.dom(GENERAL.emptyStateTitle).hasText('No Secrets Sync clients'); + assert + .dom(GENERAL.emptyStateMessage) + .hasText('No data is available because Secrets Sync has not been activated.'); + assert.dom(GENERAL.emptyStateActions).hasText('Activate Secrets Sync'); - test('it should render stats without chart for a single month', async function (assert) { - assert.expect(5); - const activityQuery = { start_time: { timestamp: END_TIME }, end_time: { timestamp: END_TIME } }; - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - const expectedTotal = formatNumber([this.activity.total.secret_syncs]); - await this.renderComponent(); - - assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist('total usage chart does not render'); - assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render'); - assert.dom(statText('Average sync clients per month')).doesNotExist(); - assert.dom(statText('Average new sync clients per month')).doesNotExist(); - assert - .dom(usageStats('Secrets sync usage')) - .hasText( - `Secrets sync usage Usage metrics tutorial This data can be used to understand how many secrets sync clients have been used for this date range. Each Vault secret that is synced to at least one destination counts as one Vault client. Total sync clients ${expectedTotal}`, - 'it renders usage stats with single month copy' - ); + assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist(); + assert.dom(statText('Total sync clients')).doesNotExist(); + assert.dom(statText('Average sync clients per month')).doesNotExist(); + }); }); - // EMPTY STATES - test('it should render empty state when sync data does not exist for a date range', async function (assert) { - assert.expect(8); - // this happens when a user queries historical data that predates the monthly breakdown (added in 1.11) - // only entity + non-entity clients existed then, so we show an empty state for sync clients - // because the activity response just returns { secret_syncs: 0 } which isn't very clear - this.activity.byMonth = []; - - await this.renderComponent(); - - assert.dom(GENERAL.emptyStateTitle).hasText('No secrets sync clients'); - assert.dom(GENERAL.emptyStateMessage).hasText('There is no sync data available for this date range.'); - - assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist('vertical bar chart does not render'); - assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render'); - assert.dom(statText('Total sync clients')).doesNotExist(); - assert.dom(statText('Average sync clients per month')).doesNotExist(); - assert.dom(statText('Average new sync clients per month')).doesNotExist(); - assert.dom(usageStats('Secrets sync usage')).doesNotExist(); - }); + module('with secrets sync activated', function (hooks) { + hooks.beforeEach(async function () { + syncHandler(this.server); + this.owner.lookup('service:flags').activatedFlags = ['secrets-sync']; + + this.store = this.owner.lookup('service:store'); + const activityQuery = { + start_time: { timestamp: START_TIME }, + end_time: { timestamp: END_TIME }, + }; + // set this to 0 + this.activity = await this.store.queryRecord('clients/activity', activityQuery); + this.startTimestamp = START_TIME; + this.endTimestamp = END_TIME; + }); - test('it should render empty state when sync data does not exist for a single month', async function (assert) { - assert.expect(1); - const activityQuery = { start_time: { timestamp: START_TIME }, end_time: { timestamp: START_TIME } }; - this.activity = await this.store.queryRecord('clients/activity', activityQuery); - this.activity.byMonth = []; - await this.renderComponent(); + test('it should render with full month activity data', async function (assert) { + const monthCount = this.activity.byMonth.length; + assert.expect(8 + monthCount * 2); + const expectedTotal = formatNumber([this.activity.total.secret_syncs]); + const expectedAvg = formatNumber([calculateAverage(this.activity.byMonth, 'secret_syncs')]); + const expectedNewAvg = formatNumber([ + calculateAverage( + this.activity.byMonth.map((m) => m?.new_clients), + 'secret_syncs' + ), + ]); + await this.renderComponent(); - assert.dom(GENERAL.emptyStateMessage).hasText('There is no sync data available for this month.'); - }); + assert + .dom(statText('Total sync clients')) + .hasText( + `Total sync clients The total number of secrets synced from Vault to other destinations during this date range. ${expectedTotal}`, + `renders correct total sync stat ${expectedTotal}` + ); + assert + .dom(statText('Average sync clients per month')) + .hasText( + `Average sync clients per month ${expectedAvg}`, + `renders correct average sync stat ${expectedAvg}` + ); + assert.dom(statText('Average new sync clients per month')).hasTextContaining(`${expectedNewAvg}`); - test('it should render an empty state if secrets sync is not activated', async function (assert) { - this.isSecretsSyncActivated = false; + const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], { + withTimeZone: true, + }); + assert.dom(CHARTS.timestamp).hasText(`Updated ${formattedTimestamp}`, 'renders response timestamp'); + + assertBarChart(assert, 'Secrets sync usage', this.activity.byMonth); + assertBarChart(assert, 'Monthly new', this.activity.byMonth); + }); + + test('it should render stats without chart for a single month', async function (assert) { + assert.expect(5); + const activityQuery = { start_time: { timestamp: END_TIME }, end_time: { timestamp: END_TIME } }; + this.activity = await this.store.queryRecord('clients/activity', activityQuery); + const expectedTotal = formatNumber([this.activity.total.secret_syncs]); + await this.renderComponent(); + + assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist('total usage chart does not render'); + assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render'); + assert.dom(statText('Average sync clients per month')).doesNotExist(); + assert.dom(statText('Average new sync clients per month')).doesNotExist(); + assert + .dom(usageStats('Secrets sync usage')) + .hasText( + `Secrets sync usage Usage metrics tutorial This data can be used to understand how many secrets sync clients have been used for this date range. Each Vault secret that is synced to at least one destination counts as one Vault client. Total sync clients ${expectedTotal}`, + 'it renders usage stats with single month copy' + ); + }); - await this.renderComponent(); + // EMPTY STATES + test('it should render empty state when sync data does not exist for a date range', async function (assert) { + assert.expect(8); + // this happens when a user queries historical data that predates the monthly breakdown (added in 1.11) + // only entity + non-entity clients existed then, so we show an empty state for sync clients + // because the activity response just returns { secret_syncs: 0 } which isn't very clear + this.activity.byMonth = []; + + await this.renderComponent(); + + assert.dom(GENERAL.emptyStateTitle).hasText('No secrets sync clients'); + assert.dom(GENERAL.emptyStateMessage).hasText('There is no sync data available for this date range.'); + + assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist('vertical bar chart does not render'); + assert.dom(CHARTS.container('Monthly new')).doesNotExist('monthly new chart does not render'); + assert.dom(statText('Total sync clients')).doesNotExist(); + assert.dom(statText('Average sync clients per month')).doesNotExist(); + assert.dom(statText('Average new sync clients per month')).doesNotExist(); + assert.dom(usageStats('Secrets sync usage')).doesNotExist(); + }); - assert.dom(GENERAL.emptyStateTitle).hasText('No Secrets Sync clients'); - assert - .dom(GENERAL.emptyStateMessage) - .hasText('No data is available because Secrets Sync has not been activated.'); - assert.dom(GENERAL.emptyStateActions).hasText('Activate Secrets Sync'); + test('it should render empty state when sync data does not exist for a single month', async function (assert) { + assert.expect(1); + const activityQuery = { start_time: { timestamp: START_TIME }, end_time: { timestamp: START_TIME } }; + this.activity = await this.store.queryRecord('clients/activity', activityQuery); + this.activity.byMonth = []; + await this.renderComponent(); - assert.dom(CHARTS.chart('Secrets sync usage')).doesNotExist(); - assert.dom(statText('Total sync clients')).doesNotExist(); - assert.dom(statText('Average sync clients per month')).doesNotExist(); - }); + assert.dom(GENERAL.emptyStateMessage).hasText('There is no sync data available for this month.'); + }); - test('it should render an empty total usage chart if secrets sync is activated but monthly syncs are null or 0', async function (assert) { - this.isSecretsSyncActivated = true; - // manually stub because mirage isn't setup to handle mixed data yet - const counts = { - clients: 10, - entity_clients: 4, - non_entity_clients: 6, - secret_syncs: 0, - }; - const monthData = { - month: '1/24', - timestamp: '2024-01-01T00:00:00-08:00', - ...counts, - namespaces: [ + test('it should render an empty total usage chart if secrets sync is activated but monthly syncs are null or 0', async function (assert) { + // manually stub because mirage isn't setup to handle mixed data yet + const counts = { + clients: 10, + entity_clients: 4, + non_entity_clients: 6, + secret_syncs: 0, + }; + const monthData = { + month: '1/24', + timestamp: '2024-01-01T00:00:00-08:00', + ...counts, + namespaces: [ + { + label: 'root', + ...counts, + mounts: [], + }, + ], + }; + this.activity.byMonth = [ { - label: 'root', - ...counts, - mounts: [], - }, - ], - }; - this.activity.byMonth = [ - { - ...monthData, - namespaces_by_key: { - root: { + ...monthData, + namespaces_by_key: { + root: { + ...monthData, + mounts_by_key: {}, + }, + }, + new_clients: { ...monthData, - mounts_by_key: {}, }, }, - new_clients: { - ...monthData, - }, - }, - ]; - this.activity.total = counts; - const monthCount = this.activity.byMonth.length; - assert.expect(6 + monthCount * 2); - await this.renderComponent(); - - assert.dom(CHARTS.chart('Secrets sync usage')).exists('renders empty sync usage chart'); - assert - .dom(statText('Total sync clients')) - .hasText( - 'Total sync clients The total number of secrets synced from Vault to other destinations during this date range. 0' - ); - assert - .dom(statText('Average sync clients per month')) - .doesNotExist('Does not render average if the calculation is 0'); - findAll(`${CHARTS.chart('Secrets sync usage')} ${CHARTS.xAxisLabel}`).forEach((e, i) => { + ]; + this.activity.total = counts; + const monthCount = this.activity.byMonth.length; + assert.expect(6 + monthCount * 2); + await this.renderComponent(); + + assert.dom(CHARTS.chart('Secrets sync usage')).exists('renders empty sync usage chart'); assert - .dom(e) + .dom(statText('Total sync clients')) .hasText( - `${this.activity.byMonth[i].month}`, - `renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}` + 'Total sync clients The total number of secrets synced from Vault to other destinations during this date range. 0' ); - }); - findAll(`${CHARTS.chart('Secrets sync usage')} ${CHARTS.verticalBar}`).forEach((e, i) => { - assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`); - }); + assert + .dom(statText('Average sync clients per month')) + .doesNotExist('Does not render average if the calculation is 0'); + findAll(`${CHARTS.chart('Secrets sync usage')} ${CHARTS.xAxisLabel}`).forEach((e, i) => { + assert + .dom(e) + .hasText( + `${this.activity.byMonth[i].month}`, + `renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}` + ); + }); + findAll(`${CHARTS.chart('Secrets sync usage')} ${CHARTS.verticalBar}`).forEach((e, i) => { + assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`); + }); - assert - .dom(CHARTS.container('Monthly new')) - .doesNotExist('empty monthly new chart does not render at all'); - assert.dom(statText('Average sync clients per month')).doesNotExist(); - assert.dom(statText('Average new sync clients per month')).doesNotExist(); + assert + .dom(CHARTS.container('Monthly new')) + .doesNotExist('empty monthly new chart does not render at all'); + assert.dom(statText('Average sync clients per month')).doesNotExist(); + assert.dom(statText('Average new sync clients per month')).doesNotExist(); + }); }); }); diff --git a/ui/tests/integration/components/sidebar/nav/cluster-test.js b/ui/tests/integration/components/sidebar/nav/cluster-test.js index e1008d2a5c56..9e76b280b12e 100644 --- a/ui/tests/integration/components/sidebar/nav/cluster-test.js +++ b/ui/tests/integration/components/sidebar/nav/cluster-test.js @@ -49,6 +49,7 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { test('it should hide links and headings user does not have access too', async function (assert) { await renderComponent(); + assert .dom('[data-test-sidebar-nav-link]') .exists({ count: 3 }, 'Nav links are hidden other than secrets, secrets sync and dashboard'); @@ -85,7 +86,8 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { test('it should render badge for promotional links on community version', async function (assert) { const promotionalLinks = ['Secrets Sync']; - stubFeaturesAndPermissions(this.owner, false, true); + // if no features passed, it defaults to all features and we need to specifically remove Secrets Sync + stubFeaturesAndPermissions(this.owner, false, true, []); await renderComponent(); promotionalLinks.forEach((link) => { @@ -135,13 +137,16 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { }); }); - test('it should not show sync links for managed cluster', async function (assert) { - this.owner.lookup('service:flags').setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']); - stubFeaturesAndPermissions(this.owner, true, true, ['Secrets Sync']); + test('it should render badge for promotional links on managed clusters', async function (assert) { + this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; + const promotionalLinks = ['Secrets Sync']; + stubFeaturesAndPermissions(this.owner, true, true); await renderComponent(); - assert - .dom(`[data-test-sidebar-nav-link="Secrets Sync"]`) - .doesNotExist(`Secret Sync is hidden in managed vault`); + promotionalLinks.forEach((link) => { + assert + .dom(`[data-test-sidebar-nav-link="${link}"]`) + .hasText(`${link} Plus`, `${link} link renders Plus badge`); + }); }); }); diff --git a/ui/tests/integration/components/sync-status-badge-test.js b/ui/tests/integration/components/sync-status-badge-test.js index 5624f9b9dca6..21419c5e5756 100644 --- a/ui/tests/integration/components/sync-status-badge-test.js +++ b/ui/tests/integration/components/sync-status-badge-test.js @@ -35,8 +35,8 @@ module('Integration | Component | SyncStatusBadge', function (hooks) { test('it should render when status does not exist', async function (assert) { assert.expect(2); await this.renderComponent(); - assert.dom(PAGE.syncBadge.icon('help')).exists('renders help icon'); - assert.dom(PAGE.syncBadge.text).hasText(this.status); + assert.dom(PAGE.badgeText.icon('help')).exists('renders help icon'); + assert.dom(PAGE.badgeText.text).hasText(this.status); }); test('it renders badge and icon for each status type', async function (assert) { @@ -46,8 +46,8 @@ module('Integration | Component | SyncStatusBadge', function (hooks) { const label = toLabel([status]); const { icon, color } = SYNC_STATUSES[status]; await this.renderComponent(); - assert.dom(PAGE.syncBadge.icon(icon)).exists(`status: ${status} renders icon: ${icon}`); - assert.dom(PAGE.syncBadge.text).hasText(label, `status: ${status} renders label: ${label}`); + assert.dom(PAGE.badgeText.icon(icon)).exists(`status: ${status} renders icon: ${icon}`); + assert.dom(PAGE.badgeText.text).hasText(label, `status: ${status} renders label: ${label}`); assert .dom('[data-test-badge]') .hasClass(`hds-badge--color-${color}`, `status: ${status} renders color: ${color}`); diff --git a/ui/tests/integration/components/sync/secrets/landing-cta-test.js b/ui/tests/integration/components/sync/secrets/landing-cta-test.js index 0822050b6e52..919b4c794c7c 100644 --- a/ui/tests/integration/components/sync/secrets/landing-cta-test.js +++ b/ui/tests/integration/components/sync/secrets/landing-cta-test.js @@ -23,44 +23,31 @@ module('Integration | Component | sync | Secrets::LandingCta', function (hooks) this.transitionStub = sinon.stub(this.owner.lookup('service:router'), 'transitionTo'); }); - test('it should render promotional copy for community or enterprise version without feature', async function (assert) { - await render(hbs` `, { + test('it should render promotional copy if feature is not activated', async function (assert) { + await render(hbs` `, { owner: this.engine, }); assert .dom(cta.summary) .hasText( - 'This premium enterprise feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need them. Learn more about Secrets Sync' + 'This feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need them. Learn more about Secrets Sync' ); assert.dom(cta.link).hasText('Learn more about Secrets Sync'); assert.dom(cta.button).doesNotExist('does not render create destination button'); }); - test('it should render CTA copy but not action when feature exists on enterprise license and is not activated', async function (assert) { - await render(hbs` `, { - owner: this.engine, - }); - assert - .dom(cta.summary) - .hasText( - 'Sync secrets to platforms and tools across your stack to get secrets when and where you need them. Secrets Sync tutorial' - ); - assert.dom(cta.link).hasText('Secrets Sync tutorial'); - assert.dom(cta.button).doesNotExist('does not render create destination button'); - }); - - test('it should render CTA copy and action when feature exists on enterprise license and is activated', async function (assert) { - await render(hbs` `, { + test('it should render CTA copy and action if feature is activated', async function (assert) { + await render(hbs` `, { owner: this.engine, }); assert .dom(cta.summary) .hasText( - 'Sync secrets to platforms and tools across your stack to get secrets when and where you need them. Secrets Sync tutorial' + 'This feature allows you to sync secrets to platforms and tools across your stack to get secrets when and where you need them. Learn more about Secrets Sync' ); - assert.dom(cta.link).hasText('Secrets Sync tutorial'); + assert.dom(cta.link).hasText('Learn more about Secrets Sync'); assert.dom(cta.button).exists('it renders create destination button'); }); }); diff --git a/ui/tests/integration/components/sync/secrets/page/overview-test.js b/ui/tests/integration/components/sync/secrets/page/overview-test.js index 1f9ab3426ac8..9e421e1b683c 100644 --- a/ui/tests/integration/components/sync/secrets/page/overview-test.js +++ b/ui/tests/integration/components/sync/secrets/page/overview-test.js @@ -15,6 +15,7 @@ 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; @@ -24,19 +25,24 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { 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); - const store = this.owner.lookup('service:store'); - this.destinations = await store.query('sync/destination', {}); + this.destinations = await this.store.query('sync/destination', {}); this.isActivated = true; + this.licenseHasSecretsSync = true; + this.isHvdManaged = false; this.renderComponent = () => { return render( - hbs``, + hbs``, { owner: this.engine, } @@ -57,9 +63,9 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { module('community', function (hooks) { hooks.beforeEach(function () { this.version.type = 'community'; - this.version.features = []; - this.destinations = []; this.isActivated = false; + this.licenseHasSecretsSync = false; + this.destinations = []; }); test('it should show an upsell CTA', async function (assert) { @@ -69,11 +75,13 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { .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 = []; }); @@ -90,6 +98,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { 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'); @@ -98,15 +107,72 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { }); }); - module('secrets sync not activated', function (hooks) { + 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', async function (assert) { + 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) { @@ -152,6 +218,18 @@ module('Integration | Component | sync | Page::Overview', function (hooks) { }); }); + 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(); diff --git a/ui/tests/integration/components/sync/sync-header-test.js b/ui/tests/integration/components/sync/sync-header-test.js index 43adba81bb4c..7f387bcb9ef0 100644 --- a/ui/tests/integration/components/sync/sync-header-test.js +++ b/ui/tests/integration/components/sync/sync-header-test.js @@ -18,6 +18,7 @@ module('Integration | Component | sync | SyncHeader', function (hooks) { hooks.beforeEach(function () { this.version = this.owner.lookup('service:version'); + this.flags = this.owner.lookup('service:flags'); this.title = 'Secrets Sync'; this.renderComponent = () => { return render(hbs``, { @@ -64,6 +65,18 @@ module('Integration | Component | sync | SyncHeader', function (hooks) { }); }); + module('managed', function (hooks) { + hooks.beforeEach(function () { + this.version.type = 'enterprise'; + this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; + }); + + test('it should render title and plus badge', async function (assert) { + await this.renderComponent(); + assert.dom(title).hasText('Secrets Sync Plus feature'); + }); + }); + test('it should yield actions block', async function (assert) { await render( hbs` diff --git a/ui/tests/unit/services/flags-test.js b/ui/tests/unit/services/flags-test.js index b2186f4a56c3..782a3fb34321 100644 --- a/ui/tests/unit/services/flags-test.js +++ b/ui/tests/unit/services/flags-test.js @@ -14,6 +14,10 @@ const ACTIVATED_FLAGS_RESPONSE = { }, }; +const FEATURE_FLAGS_RESPONSE = { + feature_flags: ['VAULT_CLOUD_ADMIN_NAMESPACE'], +}; + module('Unit | Service | flags', function (hooks) { setupTest(hooks); setupMirage(hooks); @@ -23,15 +27,10 @@ module('Unit | Service | flags', function (hooks) { }); test('it loads with defaults', function (assert) { - assert.deepEqual(this.service.flags, [], 'Flags are empty until fetched'); + assert.deepEqual(this.service.featureFlags, [], 'Flags are empty until fetched'); assert.deepEqual(this.service.activatedFlags, [], 'Activated flags are empty until fetched'); }); - test('#setFeatureFlags: it can set feature flags', function (assert) { - this.service.setFeatureFlags(['foo', 'bar']); - assert.deepEqual(this.service.flags, ['foo', 'bar'], 'Flags are set'); - }); - module('#fetchActivatedFlags', function (hooks) { hooks.beforeEach(function () { this.owner.lookup('service:version').type = 'enterprise'; @@ -75,22 +74,45 @@ module('Unit | Service | flags', function (hooks) { }); }); - module('#managedNamespaceRoot', function () { + module('#fetchFeatureFlags', function (hooks) { + hooks.beforeEach(function () { + this.owner.lookup('service:version').type = 'enterprise'; + }); + + test('it returns feature flags', async function (assert) { + assert.expect(2); + + this.server.get('sys/internal/ui/feature-flags', () => { + assert.true(true, 'GET request made to feature-flags endpoint'); + return FEATURE_FLAGS_RESPONSE; + }); + + await this.service.fetchFeatureFlags(); + + assert.deepEqual( + this.service.featureFlags, + FEATURE_FLAGS_RESPONSE.feature_flags, + 'Feature flags are fetched and set' + ); + }); + }); + + module('#hvdManagedNamespaceRoot', function () { test('it returns null when flag is not present', function (assert) { - assert.strictEqual(this.service.managedNamespaceRoot, null); + assert.strictEqual(this.service.hvdManagedNamespaceRoot, null); }); test('it returns the namespace root when flag is present', function (assert) { - this.service.setFeatureFlags(['VAULT_CLOUD_ADMIN_NAMESPACE']); + this.service.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE']; assert.strictEqual( - this.service.managedNamespaceRoot, + this.service.hvdManagedNamespaceRoot, 'admin', 'Managed namespace is admin when flag present' ); - this.service.setFeatureFlags(['SOMETHING_ELSE']); + this.service.featureFlags = ['SOMETHING_ELSE']; assert.strictEqual( - this.service.managedNamespaceRoot, + this.service.hvdManagedNamespaceRoot, null, 'Flags were overwritten and root namespace is null again' );