UI: PKI Roles Edit (#18194)

This commit is contained in:
Chelsea Shaw
2022-12-02 10:42:14 -06:00
committed by GitHub
parent bb99bfa3bd
commit 42e1ba2110
28 changed files with 498 additions and 253 deletions

View File

@@ -24,6 +24,7 @@ export default class PkiRoleModel extends Model {
@attr('string', {
label: 'Role name',
fieldValue: 'name',
editDisabled: true,
})
name;
@@ -50,7 +51,6 @@ export default class PkiRoleModel extends Model {
helperTextEnabled:
'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.',
editType: 'ttl',
hideToggle: true,
defaultValue: '30s', // The API type is "duration" which accepts both an integer and string e.g. 30 || '30s'
})
notBeforeDuration;

View File

@@ -1,6 +1,7 @@
@import 'ember-basic-dropdown';
@import 'ember-power-select';
@import './core';
@import './engines';
@mixin font-face($name) {
@font-face {

View File

@@ -287,3 +287,17 @@ ul.bullet {
.has-text-align-center {
text-align: center;
}
// Screen Readers only
.sr-only {
border: 0 !important;
clip: rect(1px, 1px, 1px, 1px) !important;
-webkit-clip-path: inset(50%) !important;
clip-path: inset(50%) !important;
height: 1px !important;
overflow: hidden !important;
margin: -1px !important;
padding: 0 !important;
position: absolute !important;
width: 1px !important;
white-space: nowrap !important;
}

View File

@@ -0,0 +1,2 @@
// PKI Engine styles
@import './pki/pki-not-valid-after-form';

View File

@@ -0,0 +1,3 @@
.pki-radiogroup-label {
align-items: baseline;
}

View File

@@ -61,6 +61,8 @@
{{/if}}
{{else if @formatDate}}
{{date-format @value @formatDate}}
{{else if @formatTtl}}
{{this.formattedTtl}}
{{else}}
{{#if (eq @type "array")}}
<InfoTableItemArray

View File

@@ -2,6 +2,7 @@ import { typeOf } from '@ember/utils';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { convertFromSeconds, largestUnitFromSeconds } from 'core/utils/duration-utils';
/**
* @module InfoTableRow
@@ -56,6 +57,14 @@ export default class InfoTableRowComponent extends Component {
return false;
}
}
get formattedTtl() {
const { value } = this.args;
if (Number.isInteger(value)) {
const unit = largestUnitFromSeconds(value);
return `${convertFromSeconds(value, unit)}${unit}`;
}
return value;
}
@action
calculateLabelOverflow(el) {

View File

@@ -1,49 +0,0 @@
<div class="column is-narrow is-flex-center has-text-grey has-right-margin-s has-top-margin-negative-s">
<RadioButton
class="radio"
name="ttl"
@value="ttl"
@onChange={{this.onRadioButtonChange}}
@groupValue={{this.groupValue}}
data-test-radio-button="ttl"
/>
<label class="has-left-margin-xs">
<TtlPicker
data-test-input="ttl"
@onChange={{this.setAndBroadcastTtl}}
@label="TTL"
@helperTextEnabled={{@attr.options.helperTextEnabled}}
@description={{@attr.helpText}}
@time={{this.ttlTime}}
@unit="d"
@hideToggle={{true}}
/>
</label>
</div>
<div class="column is-narrow is-flex-center has-text-grey has-right-margin-s">
<RadioButton
class="radio"
name="not_after"
@value="specificDate"
@onChange={{this.onRadioButtonChange}}
@groupValue={{this.groupValue}}
data-test-radio-button="not_after"
/>
<label class="has-left-margin-xs">
<span class="ttl-picker-label is-large">Specific date</span><br />
<p class="sub-text">
This value format should be given in UTC format YYYY-MM-ddTHH:MM:SSZ.
</p>
{{#if (eq this.groupValue "specificDate")}}
<input
id="not_after"
autocomplete="off"
spellcheck="false"
value={{this.notAfter}}
{{on "input" this.setAndBroadcastInput}}
class="input"
data-test-input="not_after"
/>
{{/if}}
</label>
</div>

View File

@@ -1,52 +0,0 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
/**
* @module RadioSelectTtlOrString
* `RadioSelectTtlOrString` components are yielded out within the formField component when the editType on the model is yield.
* The component is two radio buttons, where the first option is a TTL, and the second option is an input field without a title.
* This component is used in the PKI engine inside various forms.
*
* @example
* ```js
* {{#each @model.fields as |attr|}}
* <RadioSelectTtlOrString @attr={{attr}} @model={{this.model}} />
* {{/each}}
* ```
* @param {Model} model - Ember Data model that `attr` is defined on.
* @param {Object} attr - Usually derived from ember model `attributes` lookup, and all members of `attr.options` are optional.
*/
export default class RadioSelectTtlOrString extends Component {
@tracked groupValue = 'ttl';
@tracked ttlTime;
@tracked notAfter;
@action onRadioButtonChange(selection) {
this.groupValue = selection;
// Clear the previous selection if they have clicked the other radio button.
if (selection === 'specificDate') {
this.args.model.set('ttl', '');
this.ttlTime = '';
}
if (selection === 'ttl') {
this.args.model.set('notAfter', '');
this.notAfter = '';
this.args.model.set('ttl', this.ttlTime);
}
}
@action setAndBroadcastTtl(value) {
const valueToSet = value.enabled === true ? `${value.seconds}s` : 0;
if (this.groupValue === 'specificDate') {
// do not save ttl on the model until the ttl radio button is selected
return;
}
this.args.model.set('ttl', `${valueToSet}`);
}
@action setAndBroadcastInput(event) {
this.args.model.set('notAfter', event.target.value);
}
}

View File

@@ -28,37 +28,12 @@ import Duration from '@icholy/duration';
import { guidFor } from '@ember/object/internals';
import Ember from 'ember';
import { restartableTask, timeout } from 'ember-concurrency';
export const secondsMap = {
s: 1,
m: 60,
h: 3600,
d: 86400,
};
const convertToSeconds = (time, unit) => {
return time * secondsMap[unit];
};
const convertFromSeconds = (seconds, unit) => {
return seconds / secondsMap[unit];
};
const goSafeConvertFromSeconds = (seconds, unit) => {
// Go only accepts s, m, or h units
const u = unit === 'd' ? 'h' : unit;
return convertFromSeconds(seconds, u) + u;
};
const largestUnitFromSeconds = (seconds) => {
let unit = 's';
if (seconds === 0) return unit;
// get largest unit with no remainder
if (seconds % secondsMap.d === 0) {
unit = 'd';
} else if (seconds % secondsMap.h === 0) {
unit = 'h';
} else if (seconds % secondsMap.m === 0) {
unit = 'm';
}
return unit;
};
import {
convertFromSeconds,
convertToSeconds,
goSafeConvertFromSeconds,
largestUnitFromSeconds,
} from 'core/utils/duration-utils';
export default class TtlPickerComponent extends Component {
@tracked enableTTL = false;
@tracked recalculateSeconds = false;

View File

@@ -0,0 +1,52 @@
import { action } from '@ember/object';
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
/**
* Confirm that the user wants to discard unsaved changes before leaving the page.
* This decorator hooks into the willTransition action. If you override setupController,
* be sure to set 'model' on the controller to store data or this won't work.
*/
export function withConfirmLeave() {
return function decorator(SuperClass) {
if (!Object.prototype.isPrototypeOf.call(Route, SuperClass)) {
// eslint-disable-next-line
console.error(
'withConfirmLeave decorator must be used on instance of ember Route class. Decorator not applied to returned class'
);
return SuperClass;
}
return class ConfirmLeave extends SuperClass {
@service store;
@action
willTransition(transition) {
try {
super.willTransition(...arguments);
} catch (e) {
// if the SuperClass doesn't have willTransition
// defined it will throw an error.
}
const model = this.controller.get('model');
if (model && model.hasDirtyAttributes) {
if (
window.confirm(
'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?'
)
) {
// error is thrown when you attempt to unload a record that is inFlight (isSaving)
if (!model || !model.unloadRecord || model.isSaving) {
return;
}
model.rollbackAttributes();
model.destroy();
return true;
} else {
transition.abort();
return false;
}
}
}
};
};
}

View File

@@ -0,0 +1,41 @@
/**
* These utils are used for managing Duration type values
* (eg. '30m', '365d'). Most often used in the context of TTLs
*/
interface SecondsMap {
s: 1;
m: 60;
h: 3600;
d: 86400;
[key: string]: number;
}
export const secondsMap: SecondsMap = {
s: 1,
m: 60,
h: 3600,
d: 86400,
};
export const convertToSeconds = (time: number, unit: string) => {
return time * (secondsMap[unit] || 1);
};
export const convertFromSeconds = (seconds: number, unit: string) => {
return seconds / (secondsMap[unit] || 1);
};
export const goSafeConvertFromSeconds = (seconds: number, unit: string) => {
// Go only accepts s, m, or h units
const u = unit === 'd' ? 'h' : unit;
return convertFromSeconds(seconds, u) + u;
};
export const largestUnitFromSeconds = (seconds: number) => {
let unit = 's';
if (seconds === 0) return unit;
// get largest unit with no remainder
if (seconds % secondsMap.d === 0) {
unit = 'd';
} else if (seconds % secondsMap.h === 0) {
unit = 'h';
} else if (seconds % secondsMap.m === 0) {
unit = 'm';
}
return unit;
};

View File

@@ -1 +0,0 @@
export { default } from 'core/components/radio-select-ttl-or-string';

View File

@@ -0,0 +1 @@
export { withConfirmLeave } from 'core/decorators/confirm-leave';

View File

@@ -50,7 +50,7 @@
>
{{#if (gt val.length 0)}}
{{#each val as |key|}}
<span>{{key}},</span>
<span>{{key}}, </span>
{{/each}}
{{else}}
None
@@ -62,14 +62,23 @@
@value={{not val}}
@alwaysRender={{true}}
/>
{{else if (eq attr.name "customTtl")}}
{{! Show either notAfter or ttl }}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
@value={{or @role.notAfter @role.ttl}}
@alwaysRender={{true}}
@formatDate={{if @role.notAfter "MMM d yyyy HH:mm zzzz"}}
@formatTtl={{@role.ttl}}
/>
{{else}}
<InfoTableRow
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
@value={{val}}
@alwaysRender={{true}}
@formatDate={{eq attr.name "customTtl"}}
@type={{or attr.type attr.options.type}}
@defaultShown={{attr.options.defaultShown}}
@formatTtl={{eq attr.options.editType "ttl"}}
/>
{{/if}}
{{/let}}

View File

@@ -0,0 +1,56 @@
<div class="column is-narrow is-flex-center has-text-grey has-right-margin-s has-top-margin-negative-s pki-radiogroup-label">
<RadioButton
id="ttlType"
class="radio"
name="notValidAfterOption"
@value="ttl"
@onChange={{this.onRadioButtonChange}}
@groupValue={{this.groupValue}}
data-test-radio-button="ttl"
/>
<div class="has-left-margin-xs">
<label class="has-left-margin-xs" for="ttlType" data-test-radio-label="ttl">
<span class="ttl-picker-label is-large">TTL</span>
</label>
{{#if (eq this.groupValue "ttl")}}
<TtlPicker
data-test-input="ttl"
@onChange={{this.setAndBroadcastTtl}}
@label="TTL"
@helperTextEnabled={{@attr.options.helperTextEnabled}}
@description={{@attr.helpText}}
@initialValue={{@model.ttl}}
@hideToggle={{true}}
>
<label class="sr-only" for="ttl">Set relative certificate expiry with TTL</label>
</TtlPicker>
{{/if}}
</div>
</div>
<div class="column is-narrow is-flex-center has-text-grey has-right-margin-s pki-radiogroup-label">
<RadioButton
id="dateType"
class="radio"
name="notValidAfterOption"
@value="specificDate"
@onChange={{this.onRadioButtonChange}}
@groupValue={{this.groupValue}}
data-test-radio-button="not_after"
/>
<div class="has-left-margin-xs">
<label class="ttl-picker-label is-large" for="dateType" data-test-radio-label="specificDate">Specific date</label>
{{#if (eq this.groupValue "specificDate")}}
<label class="sr-only" for="not_after">Set certificate expiry with specified date value</label>
<Input
id="not_after"
@type="date"
autocomplete="off"
spellcheck="false"
@value={{this.formDate}}
{{on "input" this.setAndBroadcastInput}}
class="input"
data-test-input="not_after"
/>
{{/if}}
</div>
</div>

View File

@@ -0,0 +1,78 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { HTMLElementEvent } from 'forms';
import { format } from 'date-fns';
/**
* <PkiNotValidAfterForm /> components are used to manage two mutually exclusive role options in the form.
*/
interface Args {
model: {
notAfter: string;
ttl: string | number;
set: (key: string, value: string | number) => void;
};
}
export default class RadioSelectTtlOrString extends Component<Args> {
@tracked groupValue = 'ttl';
@tracked cachedNotAfter: string;
@tracked cachedTtl: string | number;
@tracked formDate: string;
constructor(owner: unknown, args: Args) {
super(owner, args);
const { model } = this.args;
this.cachedNotAfter = model.notAfter || '';
this.formDate = this.calculateFormDate(model.notAfter);
this.cachedTtl = model.ttl || '';
if (model.notAfter) {
this.groupValue = 'specificDate';
}
}
calculateFormDate(value: string) {
// API expects and returns full ISO string
// but the form input only accepts yyyy-MM-dd format
if (value) {
return format(new Date(value), 'yyyy-MM-dd');
}
return '';
}
@action onRadioButtonChange(selection: string) {
this.groupValue = selection;
// Clear the previous selection if they have clicked the other radio button.
if (selection === 'specificDate') {
this.args.model.ttl = '';
this.args.model.notAfter = this.cachedNotAfter;
this.formDate = this.calculateFormDate(this.cachedNotAfter);
}
if (selection === 'ttl') {
this.args.model.notAfter = '';
this.args.model.ttl = this.cachedTtl;
this.formDate = '';
}
}
@action setAndBroadcastTtl(ttlObject: { enabled: boolean; goSafeTimeString: string }) {
const { enabled, goSafeTimeString } = ttlObject;
if (this.groupValue === 'specificDate') {
// do not save ttl on the model unless the ttl radio button is selected
return;
}
const ttlVal = enabled === true ? goSafeTimeString : 0;
this.cachedTtl = ttlVal;
this.args.model.ttl = ttlVal;
}
@action setAndBroadcastInput(evt: HTMLElementEvent<HTMLInputElement>) {
const setDate = evt.target.valueAsDate?.toISOString();
if (!setDate) return;
this.cachedNotAfter = setDate;
this.args.model.notAfter = setDate;
this.formDate = this.calculateFormDate(setDate);
}
}

View File

@@ -1,26 +1,14 @@
<PageHeader as |p|>
<p.top>
<KeyValueHeader
@root={{hash label="role" text="role" path="vault.cluster.secrets.backend.pki.roles.index"}}
@isEngine={{true}}
>
<li>
<span class="sep">
/
</span>
<LinkTo @route="roles.index">
{{@model.backend}}
</LinkTo>
</li>
</KeyValueHeader>
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
</p.top>
<p.levelLeft>
<h1 class="title is-3">
<h1 class="title is-3" data-test-role-details-title>
{{#if @model.isNew}}
Create a PKI role
{{else}}
Edit a
{{@model.id}}
Edit
{{@model.name}}
{{/if}}
</h1>
</p.levelLeft>
@@ -43,7 +31,7 @@
@modelValidations={{this.modelValidations}}
@showHelpText={{false}}
>
<RadioSelectTtlOrString @attr={{attr}} @model={{@model}} />
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
</FormField>
{{/each}}
{{else}}

View File

@@ -27,6 +27,19 @@ export default class PkiRoleForm extends Component {
@tracked invalidFormAlert;
@tracked modelValidations;
get breadcrumbs() {
const backend = this.args.model.backend || 'pki';
const crumbs = [
{ label: 'secrets', route: 'secrets', linkExternal: true },
{ label: backend, route: 'overview' },
{ label: 'roles', route: 'roles.index' },
];
if (!this.args.model.isNew) {
crumbs.push({ label: this.args.model.id, route: 'roles.role.details' }, { label: 'edit' });
}
return crumbs;
}
@task
*save(event) {
event.preventDefault();

View File

@@ -1,14 +1,11 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
import PkiRolesIndexRoute from '.';
export default class PkiRolesCreateRoute extends Route {
@withConfirmLeave()
export default class PkiRolesCreateRoute extends PkiRolesIndexRoute {
@service store;
@service secretMountPath;
@service pathHelp;
beforeModel() {
return this.pathHelp.getNewModel('pki/role', 'pki');
}
model() {
return this.store.createRecord('pki/role', {

View File

@@ -1,3 +1,13 @@
import Route from '@ember/routing/route';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
import PkiRolesIndexRoute from '../index';
export default class PkiRoleEditRoute extends Route {}
@withConfirmLeave()
export default class PkiRoleEditRoute extends PkiRolesIndexRoute {
model() {
const { role } = this.paramsFor('roles/role');
return this.store.queryRecord('pki/role', {
backend: this.secretMountPath.currentPath,
id: role,
});
}
}

View File

@@ -1 +1,5 @@
roles.role.edit
<PkiRoleForm
@model={{this.model}}
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.id}}
@onSave={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.id}}
/>

View File

@@ -0,0 +1,10 @@
export const SELECTORS = {
radioTtl: '[data-test-radio-button="ttl"]',
radioTtlLabel: '[data-test-radio-label="ttl"]',
radioDate: '[data-test-radio-button="not_after"]',
radioDateLabel: '[data-test-radio-label="specificDate"]',
ttlForm: '[data-test-ttl-inputs]',
ttlTimeInput: '[data-test-ttl-value="TTL"]',
ttlUnitInput: '[data-test-select="ttl-unit"]',
dateInput: '[data-test-input="not_after"]',
};

View File

@@ -6,4 +6,5 @@ export const SELECTORS = {
noStoreValue: '[data-test-value-div="Store in storage backend"]',
keyUsageValue: '[data-test-value-div="Key usage"]',
extKeyUsageValue: '[data-test-value-div="Ext key usage"]',
customTtlValue: '[data-test-value-div="Issued certificates expire after"]',
};

View File

@@ -2,7 +2,7 @@ import { module, test } from 'qunit';
import { resolve } from 'rsvp';
import Service from '@ember/service';
import { setupRenderingTest } from 'ember-qunit';
import { render, triggerEvent } from '@ember/test-helpers';
import { render, settled, triggerEvent } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
const VALUE = 'test value';
@@ -264,4 +264,18 @@ module('Integration | Component | InfoTableRow', function (hooks) {
assert.dom('[data-test-value-div]').hasText(yearString, 'Renders date with passed format');
});
test('Formats the value as TTL when formatTtl present', async function (assert) {
this.set('value', 6000);
await render(hbs`<InfoTableRow
@label={{this.label}}
@value={{this.value}}
@formatTtl={{true}}
/>`);
assert.dom('[data-test-value-div]').hasText('100m', 'Translates number value to largest unit');
this.set('value', '45m');
await settled();
assert.dom('[data-test-value-div]').hasText('45m', 'Renders non-number values as-is');
});
});

View File

@@ -0,0 +1,133 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
import { SELECTORS } from 'vault/tests/helpers/pki/pki-not-valid-after-form';
module('Integration | Component | pki-not-valid-after-form', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.model = this.store.createRecord('pki/role', { backend: 'pki' });
this.attr = {
helpText: '',
options: {
helperTextEnabled: 'toggled on and shows text',
},
};
});
test('it should render the component with ttl selected by default', async function (assert) {
assert.expect(3);
await render(
hbs`
<div class="has-top-margin-xxl">
<PkiNotValidAfterForm
@model={{this.model}}
@attr={{this.attr}}
/>
</div>
`,
{ owner: this.engine }
);
assert.dom(SELECTORS.ttlForm).exists('shows the TTL picker');
assert.dom(SELECTORS.ttlTimeInput).hasValue('', 'default TTL is empty');
assert.dom(SELECTORS.radioTtl).isChecked('ttl is selected by default');
});
test('it clears and resets model properties from cache when changing radio selection', async function (assert) {
await render(
hbs`
<div class="has-top-margin-xxl">
<PkiNotValidAfterForm
@model={{this.model}}
@attr={{this.attr}}
/>
</div>
`,
{ owner: this.engine }
);
assert.dom(SELECTORS.radioTtl).isChecked('notBeforeDate radio is selected');
assert.dom(SELECTORS.ttlForm).exists({ count: 1 }, 'shows TTL form');
assert.dom(SELECTORS.radioDate).isNotChecked('NotAfter selection not checked');
assert.dom(SELECTORS.dateInput).doesNotExist('does not show date input field');
await click(SELECTORS.radioDateLabel);
assert.dom(SELECTORS.radioDate).isChecked('selects NotAfter radio when label clicked');
assert.dom(SELECTORS.dateInput).exists({ count: 1 }, 'shows date input field');
assert.dom(SELECTORS.radioTtl).isNotChecked('notBeforeDate radio is deselected');
assert.dom(SELECTORS.ttlForm).doesNotExist('hides TTL form');
const utcDate = '1994-11-05';
const notAfterExpected = '1994-11-05T00:00:00.000Z';
const ttlDate = 1;
await fillIn('[data-test-input="not_after"]', utcDate);
assert.strictEqual(
this.model.notAfter,
notAfterExpected,
'sets the model property notAfter when this value is selected and filled in.'
);
await click('[data-test-radio-button="ttl"]');
assert.strictEqual(
this.model.notAfter,
'',
'The notAfter is cleared on the model because the radio button was selected.'
);
await fillIn('[data-test-ttl-value="TTL"]', ttlDate);
assert.strictEqual(
this.model.ttl,
'1s',
'The ttl is now saved on the model because the radio button was selected.'
);
await click('[data-test-radio-button="not_after"]');
assert.strictEqual(this.model.ttl, '', 'TTL is cleared after radio select.');
assert.strictEqual(this.model.notAfter, notAfterExpected, 'notAfter gets populated from local cache');
});
test('Form renders properly for edit when TTL present', async function (assert) {
this.model = this.store.createRecord('pki/role', { backend: 'pki', ttl: 6000 });
await render(
hbs`
<div class="has-top-margin-xxl">
<PkiNotValidAfterForm
@model={{this.model}}
@attr={{this.attr}}
/>
</div>
`,
{ owner: this.engine }
);
assert.dom(SELECTORS.radioTtl).isChecked('notBeforeDate radio is selected');
assert.dom(SELECTORS.ttlForm).exists({ count: 1 }, 'shows TTL form');
assert.dom(SELECTORS.radioDate).isNotChecked('NotAfter selection not checked');
assert.dom(SELECTORS.dateInput).doesNotExist('does not show date input field');
assert.dom(SELECTORS.ttlTimeInput).hasValue('100', 'TTL value is correctly shown');
assert.dom(SELECTORS.ttlUnitInput).hasValue('m', 'TTL unit is correctly shown');
});
test('Form renders properly for edit when notAfter present', async function (assert) {
const utcDate = '1994-11-05T00:00:00.000Z';
this.model = this.store.createRecord('pki/role', { backend: 'pki', notAfter: utcDate });
await render(
hbs`
<div class="has-top-margin-xxl">
<PkiNotValidAfterForm
@model={{this.model}}
@attr={{this.attr}}
/>
</div>
`,
{ owner: this.engine }
);
assert.dom(SELECTORS.radioDate).isChecked('notAfter radio is selected');
assert.dom(SELECTORS.dateInput).exists({ count: 1 }, 'shows date picker');
assert.dom(SELECTORS.radioTtl).isNotChecked('ttl radio not selected');
assert.dom(SELECTORS.ttlForm).doesNotExist('does not show date TTL picker');
// Due to timezones, can't check specific match on input date
assert.dom(SELECTORS.dateInput).hasAnyValue('date input shows date');
});
});

View File

@@ -1,87 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
module('Integration | Component | radio-select-ttl-or-string', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'pki');
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.model = this.store.createRecord('pki/role');
this.model.backend = 'pki';
this.attr = {
helpText: '',
options: {
helperTextEnabled: 'toggled on and shows text',
},
};
});
test('it should render the component and init with ttl selected', async function (assert) {
assert.expect(3);
await render(
hbs`
<div class="has-top-margin-xxl">
<RadioSelectTtlOrString
@model={{this.model}}
@attr={{this.attr}}
/>
</div>
`,
{ owner: this.engine }
);
assert.dom('[data-test-ttl-inputs]').exists('shows the TTL component');
assert.dom('[data-test-ttl-value]').hasValue('', 'default TTL is empty');
assert.dom('[data-test-radio-button="ttl"]').isChecked('ttl is selected by default');
});
test('it should set the model properties ttl or notAfter based on the radio button selections', async function (assert) {
assert.expect(7);
await render(
hbs`
<div class="has-top-margin-xxl">
<RadioSelectTtlOrString
@model={{this.model}}
@attr={{this.attr}}
/>
</div>
`,
{ owner: this.engine }
);
assert.dom('[data-test-input="not_after"]').doesNotExist('does not show input field on initial render');
await click('[data-test-radio-button="not_after"]');
assert
.dom('[data-test-input="not_after"]')
.exists('does show input field after clicking the radio button');
const utcDate = '1994-11-05T08:15:30-05:0';
const ttlDate = 1;
await fillIn('[data-test-input="not_after"]', utcDate);
assert.strictEqual(
this.model.notAfter,
utcDate,
'sets the model property notAfter when this value is selected and filled in.'
);
await click('[data-test-radio-button="ttl"]');
assert.strictEqual(
this.model.notAfter,
'',
'The notAfter is cleared on the model because the radio button was selected.'
);
await fillIn('[data-test-ttl-value="TTL"]', ttlDate);
assert.strictEqual(
this.model.ttl,
'1s',
'The ttl is now saved on the model because the radio button was selected.'
);
await click('[data-test-radio-button="not_after"]');
assert.strictEqual(this.model.ttl, '', 'TTL is cleared after radio select.');
assert.strictEqual(this.model.notAfter, '', 'notAfter is cleared after radio select.');
});
});

View File

@@ -13,15 +13,16 @@ module('Integration | Component | pki role details page', function (hooks) {
this.store = this.owner.lookup('service:store');
this.model = this.store.createRecord('pki/role', {
name: 'Foobar',
backend: 'pki',
noStore: false,
keyUsage: [],
extKeyUsage: ['bar', 'baz'],
ttl: 600,
});
this.model.backend = 'pki';
});
test('it should render the page component', async function (assert) {
assert.expect(7);
assert.expect(8);
await render(
hbs`
<Page::PkiRoleDetails @role={{this.model}} />
@@ -38,5 +39,25 @@ module('Integration | Component | pki role details page', function (hooks) {
.dom(SELECTORS.extKeyUsageValue)
.hasText('bar, baz,', 'Key usage shows comma-joined values when array has items');
assert.dom(SELECTORS.noStoreValue).containsText('Yes', 'noStore shows opposite of what the value is');
assert.dom(SELECTORS.customTtlValue).containsText('10m', 'TTL shown as duration');
});
test('it should render the notAfter date if present', async function (assert) {
assert.expect(1);
this.model = this.store.createRecord('pki/role', {
name: 'Foobar',
backend: 'pki',
noStore: false,
keyUsage: [],
extKeyUsage: ['bar', 'baz'],
notAfter: '2030-05-04T12:00:00.000Z',
});
await render(
hbs`
<Page::PkiRoleDetails @role={{this.model}} />
`,
{ owner: this.engine }
);
assert.dom(SELECTORS.customTtlValue).containsText('May', 'Formats the notAfter date instead of TTL');
});
});