UI: Build KV v2 overview page (#28106)

* move date-from-now helper to addon

* make overview cards consistent across engines

* make kv-paths-card component

* remove overview margin all together

* small styling changes for paths card

* small selector additions

* add overview card test

* add overview page and test

* add default timestamp format

* cleanup paths test

* fix dateFromNow import

* fix selectors, cleanup pki selectors

* and more selector cleanup

* make deactivated state single arg

* fix template and remove @isDeleted and @isDestroyed

* add test and hide badge unless deactivated

* address failings from changing selectors

* oops, not ready to show overview tab just yet!

* add deletionTime to currentSecret metadata getter
This commit is contained in:
claire bontempo 2024-08-16 14:40:23 -07:00 committed by GitHub
parent 255db7aab1
commit 30da9aef46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 875 additions and 229 deletions

View File

@ -69,11 +69,6 @@ export default class KvSecretMetadataModel extends Model {
return keyIsFolder(this.path);
}
// cannot use isDeleted due to ember property conflict
get isSecretDeleted() {
return isDeleted(this.deletionTime);
}
// turns version object into an array for version dropdown menu
get sortedVersions() {
const array = [];
@ -93,6 +88,7 @@ export default class KvSecretMetadataModel extends Model {
return {
state,
isDeactivated: state !== 'created',
deletionTime: data.deletion_time,
};
}

View File

@ -16,6 +16,7 @@ import { obfuscateData } from 'core/utils/advanced-secret';
* <JsonEditor @title="Policy" @value={{hash foo="bar"}} @viewportMargin={{100}} />
*
* @param {string} [title] - Name above codemirror view
* @param {boolean} [showToolbar=true] - If false, toolbar and title are hidden
* @param {string} value - a specific string the comes from codemirror. It's the value inside the codemirror display
* @param {Function} [valueUpdated] - action to preform when you edit the codemirror value.
* @param {Function} [onFocusOut] - action to preform when you focus out of codemirror.

View File

@ -10,10 +10,14 @@
data-test-overview-card-container={{@cardTitle}}
...attributes
>
<div class="flex row-wrap space-between has-bottom-margin-s" data-test-overview-card={{@cardTitle}}>
<Hds::Text::Display @weight="bold" @size="300" data-test-overview-card-title={{@cardTitle}}>
{{@cardTitle}}
</Hds::Text::Display>
<div class="flex row-wrap space-between" data-test-overview-card={{@cardTitle}}>
{{#if (has-block "customTitle")}}
{{yield to="customTitle"}}
{{else}}
<Hds::Text::Display @weight="semibold" @size="300" data-test-overview-card-title={{@cardTitle}}>
{{@cardTitle}}
</Hds::Text::Display>
{{/if}}
{{#if (has-block "action")}}
{{yield to="action"}}

View File

@ -43,7 +43,7 @@ function dateFromString(str) {
return null;
}
export function dateFormat([value, style], { withTimeZone = false }) {
export function dateFormat([value, style = 'MMM d yyyy, h:mm:ss aa'], { withTimeZone = false }) {
// see format breaking in upgrade to date-fns 2.x https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md#changed-5
let date;
switch (checkType(value)) {

View File

@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/helpers/date-from-now';

View File

@ -0,0 +1,82 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<div data-test-overview-card-container="Paths" ...attributes>
<Hds::Text::Display @weight="semibold" @size="300" @tag="h2">
Paths
</Hds::Text::Display>
{{#if @isCondensed}}
<Hds::Text::Body @tag="p" @color="faint">
The paths to use when referring to this secret in API or CLI.
</Hds::Text::Body>
{{/if}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless {{if @isCondensed 'is-shadowless'}} ">
{{#each this.paths as |path|}}
<InfoTableRow
@label={{path.label}}
@labelWidth={{if @isCondensed "is-one-quarter" "is-one-third"}}
@helperText={{if @isCondensed "" path.text}}
@truncateValue={{true}}
>
<Hds::Copy::Button
@text="Copy"
@isIconOnly={{true}}
@textToCopy={{path.snippet}}
@onError={{fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger")}}
data-test-copy-button={{path.snippet}}
class="transparent"
/>
<code class="is-flex-1 text-overflow-ellipsis has-left-margin-s">
{{path.snippet}}
</code>
</InfoTableRow>
{{/each}}
</div>
{{#unless @isCondensed}}
<Hds::Text::Display @weight="semibold" @size="300" @tag="h3" class="has-top-margin-xl">
Commands
</Hds::Text::Display>
<div class="box is-fullwidth is-sideless">
<h3 class="is-label">
CLI
<Hds::Badge @text="kv get" @color="neutral" />
</h3>
<p class="helper-text has-text-grey-light has-bottom-padding-s">
This command retrieves the value from KV secrets engine at the given key name. See our
<Hds::Link::Inline @isHrefExternal={{true}} @href={{doc-link "/vault/docs/commands/kv"}}>
documentation</Hds::Link::Inline>
for other CLI commands.
</p>
<Hds::CodeBlock
data-test-commands="cli"
@language="bash"
@hasLineNumbers={{false}}
@hasCopyButton={{true}}
@value={{this.commands.cli}}
/>
<h3 class="has-top-margin-l is-label">
API read secret version
</h3>
<p class="helper-text has-text-grey-light has-bottom-padding-s">
This command obtains data and metadata for the latest version of this secret. In this example, Vault is located at
https://127.0.0.1:8200. For other API commands,
<DocLink @path="/vault/api-docs/secret/kv/kv-v2">
learn more.
</DocLink>
</p>
<Hds::CodeBlock
data-test-commands="api"
@language="bash"
@hasLineNumbers={{false}}
@hasCopyButton={{true}}
@value={{this.commands.api}}
/>
</div>
{{/unless}}
</div>

View File

@ -9,23 +9,21 @@ import { kvMetadataPath, kvDataPath } from 'vault/utils/kv-path';
import { encodePath } from 'vault/utils/path-encoding-helpers';
/**
* @module KvSecretPaths is used to display copyable secret paths for KV v2 for CLI and API use.
* This view is permission agnostic because args come from the views mount path and url params.
* @module KvPathsCard is used to display copyable secret paths for KV v2 for CLI and API use.
* This component is permission agnostic because args come from the views mount path and url params.
*
* <Page::Secret::Paths
* <KvPathsCard
* @path={{this.model.path}}
* @backend={{this.model.backend}}
* @breadcrumbs={{this.breadcrumbs}}
* @canReadMetadata={{this.model.secret.canReadMetadata}}
* @isCondensed={{false}}
* />
*
* @param {string} path - kv secret path for building the CLI and API paths
* @param {string} backend - the secret engine mount path, comes from the secretMountPath service defined in the route
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component
* @param {boolean} [canReadMetadata=true] - if true, displays tab for Version History
* @param {boolean} isCondensed - if true a smaller version displays with no commands section or extra explanatory text
*/
export default class KvSecretPaths extends Component {
export default class KvPathsCard extends Component {
@service namespace;
get paths() {
@ -46,11 +44,15 @@ export default class KvSecretPaths extends Component {
snippet: namespace ? `-namespace=${namespace} ${cli}` : cli,
text: 'Use this path when referring to this secret in the CLI.',
},
{
label: 'API path for metadata',
snippet: namespace ? `/v1/${encodePath(namespace)}/${metadata}` : `/v1/${metadata}`,
text: `Use this path when referring to this secret's metadata in the API and permanent secret deletion.`,
},
...(this.args.isCondensed
? []
: [
{
label: 'API path for metadata',
snippet: namespace ? `/v1/${encodePath(namespace)}/${metadata}` : `/v1/${metadata}`,
text: `Use this path when referring to this secret's metadata in the API and permanent secret deletion.`,
},
]),
];
}

View File

@ -3,9 +3,9 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<OverviewCard @cardTitle="Subkeys">
<OverviewCard @cardTitle="Subkeys" class="has-top-margin-l">
<:customSubtext>
<Hds::Text::Body @color="faint" data-test-overview-card-subtitle="Subkeys">
<Hds::Text::Body @tag="p" @color="faint" data-test-overview-card-subtitle="Subkeys">
{{#if this.showJson}}
These are the subkeys within this secret. All underlying values of leaf keys are not retrieved and are replaced with
<code>null</code>

View File

@ -0,0 +1,105 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<KvPageHeader @breadcrumbs={{@breadcrumbs}} @pageTitle={{@path}}>
<:tabLinks>
<li>
<LinkTo @route="secret.index" @models={{array @backend @path}} data-test-secrets-tab="Overview">Overview</LinkTo>
</li>
<li>
<LinkTo @route="secret.details" @models={{array @backend @path}} data-test-secrets-tab="Secret">Secret</LinkTo>
</li>
<li>
<LinkTo
@route="secret.metadata.index"
@models={{array @backend @path}}
data-test-secrets-tab="Metadata"
>Metadata</LinkTo>
</li>
<li>
<LinkTo @route="secret.paths" @models={{array @backend @path}} data-test-secrets-tab="Paths">Paths</LinkTo>
</li>
{{#if @canReadMetadata}}
<li>
<LinkTo
@route="secret.metadata.versions"
@models={{array @backend @path}}
data-test-secrets-tab="Version History"
>Version History</LinkTo>
</li>
{{/if}}
</:tabLinks>
</KvPageHeader>
{{#if (or @metadata @subkeys)}}
<div class="flex row-wrap gap-24 has-top-margin-l">
<OverviewCard @cardTitle="Current version" @subText={{this.versionSubtext}} class="is-flex-1">
<:customTitle>
<Hds::Text::Display @weight="semibold" @size="300">
Current version
{{#unless this.isActive}}
<Hds::Badge
@text={{capitalize @secretState}}
@type={{if (eq @secretState "destroyed") "outlined" "inverted"}}
@color={{if (eq @secretState "destroyed") "critical"}}
@icon="x"
/>
{{/unless}}
</Hds::Text::Display>
</:customTitle>
<:action>
{{#if @canUpdateSecret}}
<Hds::Link::Standalone
@text="Create new"
@route="secret.details.edit"
@models={{array @backend @path}}
@icon="plus"
@iconPosition="trailing"
/>
{{/if}}
</:action>
<:content>
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
{{or @metadata.currentVersion @subkeys.metadata.version}}
</Hds::Text::Display>
</:content>
</OverviewCard>
{{#if this.isActive}}
{{#let (or @metadata.createdTime @subkeys.metadata.created_time) as |timestamp|}}
<OverviewCard
@cardTitle="Secret age"
@subText="Time since last update at {{date-format timestamp}}."
class="is-flex-1"
>
<:action>
{{#if @canReadMetadata}}
<Hds::Link::Standalone
@text="View metadata"
@route="secret.metadata"
@models={{array @backend @path}}
@icon="arrow-right"
@iconPosition="trailing"
/>
{{/if}}
</:action>
<:content>
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
{{date-from-now timestamp}}
</Hds::Text::Display>
</:content>
</OverviewCard>
{{/let}}
{{/if}}
</div>
{{/if}}
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l has-top-margin-l">
<KvPathsCard @backend={{@backend}} @path={{@path}} @isCondensed={{true}} />
</Hds::Card::Container>
{{#if @subkeys.subkeys}}
<KvSubkeys @subkeys={{@subkeys.subkeys}} />
{{/if}}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { dateFormat } from 'core/helpers/date-format';
/**
* @module KvSecretOverview
* <Page::Secret::Overview
* @backend={{this.model.backend}}
* @breadcrumbs={{this.breadcrumbs}}
* @canReadMetadata={{true}}
* @canUpdateSecret={{true}}
* @metadata={{this.model.metadata}}
* @path={{this.model.path}}
* @secretState="created"
* @subkeys={{this.model.subkeys}}
* />
*
* @param {string} backend - kv secret mount to make network request
* @param {array} breadcrumbs - Array to generate breadcrumbs, passed to the page header component
* @param {boolean} canReadMetadata - permissions to read metadata
* @param {boolean} canUpdateSecret - permissions to create a new version of a secret
* @param {model} metadata - Ember data model: 'kv/metadata'
* @param {string} path - path to request secret data for selected version
* @param {string} secretState - if a secret has been "destroyed", "deleted" or "created" (still active)
* @param {object} subkeys - API response from subkeys endpoint, object with "subkeys" and "metadata" keys
*/
export default class KvSecretOverview extends Component {
get isActive() {
const state = this.args.secretState;
return state !== 'destroyed' && state !== 'deleted';
}
get versionSubtext() {
const state = this.args.secretState;
if (state === 'destroyed') {
return 'The current version of this secret has been permanently deleted and cannot be restored.';
}
if (state === 'deleted') {
const time =
this.args.metadata?.currentSecret.deletionTime || this.args.subkeys?.metadata.deletion_time;
const date = dateFormat([time], {});
return `The current version of this secret was deleted ${date}.`;
}
return 'The current version of this secret.';
}
}

View File

@ -36,66 +36,4 @@
</:tabLinks>
</KvPageHeader>
<h2 class="title is-5 has-top-margin-xl">
Paths
</h2>
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
{{#each this.paths as |path|}}
<InfoTableRow @label={{path.label}} @labelWidth="is-one-third" @helperText={{path.text}} @truncateValue={{true}}>
<Hds::Copy::Button
@text="Copy"
@isIconOnly={{true}}
@textToCopy={{path.snippet}}
@onError={{(fn (set-flash-message "Clipboard copy failed. The Clipboard API requires a secure context." "danger"))}}
data-test-copy-button={{path.snippet}}
class="transparent"
/>
<code class="is-flex-1 text-overflow-ellipsis has-left-margin-s">
{{path.snippet}}
</code>
</InfoTableRow>
{{/each}}
</div>
<h2 class="title is-5 has-top-margin-xl">
Commands
</h2>
<div class="box is-fullwidth is-sideless">
<h3 class="is-label">
CLI
<Hds::Badge @text="kv get" @color="neutral" />
</h3>
<p class="helper-text has-text-grey-light has-bottom-padding-s">
This command retrieves the value from KV secrets engine at the given key name. For other CLI commands,
<DocLink @path="/vault/docs/commands/kv">
learn more.
</DocLink>
</p>
<Hds::CodeBlock
data-test-commands="cli"
@language="bash"
@hasLineNumbers={{false}}
@hasCopyButton={{true}}
@value={{this.commands.cli}}
/>
<h3 class="has-top-margin-l is-label">
API read secret version
</h3>
<p class="helper-text has-text-grey-light has-bottom-padding-s">
This command obtains data and metadata for the latest version of this secret. In this example, Vault is located at
https://127.0.0.1:8200. For other API commands,
<DocLink @path="/vault/api-docs/secret/kv/kv-v2">
learn more.
</DocLink>
</p>
<Hds::CodeBlock
data-test-commands="api"
@language="bash"
@hasLineNumbers={{false}}
@hasCopyButton={{true}}
@value={{this.commands.api}}
/>
</div>
<KvPathsCard @backend={{@backend}} @path={{@path}} class="has-top-margin-xl" />

View File

@ -18,7 +18,7 @@
/>
</:action>
<:content>
<Hds::Text::Display @tag="h2" @size="500">
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
{{format-number (if (eq @issuers 404) 0 @issuers.length)}}
</Hds::Text::Display>
</:content>
@ -38,7 +38,7 @@
/>
</:action>
<:content>
<Hds::Text::Display @tag="h2" @size="500">
<Hds::Text::Display @tag="p" @size="400" @weight="medium" class="has-top-margin-m">
{{format-number (if (eq @roles 404) 0 @roles.length)}}
</Hds::Text::Display>
</:content>

View File

@ -14,6 +14,8 @@ import { click, currentURL, currentRouteName, visit } from '@ember/test-helpers'
import { runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
import { clearRecords } from 'vault/tests/helpers/pki/pki-helpers';
import { PKI_OVERVIEW } from 'vault/tests/helpers/pki/pki-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
const { overviewCard } = GENERAL;
module('Acceptance | pki overview', function (hooks) {
setupApplicationTest(hooks);
@ -59,9 +61,9 @@ module('Acceptance | pki overview', function (hooks) {
test('navigates to view issuers when link is clicked on issuer card', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(PKI_OVERVIEW.issuersCardTitle).hasText('Issuers');
assert.dom(PKI_OVERVIEW.issuersCardOverviewNum).hasText('1');
await click(PKI_OVERVIEW.issuersCardLink);
assert.dom(overviewCard.title('Issuers')).hasText('Issuers');
assert.dom(`${overviewCard.container('Issuers')} p`).hasText('1');
await click(overviewCard.actionLink('Issuers'));
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
});
@ -69,9 +71,9 @@ module('Acceptance | pki overview', function (hooks) {
test('navigates to view roles when link is clicked on roles card', async function (assert) {
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(PKI_OVERVIEW.rolesCardTitle).hasText('Roles');
assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('0');
await click(PKI_OVERVIEW.rolesCardLink);
assert.dom(overviewCard.title('Roles')).hasText('Roles');
assert.dom(`${overviewCard.container('Roles')} p`).hasText('0');
await click(overviewCard.actionLink('Roles'));
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/roles`);
await runCmd([
`write ${this.mountPath}/roles/some-role \
@ -81,14 +83,14 @@ module('Acceptance | pki overview', function (hooks) {
max_ttl="720h"`,
]);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('1');
assert.dom(`${overviewCard.container('Roles')} p`).hasText('1');
});
test('hides roles card if user does not have permissions', async function (assert) {
await authPage.login(this.pkiIssuersList);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
assert.dom(PKI_OVERVIEW.rolesCardTitle).doesNotExist('Roles card does not exist');
assert.dom(PKI_OVERVIEW.issuersCardTitle).exists('Issuers card exists');
assert.dom(overviewCard.title('Roles')).doesNotExist('Roles card does not exist');
assert.dom(overviewCard.title('Issuers')).hasText('Issuers');
});
test('navigates to generate certificate page for Issue Certificates card', async function (assert) {

View File

@ -64,7 +64,7 @@ module('Acceptance | sync | overview', function (hooks) {
await click(ts.navLink('Secrets Sync'));
await click(ts.destinations.list.create);
await click(ts.createCancel);
await click(ts.overviewCard.actionLink('Create new'));
await click(ts.overviewCard.actionText('Create new'));
await click(ts.createCancel);
await waitFor(ts.overview.table.actionToggle(0));
await click(ts.overview.table.actionToggle(0));

View File

@ -74,11 +74,12 @@ export const GENERAL = {
removeSelected: '[data-test-selected-list-button="delete"]',
},
overviewCard: {
container: (title: string) => `[data-test-overview-card-container="${title}"]`,
title: (title: string) => `[data-test-overview-card-title="${title}"]`,
description: (title: string) => `[data-test-overview-card-subtitle="${title}"]`,
content: (title: string) => `[data-test-overview-card-content="${title}"]`,
action: (title: string) => `[data-test-overview-card-container="${title}"] [data-test-action-text]`,
actionLink: (label: string) => `[data-test-action-text="${label}"]`,
actionText: (text: string) => `[data-test-action-text="${text}"]`,
actionLink: (label: string) => `[data-test-overview-card="${label}"] a`,
},
pagination: {
next: '.hds-pagination-nav__arrow--direction-next',

View File

@ -7,6 +7,9 @@
import { click, fillIn, visit, settled } from '@ember/test-helpers';
import { FORM } from './kv-selectors';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
import { assert } from '@ember/debug';
import { kvMetadataPath } from 'vault/utils/kv-path';
// CUSTOM ACTIONS RELEVANT TO KV-V2
@ -66,3 +69,30 @@ export function clearRecords(store) {
store.unloadAll('kv/metatata');
store.unloadAll('capabilities');
}
// TEST SETUP HELPERS
// sets basic path, backend, and metadata
export const baseSetup = (context) => {
assert(
`'baseSetup()' requires mirage: import { setupMirage } from 'ember-cli-mirage/test-support'`,
context.server
);
context.store = context.owner.lookup('service:store');
context.server.post('/sys/capabilities-self', allowAllCapabilitiesStub());
context.backend = 'kv-engine';
context.path = 'my-secret';
context.metadata = metadataModel(context, { withCustom: false });
};
export const metadataModel = (context, { withCustom = false }) => {
const metadata = withCustom
? context.server.create('kv-metadatum', 'withCustomMetadata')
: context.server.create('kv-metadatum');
metadata.id = kvMetadataPath(context.backend, context.path);
context.store.pushPayload('kv/metadata', {
modelName: 'kv/metadata',
...metadata,
});
return context.store.peekRecord('kv/metadata', metadata.id);
};

View File

@ -4,23 +4,12 @@
*/
export const PKI_OVERVIEW = {
issuersCardTitle: '[data-test-overview-card-title="Issuers"]',
issuersCardSubtitle: '[data-test-overview-card-subtitle="Issuers"]',
issuersCardLink: '[data-test-overview-card-container="Issuers"] a',
issuersCardOverviewNum: '[data-test-overview-card-container="Issuers"] h2',
rolesCardTitle: '[data-test-overview-card-title="Roles"]',
rolesCardSubtitle: '[data-test-overview-card-subtitle="Roles"]',
rolesCardLink: '[data-test-overview-card-container="Roles"] a',
rolesCardOverviewNum: '[data-test-overview-card-container="Roles"] h2',
issueCertificate: '[data-test-overview-card-title="Issue certificate"]',
issueCertificateInput: '[data-test-issue-certificate-input]',
issueCertificatePowerSearch: '[data-test-issue-certificate-input] span',
issueCertificateButton: '[data-test-issue-certificate-button]',
viewCertificate: '[data-test-overview-card-title="View certificate"]',
viewCertificateInput: '[data-test-view-certificate-input]',
viewCertificatePowerSearch: '[data-test-view-certificate-input] span',
viewCertificateButton: '[data-test-view-certificate-button]',
viewIssuerInput: '[data-test-issue-issuer-input]',
viewIssuerPowerSearch: '[data-test-issue-issuer-input] span',
viewIssuerButton: '[data-test-view-issuer-button]',
firstPowerSelectOption: '[data-option-index="0"]',

View File

@ -0,0 +1,137 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { setupEngine } from 'ember-engines/test-support';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
/* eslint-disable no-useless-escape */
module('Integration | Component | kv-v2 | KvPathsCard', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'kv');
hooks.beforeEach(async function () {
this.backend = 'kv-engine';
this.path = 'my-secret';
this.isCondensed = false;
this.assertClipboard = (assert, element, expected) => {
assert.dom(element).hasAttribute('data-test-copy-button', expected);
};
this.renderComponent = async () => {
return render(
hbs`<KvPathsCard @backend={{this.backend}} @path={{this.path}} @isCondensed={{this.isCondensed}} />`,
{
owner: this.engine,
}
);
};
});
test('it renders condensed version', async function (assert) {
this.isCondensed = true;
await this.renderComponent();
assert.dom('[data-test-component="info-table-row"] .helper-text').doesNotExist('subtext does not render');
assert.dom('[data-test-label-div]').hasClass('is-one-quarter');
assert.dom(PAGE.infoRowValue('API path for metadata')).doesNotExist();
assert.dom(PAGE.paths.codeSnippet('cli')).doesNotExist();
assert.dom(PAGE.paths.codeSnippet('api')).doesNotExist();
const paths = [
{ label: 'API path', expected: `/v1/${this.backend}/data/${this.path}` },
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
];
for (const path of paths) {
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
}
});
test('it renders uncondensed version', async function (assert) {
await this.renderComponent();
assert.dom('[data-test-component="info-table-row"] .helper-text').exists('subtext renders');
assert.dom('[data-test-label-div]').hasClass('is-one-third');
assert.dom(PAGE.infoRowValue('API path for metadata')).exists();
assert.dom(PAGE.paths.codeSnippet('cli')).exists();
assert.dom(PAGE.paths.codeSnippet('api')).exists();
});
test('it renders copyable paths', async function (assert) {
const paths = [
{ label: 'API path', expected: `/v1/${this.backend}/data/${this.path}` },
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
{ label: 'API path for metadata', expected: `/v1/${this.backend}/metadata/${this.path}` },
];
await this.renderComponent();
for (const path of paths) {
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
}
});
test('it renders copyable encoded mount and secret paths', async function (assert) {
this.path = `my spacey!"secret`;
this.backend = `my fancy!"backend`;
const backend = encodeURIComponent(this.backend);
const path = encodeURIComponent(this.path);
const paths = [
{
label: 'API path',
expected: `/v1/${backend}/data/${path}`,
},
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
{
label: 'API path for metadata',
expected: `/v1/${backend}/metadata/${path}`,
},
];
await this.renderComponent();
for (const path of paths) {
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
}
});
test('it renders copyable commands', async function (assert) {
const url = `https://127.0.0.1:8200/v1/${this.backend}/data/${this.path}`;
const expected = {
cli: `vault kv get -mount="${this.backend}" "${this.path}"`,
api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`,
};
await this.renderComponent();
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api);
});
test('it renders copyable encoded mount and path commands', async function (assert) {
this.path = `my spacey!"secret`;
this.backend = `my fancy!"backend`;
const backend = encodeURIComponent(this.backend);
const path = encodeURIComponent(this.path);
const url = `https://127.0.0.1:8200/v1/${backend}/data/${path}`;
const expected = {
cli: `vault kv get -mount="${this.backend}" "${this.path}"`,
api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`,
};
await this.renderComponent();
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api);
});
});

View File

@ -0,0 +1,341 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
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 } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { dateFormat } from 'core/helpers/date-format';
import { dateFromNow } from 'core/helpers/date-from-now';
import { baseSetup } from 'vault/tests/helpers/kv/kv-run-commands';
const { overviewCard } = GENERAL;
module('Integration | Component | kv-v2 | Page::Secret::Overview', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'kv');
setupMirage(hooks);
hooks.beforeEach(async function () {
baseSetup(this);
this.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: this.backend, route: 'list' },
{ label: this.path },
];
this.subkeys = {
subkeys: {
foo: null,
bar: {
baz: null,
},
quux: null,
},
metadata: {
created_time: '2021-12-14T20:28:00.773477Z',
custom_metadata: null,
deletion_time: '',
destroyed: false,
version: 1,
},
};
this.canReadMetadata = true;
this.canUpdateSecret = true;
this.secretState = 'created';
this.format = (time) => dateFormat([time, 'MMM d yyyy, h:mm:ss aa'], {});
this.renderComponent = async () => {
return render(
hbs`
<Page::Secret::Overview
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
@canReadMetadata={{this.canReadMetadata}}
@canUpdateSecret={{this.canUpdateSecret}}
@metadata={{this.metadata}}
@path={{this.path}}
@secretState={{this.secretState}}
@subkeys={{this.subkeys}}
/>`,
{ owner: this.engine }
);
};
});
module('it renders when version is not deleted nor destroyed', function () {
test('it renders tabs', async function (assert) {
await this.renderComponent();
const tabs = ['Overview', 'Secret', 'Metadata', 'Paths', 'Version History'];
for (const tab of tabs) {
assert.dom(PAGE.secretTab(tab)).hasText(tab);
}
});
test('it renders header', async function (assert) {
await this.renderComponent();
assert.dom(PAGE.breadcrumbs).hasText(`Secrets ${this.backend} ${this.path}`);
assert.dom(PAGE.title).hasText(this.path);
});
test('it renders with full permissions', async function (assert) {
await this.renderComponent();
const fromNow = dateFromNow([this.metadata.createdTime]); // uses date-fns so can't stub timestamp util
assert.dom(`${overviewCard.container('Current version')} .hds-badge`).doesNotExist();
assert
.dom(overviewCard.container('Current version'))
.hasText(
`Current version Create new The current version of this secret. ${this.metadata.currentVersion}`
);
assert
.dom(overviewCard.container('Secret age'))
.hasText(
`Secret age View metadata Time since last update at ${this.format(
this.metadata.createdTime
)}. ${fromNow}`
);
assert
.dom(overviewCard.container('Paths'))
.hasText(
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
);
assert
.dom(overviewCard.container('Subkeys'))
.hasText(
`Subkeys JSON The table is displaying the top level subkeys. Toggle on the JSON view to see the full depth. Keys ${Object.keys(
this.subkeys.subkeys
).join(' ')}`
);
});
test('it hides link when no secret update permissions', async function (assert) {
// creating a new version of a secret is updating a secret
// the overview only exists after an initial version is created
// which is why we just check for update and not also create
this.canUpdateSecret = false;
await this.renderComponent();
assert
.dom(`${overviewCard.container('Current version')} a`)
.doesNotExist('create link does not render');
assert
.dom(overviewCard.container('Current version'))
.hasText(`Current version The current version of this secret. ${this.metadata.currentVersion}`);
});
test('it renders with no metadata permissions', async function (assert) {
this.metadata = null;
this.canReadMetadata = false;
// all secret metadata instead comes from subkeys endpoint
const subkeyMeta = this.subkeys.metadata;
await this.renderComponent();
const fromNow = dateFromNow([subkeyMeta.created_time]); // uses date-fns so can't stub timestamp util
assert
.dom(overviewCard.container('Current version'))
.hasText(`Current version Create new The current version of this secret. ${subkeyMeta.version}`);
assert
.dom(overviewCard.container('Secret age'))
.hasText(`Secret age Time since last update at ${this.format(subkeyMeta.created_time)}. ${fromNow}`);
assert.dom(`${overviewCard.container('Secret age')} a`).doesNotExist('metadata link does not render');
assert
.dom(overviewCard.container('Paths'))
.hasText(
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
);
assert
.dom(overviewCard.container('Subkeys'))
.hasText(
`Subkeys JSON The table is displaying the top level subkeys. Toggle on the JSON view to see the full depth. Keys ${Object.keys(
this.subkeys.subkeys
).join(' ')}`
);
});
test('it renders with no subkeys permissions', async function (assert) {
this.subkeys = null;
await this.renderComponent();
const fromNow = dateFromNow([this.metadata.createdTime]); // uses date-fns so can't stub timestamp util
const expectedTime = this.format(this.metadata.createdTime);
assert
.dom(overviewCard.container('Current version'))
.hasText(
`Current version Create new The current version of this secret. ${this.metadata.currentVersion}`
);
assert
.dom(overviewCard.container('Secret age'))
.hasText(`Secret age View metadata Time since last update at ${expectedTime}. ${fromNow}`);
assert
.dom(overviewCard.container('Paths'))
.hasText(
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
);
assert.dom(overviewCard.container('Subkeys')).doesNotExist();
});
test('it renders with no subkey or metadata permissions', async function (assert) {
this.subkeys = null;
this.metadata = null;
await this.renderComponent();
assert.dom(overviewCard.container('Current version')).doesNotExist();
assert.dom(overviewCard.container('Secret age')).doesNotExist();
assert.dom(overviewCard.container('Subkeys')).doesNotExist();
assert
.dom(overviewCard.container('Paths'))
.hasText(
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
);
});
});
module('it renders when version is deleted', function (hooks) {
hooks.beforeEach(async function () {
this.secretState = 'deleted';
// subkeys is null but metadata still has data
this.subkeys = {
subkeys: null,
metadata: {
created_time: '2021-12-14T20:28:00.773477Z',
custom_metadata: null,
deletion_time: '2022-02-14T20:28:00.773477Z',
destroyed: false,
version: 1,
},
};
this.metadata.versions[4].deletion_time = '2024-08-15T23:01:08.312332Z';
this.assertBadge = (assert) => {
assert
.dom(`${overviewCard.container('Current version')} .hds-badge`)
.hasClass('hds-badge--color-neutral');
assert
.dom(`${overviewCard.container('Current version')} .hds-badge`)
.hasClass('hds-badge--type-inverted');
assert.dom(`${overviewCard.container('Current version')} .hds-badge`).hasText('Deleted');
};
});
test('with full permissions', async function (assert) {
const expectedTime = this.format(this.metadata.versions[4].deletion_time);
await this.renderComponent();
this.assertBadge(assert);
assert
.dom(overviewCard.container('Current version'))
.hasText(
`Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.metadata.currentVersion}`
);
assert.dom(overviewCard.container('Secret age')).doesNotExist();
assert.dom(overviewCard.container('Subkeys')).doesNotExist();
assert
.dom(overviewCard.container('Paths'))
.hasText(
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
);
});
test('with no metadata permissions', async function (assert) {
this.metadata = null;
const expectedTime = this.format(this.subkeys.metadata.deletion_time);
await this.renderComponent();
this.assertBadge(assert);
assert
.dom(overviewCard.container('Current version'))
.hasText(
`Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.subkeys.metadata.version}`
);
});
test('with no subkey permissions', async function (assert) {
this.subkeys = null;
const expectedTime = this.format(this.metadata.versions[4].deletion_time);
await this.renderComponent();
this.assertBadge(assert);
assert
.dom(overviewCard.container('Current version'))
.hasText(
`Current version Deleted Create new The current version of this secret was deleted ${expectedTime}. ${this.metadata.currentVersion}`
);
});
test('with no permissions', async function (assert) {
this.subkeys = null;
this.metadata = null;
await this.renderComponent();
assert.dom(overviewCard.container('Current version')).doesNotExist();
});
});
module('it renders when version is destroyed', function (hooks) {
hooks.beforeEach(async function () {
this.secretState = 'destroyed';
// subkeys is null but metadata still has data
this.subkeys = {
subkeys: null,
metadata: {
created_time: '2024-08-15T01:24:43.658478Z',
custom_metadata: null,
deletion_time: '',
destroyed: true,
version: 1,
},
};
this.metadata.versions[4].destroyed = true;
this.assertBadge = (assert) => {
assert
.dom(`${overviewCard.container('Current version')} .hds-badge`)
.hasClass('hds-badge--color-critical');
assert
.dom(`${overviewCard.container('Current version')} .hds-badge`)
.hasClass('hds-badge--type-outlined');
assert.dom(`${overviewCard.container('Current version')} .hds-badge`).hasText('Destroyed');
};
});
test('with full permissions', async function (assert) {
await this.renderComponent();
this.assertBadge(assert);
assert
.dom(overviewCard.container('Current version'))
.hasText(
`Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.metadata.currentVersion}`
);
assert.dom(overviewCard.container('Secret age')).doesNotExist();
assert.dom(overviewCard.container('Subkeys')).doesNotExist();
assert
.dom(overviewCard.container('Paths'))
.hasText(
`Paths The paths to use when referring to this secret in API or CLI. API path /v1/${this.backend}/data/${this.path} CLI path -mount="${this.backend}" "${this.path}"`
);
});
test('with no metadata permissions', async function (assert) {
this.metadata = null;
await this.renderComponent();
this.assertBadge(assert);
assert
.dom(overviewCard.container('Current version'))
.hasText(
`Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.subkeys.metadata.version}`
);
});
test('with no subkeys permissions', async function (assert) {
this.subkeys = null;
await this.renderComponent();
this.assertBadge(assert);
assert
.dom(overviewCard.container('Current version'))
.hasText(
`Current version Destroyed Create new The current version of this secret has been permanently deleted and cannot be restored. ${this.metadata.currentVersion}`
);
});
test('with no permissions', async function (assert) {
this.subkeys = null;
this.metadata = null;
await this.renderComponent();
assert.dom(overviewCard.container('Current version')).doesNotExist();
});
});
});

View File

@ -9,8 +9,6 @@ import { setupEngine } from 'ember-engines/test-support';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { PAGE } from 'vault/tests/helpers/kv/kv-selectors';
/* eslint-disable no-useless-escape */
module('Integration | Component | kv-v2 | Page::Secret::Paths', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'kv');
@ -18,123 +16,55 @@ module('Integration | Component | kv-v2 | Page::Secret::Paths', function (hooks)
hooks.beforeEach(async function () {
this.backend = 'kv-engine';
this.path = 'my-secret';
this.canReadMetadata = true;
this.breadcrumbs = [
{ label: 'Secrets', route: 'secrets', linkExternal: true },
{ label: this.backend, route: 'list' },
{ label: this.path },
];
this.assertClipboard = (assert, element, expected) => {
assert.dom(element).hasAttribute('data-test-copy-button', expected);
this.renderComponent = async () => {
await render(
hbs`
<Page::Secret::Paths
@path={{this.path}}
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
@canReadMetadata={{this.canReadMetadata}}
/>
`,
{ owner: this.engine }
);
};
});
test('it renders copyable paths', async function (assert) {
assert.expect(6);
const paths = [
{ label: 'API path', expected: `/v1/${this.backend}/data/${this.path}` },
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
{ label: 'API path for metadata', expected: `/v1/${this.backend}/metadata/${this.path}` },
];
await render(
hbs`
<Page::Secret::Paths
@path={{this.path}}
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
/>
`,
{ owner: this.engine }
);
for (const path of paths) {
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
test('it renders tabs', async function (assert) {
await this.renderComponent();
const tabs = ['Secret', 'Metadata', 'Paths', 'Version History'];
for (const tab of tabs) {
assert.dom(PAGE.secretTab(tab)).hasText(tab);
}
});
test('it renders copyable encoded mount and secret paths', async function (assert) {
assert.expect(6);
this.path = `my spacey!"secret`;
this.backend = `my fancy!"backend`;
const backend = encodeURIComponent(this.backend);
const path = encodeURIComponent(this.path);
const paths = [
{
label: 'API path',
expected: `/v1/${backend}/data/${path}`,
},
{ label: 'CLI path', expected: `-mount="${this.backend}" "${this.path}"` },
{
label: 'API path for metadata',
expected: `/v1/${backend}/metadata/${path}`,
},
];
await render(
hbs`
<Page::Secret::Paths
@path={{this.path}}
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
/>
`,
{ owner: this.engine }
);
for (const path of paths) {
assert.dom(PAGE.infoRowValue(path.label)).hasText(path.expected);
this.assertClipboard(assert, PAGE.paths.copyButton(path.label), path.expected);
test('it hides version history when cannot READ metadata', async function (assert) {
this.canReadMetadata = false;
await this.renderComponent();
const tabs = ['Secret', 'Metadata', 'Paths'];
for (const tab of tabs) {
assert.dom(PAGE.secretTab(tab)).hasText(tab);
}
assert.dom(PAGE.secretTab('Version History')).doesNotExist();
});
test('it renders copyable commands', async function (assert) {
const url = `https://127.0.0.1:8200/v1/${this.backend}/data/${this.path}`;
const expected = {
cli: `vault kv get -mount="${this.backend}" "${this.path}"`,
api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`,
};
await render(
hbs`
<Page::Secret::Paths
@path={{this.path}}
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
/>
`,
{ owner: this.engine }
);
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api);
test('it renders header', async function (assert) {
await this.renderComponent();
assert.dom(PAGE.breadcrumbs).hasText(`Secrets ${this.backend} ${this.path}`);
assert.dom(PAGE.title).hasText(this.path);
});
test('it renders copyable encoded mount and path commands', async function (assert) {
this.path = `my spacey!"secret`;
this.backend = `my fancy!"backend`;
const backend = encodeURIComponent(this.backend);
const path = encodeURIComponent(this.path);
const url = `https://127.0.0.1:8200/v1/${backend}/data/${path}`;
const expected = {
cli: `vault kv get -mount="${this.backend}" "${this.path}"`,
api: `curl \\ --header \"X-Vault-Token: ...\" \\ --request GET \\ ${url}`,
};
await render(
hbs`
<Page::Secret::Paths
@path={{this.path}}
@backend={{this.backend}}
@breadcrumbs={{this.breadcrumbs}}
/>
`,
{ owner: this.engine }
);
assert.dom(PAGE.paths.codeSnippet('cli')).hasText(expected.cli);
assert.dom(PAGE.paths.codeSnippet('api')).hasText(expected.api);
test('it renders commands which is the uncondensed version of KvPathsCard', async function (assert) {
await this.renderComponent();
assert.dom(PAGE.paths.codeSnippet('cli')).exists();
assert.dom(PAGE.paths.codeSnippet('api')).exists();
});
});

View File

@ -13,6 +13,7 @@ const ACTION_TEXT = 'View card';
const SUBTEXT = 'This is subtext for card';
const SELECTORS = {
container: '[data-test-overview-card-container]',
title: '[data-test-overview-card-title]',
subtitle: '[data-test-overview-card-subtitle]',
action: '[data-test-action-text]',
@ -28,10 +29,21 @@ module('Integration | Component | overview-card', function (hooks) {
this.set('subText', SUBTEXT);
});
test('it returns card title, ', async function (assert) {
test('it returns card title', async function (assert) {
await render(hbs`<OverviewCard @cardTitle={{this.cardTitle}}/>`);
assert.dom(SELECTORS.title).hasText('Card title');
});
test('it returns custom title if both exist', async function (assert) {
await render(hbs`
<OverviewCard @cardTitle={{this.cardTitle}}>
<:customTitle>
Fancy custom title
</:customTitle>
</OverviewCard>
`);
assert.dom(SELECTORS.container).hasText('Fancy custom title');
assert.dom(SELECTORS.container).doesNotIncludeText(this.cardTitle);
});
test('it renders card @subText arg, ', async function (assert) {
await render(hbs`<OverviewCard @cardTitle={{this.cardTitle}} @subText={{this.subText}} />`);
assert.dom(SELECTORS.subtitle).hasText('This is subtext for card');

View File

@ -10,7 +10,9 @@ import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { PKI_OVERVIEW } from 'vault/tests/helpers/pki/pki-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
const { overviewCard } = GENERAL;
module('Integration | Component | Page::PkiOverview', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
@ -39,9 +41,11 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
{ owner: this.engine }
);
assert.dom(PKI_OVERVIEW.issuersCardTitle).hasText('Issuers');
assert.dom(PKI_OVERVIEW.issuersCardOverviewNum).hasText('2');
assert.dom(PKI_OVERVIEW.issuersCardLink).hasText('View issuers');
assert
.dom(overviewCard.container('Issuers'))
.hasText(
'Issuers View issuers The total number of issuers in this PKI mount. Includes both root and intermediate certificates. 2'
);
});
test('shows the correct information on roles card', async function (assert) {
@ -49,15 +53,21 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
{ owner: this.engine }
);
assert.dom(PKI_OVERVIEW.rolesCardTitle).hasText('Roles');
assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('3');
assert.dom(PKI_OVERVIEW.rolesCardLink).hasText('View roles');
assert
.dom(overviewCard.container('Roles'))
.hasText(
'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 3'
);
this.roles = 404;
await render(
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
{ owner: this.engine }
);
assert.dom(PKI_OVERVIEW.rolesCardOverviewNum).hasText('0');
assert
.dom(overviewCard.container('Roles'))
.hasText(
'Roles View roles The total number of roles in this PKI mount that have been created to generate certificates. 0'
);
});
test('shows the input search fields for View Certificates card', async function (assert) {
@ -65,7 +75,7 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
{ owner: this.engine }
);
assert.dom(PKI_OVERVIEW.issueCertificate).hasText('Issue certificate');
assert.dom(overviewCard.title('Issue certificate')).hasText('Issue certificate');
assert.dom(PKI_OVERVIEW.issueCertificateInput).exists();
assert.dom(PKI_OVERVIEW.issueCertificateButton).hasText('Issue');
});
@ -75,7 +85,7 @@ module('Integration | Component | Page::PkiOverview', function (hooks) {
hbs`<Page::PkiOverview @issuers={{this.issuers}} @roles={{this.roles}} @engine={{this.engineId}} />,`,
{ owner: this.engine }
);
assert.dom(PKI_OVERVIEW.viewCertificate).hasText('View certificate');
assert.dom(overviewCard.title('View certificate')).hasText('View certificate');
assert.dom(PKI_OVERVIEW.viewCertificateInput).exists();
assert.dom(PKI_OVERVIEW.viewCertificateButton).hasText('View');
});

View File

@ -358,7 +358,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
test('it should show the Totals cards', async function (assert) {
await this.renderComponent();
const { title, description, action, content } = overviewCard;
const { title, description, actionLink, content } = overviewCard;
const cardData = [
{
cardTitle: 'Total destinations',
@ -379,7 +379,7 @@ module('Integration | Component | sync | Page::Overview', function (hooks) {
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');
assert.dom(actionLink(cardTitle)).hasText(actionText, 'Card action renders');
});
});
});

View File

@ -8,6 +8,7 @@ import { setupRenderingTest } from 'ember-qunit';
import { find, render, settled } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { formatTimeZone } from 'core/helpers/date-format';
import { isMatch } from 'date-fns';
const TEST_DATE = new Date('2018-04-03T14:15:30');
@ -56,6 +57,14 @@ module('Integration | Helper | date-format', function (hooks) {
assert.strictEqual(resultLengthWithTimezone - 4, 4, 'Adds 4 characters for timezone');
});
test('it renders default format', async function (assert) {
this.set('timestampDate', TEST_DATE);
await render(hbs`<span data-test-formatted>{{date-format this.timestampDate}}</span>`);
const value = find('[data-test-formatted]').innerText;
const format = 'MMM d yyyy, h:mm:ss aa';
assert.true(isMatch(value, format), `${value} is formatted as ${format}`);
});
test('fails gracefully if given a non-date value', async function (assert) {
this.set('value', 'not a date');

View File

@ -6,7 +6,7 @@
import { module, test } from 'qunit';
import { subMinutes } from 'date-fns';
import { setupRenderingTest } from 'ember-qunit';
import { dateFromNow } from '../../../helpers/date-from-now';
import { dateFromNow } from 'core/helpers/date-from-now';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';