UI - identity details (#4502)

* add popups
* add ability to disable entity and banner when entity is disabled
* re-add alias-popup template
* add accpetance tests for creating entities
* add more entity creation acceptance tests
* add delete to edit-form
* add more identity tests and associated selectors
* add onSuccess hook and use UnloadModel route mixins
* add ability to toggle entity disabling from the popover
* fix store list cache because unloadAll isn't synchronous
* fill out tests for identity items and aliases
* add ability to enable entity from the detail page
* toArray on the peekAll
* fix other tests/behavior that relied on a RecordArray
* adjust layout for disabled entity and label for disabling an entity on the edit form
* add item-details integration tests
* move disable field on the entity form
* use ghost buttons for delete in identity and policy edit forms
* adding computed macros for lazy capability fetching and using them in the identity models
This commit is contained in:
Matthew Irish
2018-05-23 22:10:21 -05:00
committed by GitHub
parent 70434e28ed
commit 3bc90acdf5
85 changed files with 1426 additions and 220 deletions

View File

@@ -16,6 +16,7 @@ export default Ember.Component.extend({
class={{buttonClasses}}
type="button"
disabled={{disabled}}
data-test-confirm-action-trigger=true
{{action 'toggleConfirm'}}
>
{{yield}}

View File

@@ -0,0 +1,40 @@
import Ember from 'ember';
const { assert, inject, Component } = Ember;
export default Component.extend({
tagName: '',
flashMessages: inject.service(),
params: null,
successMessage() {
return 'Save was successful';
},
errorMessage() {
return 'There was an error saving';
},
onError(model) {
if (model && model.rollbackAttributes) {
model.rollbackAttributes();
}
},
onSuccess(){},
// override and return a promise
transaction() {
assert('override transaction call in an extension of popup-base', false);
},
actions: {
performTransaction() {
let args = [...arguments];
let messageArgs = this.messageArgs(...args);
return this.transaction(...args)
.then(() => {
this.get('onSuccess')();
this.get('flashMessages').success(this.successMessage(...messageArgs));
})
.catch(e => {
this.onError(...messageArgs);
this.get('flashMessages').success(this.errorMessage(e, ...messageArgs));
});
},
},
});

View File

@@ -2,20 +2,23 @@ import Ember from 'ember';
import { task } from 'ember-concurrency';
import { humanize } from 'vault/helpers/humanize';
const { computed } = Ember;
const { computed, inject } = Ember;
export default Ember.Component.extend({
flashMessages: inject.service(),
'data-test-component': 'identity-edit-form',
model: null,
// 'create', 'edit', 'merge'
mode: 'create',
/*
* @param Function
* @public
*
* Optional param to call a function upon successfully mounting a backend
*
* Optional param to call a function upon successfully saving an entity
*/
onSave: () => {},
cancelLink: computed('mode', 'model', function() {
cancelLink: computed('mode', 'model.identityType', function() {
let { model, mode } = this.getProperties('model', 'mode');
let key = `${mode}-${model.get('identityType')}`;
let routes = {
@@ -33,16 +36,17 @@ export default Ember.Component.extend({
return routes[key];
}),
getMessage(model) {
getMessage(model, isDelete = false) {
let mode = this.get('mode');
let typeDisplay = humanize([model.get('identityType')]);
let action = isDelete ? 'deleted' : 'saved';
if (mode === 'merge') {
return 'Successfully merged entities';
}
if (model.get('id')) {
return `Successfully saved ${typeDisplay} ${model.id}.`;
return `Successfully ${action} ${typeDisplay} ${model.id}.`;
}
return `Successfully saved ${typeDisplay}.`;
return `Successfully ${action} ${typeDisplay}.`;
},
save: task(function*() {
@@ -56,13 +60,26 @@ export default Ember.Component.extend({
return;
}
this.get('flashMessages').success(message);
yield this.get('onSave')(model);
yield this.get('onSave')({saveType: 'save', model});
}).drop(),
willDestroy() {
let model = this.get('model');
if (!model.isDestroyed || !model.isDestroying) {
if ((model.get('isDirty') && !model.isDestroyed) || !model.isDestroying) {
model.rollbackAttributes();
}
},
actions: {
deleteItem(model) {
let message = this.getMessage(model, true);
let flash = this.get('flashMessages');
model
.destroyRecord()
.then(() => {
flash.success(message);
return this.get('onSave')({saveType: 'delete', model});
});
},
},
});

View File

@@ -0,0 +1,23 @@
import Ember from 'ember';
const { inject } = Ember;
export default Ember.Component.extend({
flashMessages: inject.service(),
actions: {
enable(model) {
model.set('disabled', false);
model.save().
then(() => {
this.get('flashMessages').success(`Successfully enabled entity: ${model.id}`);
})
.catch(e => {
this.get('flashMessages').success(
`There was a problem enabling the entity: ${model.id} - ${e.error.join(' ') || e.message}`
);
});
}
}
});

View File

@@ -0,0 +1,22 @@
import Base from './_popup-base';
export default Base.extend({
messageArgs(model) {
let type = model.get('identityType');
let id = model.id;
return [type, id];
},
successMessage(type, id) {
return `Successfully deleted ${type}: ${id}`;
},
errorMessage(e, type, id) {
let error = e.errors ? e.errors.join(' ') : e.message;
return `There was a problem deleting ${type}: ${id} - ${error}`;
},
transaction(model) {
return model.destroyRecord();
},
});

View File

@@ -0,0 +1,34 @@
import Base from './_popup-base';
import Ember from 'ember';
const { computed } = Ember;
export default Base.extend({
model: computed.alias('params.firstObject'),
groupArray: computed('params', function() {
return this.get('params').objectAt(1);
}),
memberId: computed('params', function() {
return this.get('params').objectAt(2);
}),
messageArgs(/*model, groupArray, memberId*/) {
return [...arguments];
},
successMessage(model, groupArray, memberId) {
return `Successfully removed '${memberId}' from the group`;
},
errorMessage(e, model, groupArray, memberId) {
let error = e.errors ? e.errors.join(' ') : e.message;
return `There was a problem removing '${memberId}' from the group - ${error}`;
},
transaction(model, groupArray, memberId) {
let members = model.get(groupArray);
model.set(groupArray, members.without(memberId));
return model.save();
},
});

View File

@@ -0,0 +1,29 @@
import Base from './_popup-base';
import Ember from 'ember';
const { computed } = Ember;
export default Base.extend({
model: computed.alias('params.firstObject'),
key: computed('params', function() {
return this.get('params').objectAt(1);
}),
messageArgs(model, key) {
return [model, key];
},
successMessage(model, key) {
return `Successfully removed '${key}' from metadata`;
},
errorMessage(e, model, key) {
let error = e.errors ? e.errors.join(' ') : e.message;
return `There was a problem removing '${key}' from the metadata - ${error}`;
},
transaction(model, key) {
let metadata = model.get('metadata');
delete metadata[key];
model.set('metadata', { ...metadata });
return model.save();
},
});

View File

@@ -0,0 +1,29 @@
import Base from './_popup-base';
import Ember from 'ember';
const { computed } = Ember;
export default Base.extend({
model: computed.alias('params.firstObject'),
policyName: computed('params', function() {
return this.get('params').objectAt(1);
}),
messageArgs(model, policyName) {
return [model, policyName];
},
successMessage(model, policyName) {
return `Successfully removed '${policyName}' policy from ${model.id} `;
},
errorMessage(e, model, policyName) {
let error = e.errors ? e.errors.join(' ') : e.message;
return `There was a problem removing '${policyName}' policy - ${error}`;
},
transaction(model, policyName) {
let policies = model.get('policies');
model.set('policies', policies.without(policyName));
return model.save();
},
});

View File

@@ -1,6 +1,7 @@
import Ember from 'ember';
export default Ember.Component.extend({
'data-test-component': 'info-table-row',
classNames: ['info-table-row'],
isVisible: Ember.computed.or('alwaysRender', 'value'),

View File

@@ -6,6 +6,8 @@ const { computed } = Ember;
export default Ember.Component.extend({
type: null,
yieldWithoutColumn: false,
classNameBindings: ['containerClass'],
containerClass: computed('type', function() {

View File

@@ -9,6 +9,4 @@ export default Ember.Component.extend({
baseKey: null,
backendCrumb: null,
model: null,
});

View File

@@ -71,7 +71,7 @@ export default Ember.Component.extend(DEFAULTS, {
handleSuccess(resp, action) {
let props = {};
let secret = resp && resp.data || resp.auth;
let secret = (resp && resp.data) || resp.auth;
if (secret && action === 'unwrap') {
props = Ember.assign({}, props, { unwrap_data: secret });
}

View File

@@ -1,4 +1,10 @@
import Ember from 'ember';
import ListController from 'vault/mixins/list-controller';
export default Ember.Controller.extend(ListController);
export default Ember.Controller.extend(ListController, {
actions: {
onDelete() {
this.send('reload');
}
}
});

View File

@@ -4,7 +4,26 @@ import { task } from 'ember-concurrency';
export default Ember.Controller.extend({
showRoute: 'vault.cluster.access.identity.show',
showTab: 'details',
navToShow: task(function*(model) {
yield this.transitionToRoute(this.get('showRoute'), model.id, this.get('showTab'));
navAfterSave: task(function*({saveType, model}) {
let isDelete = saveType === 'delete';
let type = model.get('identityType');
let listRoutes= {
'entity-alias': 'vault.cluster.access.identity.aliases.index',
'group-alias': 'vault.cluster.access.identity.aliases.index',
'group': 'vault.cluster.access.identity.index',
'entity': 'vault.cluster.access.identity.index',
};
let routeName = listRoutes[type]
if (!isDelete) {
yield this.transitionToRoute(
this.get('showRoute'),
model.id,
this.get('showTab')
);
return;
}
yield this.transitionToRoute(
routeName
);
}),
});

View File

@@ -1,4 +1,46 @@
import Ember from 'ember';
import ListController from 'vault/mixins/list-controller';
export default Ember.Controller.extend(ListController);
const { inject } = Ember;
export default Ember.Controller.extend(ListController, {
flashMessages: inject.service(),
actions: {
delete(model) {
let type = model.get('identityType');
let id = model.id;
return model
.destroyRecord()
.then(() => {
this.send('reload');
this.get('flashMessages').success(`Successfully deleted ${type}: ${id}`);
})
.catch(e => {
this.get('flashMessages').success(
`There was a problem deleting ${type}: ${id} - ${e.error.join(' ') || e.message}`
);
});
},
toggleDisabled(model) {
let action = model.get('disabled') ? ['enabled', 'enabling'] : ['disabled', 'disabling'];
let type = model.get('identityType');
let id = model.id;
model.toggleProperty('disabled');
model.save().
then(() => {
this.get('flashMessages').success(`Successfully ${action[0]} ${type}: ${id}`);
})
.catch(e => {
this.get('flashMessages').success(
`There was a problem ${action[1]} ${type}: ${id} - ${e.error.join(' ') || e.message}`
);
});
},
reloadRecord(model) {
model.reload();
},
},
});

View File

@@ -1,9 +1,11 @@
import Ember from 'ember';
import utils from 'vault/lib/key-utils';
export default Ember.Controller.extend({
flashMessages: Ember.inject.service(),
clusterController: Ember.inject.controller('vault.cluster'),
const { inject, computed, Controller } = Ember;
export default Controller.extend({
flashMessages: inject.service(),
store: inject.service(),
clusterController: inject.controller('vault.cluster'),
queryParams: {
page: 'page',
pageFilter: 'pageFilter',
@@ -13,7 +15,7 @@ export default Ember.Controller.extend({
pageFilter: null,
filter: null,
backendCrumb: Ember.computed(function() {
backendCrumb: computed(function() {
return {
label: 'leases',
text: 'leases',
@@ -24,13 +26,13 @@ export default Ember.Controller.extend({
isLoading: false,
filterMatchesKey: Ember.computed('filter', 'model', 'model.[]', function() {
filterMatchesKey: computed('filter', 'model', 'model.[]', function() {
var filter = this.get('filter');
var content = this.get('model');
return !!(content.length && content.findBy('id', filter));
}),
firstPartialMatch: Ember.computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
firstPartialMatch: computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
var filter = this.get('filter');
var content = this.get('model');
var filterMatchesKey = this.get('filterMatchesKey');
@@ -42,7 +44,7 @@ export default Ember.Controller.extend({
});
}),
filterIsFolder: Ember.computed('filter', function() {
filterIsFolder: computed('filter', function() {
return !!utils.keyIsFolder(this.get('filter'));
}),
@@ -56,7 +58,7 @@ export default Ember.Controller.extend({
},
revokePrefix(prefix, isForce) {
const adapter = this.model.store.adapterFor('lease');
const adapter = this.get('store').adapterFor('lease');
const method = isForce ? 'forceRevokePrefix' : 'revokePrefix';
const fn = adapter[method];
fn

View File

@@ -66,6 +66,7 @@ export default Ember.Controller.extend(BackendCrumbMixin, {
delete(item) {
const name = item.id;
item.destroyRecord().then(() => {
this.send('reload');
this.get('flashMessages').success(`${name} was successfully deleted.`);
});
},

View File

@@ -0,0 +1,5 @@
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
export default function() {
return lazyCapabilities(apiPath`identity/${'identityType'}/id/${'id'}`, 'id', 'identityType');
}

View File

@@ -0,0 +1,25 @@
import { queryRecord } from 'ember-computed-query';
export function apiPath(strings, ...keys) {
return function(data) {
let dict = data || {};
let result = [strings[0]];
keys.forEach((key, i) => {
result.push(dict[key], strings[i + 1]);
});
return result.join('');
};
}
export default function() {
let [templateFn, ...keys] = arguments;
return queryRecord(
'capabilities',
context => {
return {
id: templateFn(context.getProperties(...keys)),
};
},
...keys
);
}

View File

@@ -1,8 +1,12 @@
import IdentityModel from './_base';
import DS from 'ember-data';
import Ember from 'ember';
import identityCapabilities from 'vault/macros/identity-capabilities';
const { attr, belongsTo } = DS;
const { computed } = Ember;
export default IdentityModel.extend({
parentType: 'entity',
formFields: ['name', 'mountAccessor', 'metadata'],
entity: belongsTo('identity/entity', { readOnly: true, async: false }),
@@ -12,7 +16,7 @@ export default IdentityModel.extend({
label: 'Auth Backend',
editType: 'mountAccessor',
}),
metadata: attr('object', {
metadata: attr({
editType: 'kv',
}),
mountPath: attr('string', {
@@ -28,4 +32,8 @@ export default IdentityModel.extend({
readOnly: true,
}),
mergedFromCanonicalIds: attr(),
updatePath: identityCapabilities(),
canDelete: computed.alias('updatePath.canDelete'),
canEdit: computed.alias('updatePath.canUpdate'),
});

View File

@@ -1,12 +1,23 @@
import Ember from 'ember';
import IdentityModel from './_base';
import DS from 'ember-data';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import identityCapabilities from 'vault/macros/identity-capabilities';
const { computed } = Ember;
const { attr, hasMany } = DS;
export default IdentityModel.extend({
formFields: ['name', 'policies', 'metadata'],
formFields: ['name', 'disabled', 'policies', 'metadata'],
name: attr('string'),
disabled: attr('boolean', {
defaultValue: false,
label: 'Disable entity',
helpText: 'All associated tokens cannot be used, but are not revoked.',
}),
mergedEntityIds: attr(),
metadata: attr('object', {
metadata: attr({
editType: 'kv',
}),
policies: attr({
@@ -28,4 +39,11 @@ export default IdentityModel.extend({
inheritedGroupIds: attr({
readOnly: true,
}),
updatePath: identityCapabilities(),
canDelete: computed.alias('updatePath.canDelete'),
canEdit: computed.alias('updatePath.canUpdate'),
aliasPath: lazyCapabilities(apiPath`identity/entity-alias`),
canAddAlias: computed.alias('aliasPath.canCreate'),
});

View File

@@ -1,8 +1,13 @@
import IdentityModel from './_base';
import DS from 'ember-data';
import Ember from 'ember';
import identityCapabilities from 'vault/macros/identity-capabilities';
const { attr, belongsTo } = DS;
const { computed } = Ember;
export default IdentityModel.extend({
parentType: 'group',
formFields: ['name', 'mountAccessor'],
group: belongsTo('identity/group', { readOnly: true, async: false }),
@@ -26,4 +31,9 @@ export default IdentityModel.extend({
lastUpdateTime: attr('string', {
readOnly: true,
}),
updatePath: identityCapabilities(),
canDelete: computed.alias('updatePath.canDelete'),
canEdit: computed.alias('updatePath.canUpdate'),
});

View File

@@ -1,6 +1,8 @@
import Ember from 'ember';
import IdentityModel from './_base';
import DS from 'ember-data';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
import identityCapabilities from 'vault/macros/identity-capabilities';
const { computed } = Ember;
const { attr, belongsTo } = DS;
@@ -52,4 +54,18 @@ export default IdentityModel.extend({
),
alias: belongsTo('identity/group-alias', { async: false, readOnly: true }),
updatePath: identityCapabilities(),
canDelete: computed.alias('updatePath.canDelete'),
canEdit: computed.alias('updatePath.canUpdate'),
aliasPath: lazyCapabilities(apiPath`identity/group-alias`),
canAddAlias: computed('aliasPath.canCreate', 'type', 'alias', function() {
let type = this.get('type');
let alias = this.get('alias');
// internal groups can't have aliases, and external groups can only have one
if (type === 'internal' || alias) {
return false;
}
return this.get('aliasPath.canCreate');
}),
});

View File

@@ -1,6 +1,8 @@
import Ember from 'ember';
import UnloadModelRoute from 'vault/mixins/unload-model-route';
import UnsavedModelRoute from 'vault/mixins/unsaved-model-route';
export default Ember.Route.extend({
export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, {
model(params) {
let itemType = this.modelFor('vault.cluster.access.identity');
let modelType = `identity/${itemType}-alias`;

View File

@@ -1,6 +1,8 @@
import Ember from 'ember';
import UnloadModelRoute from 'vault/mixins/unload-model-route';
import UnsavedModelRoute from 'vault/mixins/unsaved-model-route';
export default Ember.Route.extend({
export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, {
model(params) {
let itemType = this.modelFor('vault.cluster.access.identity');
let modelType = `identity/${itemType}-alias`;

View File

@@ -27,10 +27,14 @@ export default Ember.Route.extend(ListRoute, {
actions: {
willTransition(transition) {
window.scrollTo(0, 0);
if (transition.targetName !== this.routeName) {
if (!transition || transition.targetName !== this.routeName) {
this.store.clearAllDatasets();
}
return true;
},
reload() {
this.store.clearAllDatasets();
this.refresh();
}
},
});

View File

@@ -1,6 +1,8 @@
import Ember from 'ember';
import UnloadModelRoute from 'vault/mixins/unload-model-route';
import UnsavedModelRoute from 'vault/mixins/unsaved-model-route';
export default Ember.Route.extend({
export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, {
model() {
let itemType = this.modelFor('vault.cluster.access.identity');
let modelType = `identity/${itemType}`;

View File

@@ -1,6 +1,8 @@
import Ember from 'ember';
import UnloadModelRoute from 'vault/mixins/unload-model-route';
import UnsavedModelRoute from 'vault/mixins/unsaved-model-route';
export default Ember.Route.extend({
export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, {
model(params) {
let itemType = this.modelFor('vault.cluster.access.identity');
let modelType = `identity/${itemType}`;

View File

@@ -34,5 +34,9 @@ export default Ember.Route.extend(ListRoute, {
}
return true;
},
reload() {
this.store.clearAllDatasets();
this.refresh();
}
},
});

View File

@@ -13,13 +13,36 @@ export default Ember.Route.extend({
Ember.set(error, 'httpStatus', 404);
throw error;
}
// TODO peekRecord here to see if we have the record already
// if the record is in the store use that
let model = this.store.peekRecord(modelType, params.item_id);
// if we don't have creationTime, we only have a partial model so reload
if (model && !model.get('creationTime')) {
model = model.reload();
}
// if there's no model, we need to fetch it
if (!model) {
model = this.store.findRecord(modelType, params.item_id);
}
return Ember.RSVP.hash({
model: this.store.findRecord(modelType, params.item_id),
model,
section,
});
},
activate() {
// if we're just entering the route, and it's not a hard reload
// reload to make sure we have the newest info
if (this.currentModel) {
Ember.run.next(() => {
this.controller.get('model').reload();
});
}
},
afterModel(resolvedModel) {
let { section, model } = resolvedModel;
if (model.get('identityType') === 'group' && model.get('type') === 'internal' && section === 'aliases') {

View File

@@ -159,5 +159,9 @@ export default Ember.Route.extend({
}
return true;
},
reload() {
this.refresh();
this.store.clearAllDatasets();
}
},
});

View File

@@ -131,13 +131,25 @@ export default DS.Store.extend({
// pushes records into the store and returns the result
fetchPage(modelName, query) {
const response = this.constructResponse(modelName, query);
this.unloadAll(modelName);
this.push(
this.serializerFor(modelName).normalizeResponse(this, this.modelFor(modelName), response, null, 'query')
);
const model = this.peekAll(modelName);
model.set('meta', response.meta);
return model;
this.peekAll(modelName).forEach(record => {
record.unloadRecord();
});
return new Ember.RSVP.Promise(resolve => {
Ember.run.schedule('destroy', () => {
this.push(
this.serializerFor(modelName).normalizeResponse(
this,
this.modelFor(modelName),
response,
null,
'query'
)
);
let model = this.peekAll(modelName).toArray();
model.set('meta', response.meta);
resolve(model);
});
});
},
// get cached data

View File

@@ -49,6 +49,7 @@
height: auto;
width: 100%;
text-align: left;
text-decoration: none;
&:hover {
background-color: $menu-item-hover-background-color;

View File

@@ -133,6 +133,17 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
}
}
&.is-orange {
background-color: $orange;
border-color: $orange;
color: $white;
&:hover,
&.is-hovered {
background-color: darken($orange, 5%);
border-color: darken($orange, 5%);
}
}
&.is-compact {
height: 2rem;
padding: $size-11 $size-8;

View File

@@ -10,24 +10,39 @@
{{form-field data-test-field attr=attr model=model}}
{{/each}}
</div>
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button type="submit" data-test-identity-submit=true class="button is-primary {{if save.isRunning 'loading'}}" disabled={{save.isRunning}}>
{{#if (eq mode "create")}}
Create
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="field is-grouped">
<div class="control">
<button type="submit" data-test-identity-submit=true class="button is-primary {{if save.isRunning 'loading'}}" disabled={{save.isRunning}}>
{{#if (eq mode "create")}}
Create
{{else}}
Save
{{/if}}
</button>
{{#if (or (eq mode "merge") (eq mode "create" ))}}
<a href={{href-to cancelLink}} class="button" data-test-cancel-link>
Cancel
</a>
{{else}}
Save
<a href={{href-to cancelLink model.id "details"}} class="button" data-test-cancel-link>
Cancel
</a>
{{/if}}
</button>
{{#if (or (eq mode "merge") (eq mode "create" ))}}
<a href={{href-to cancelLink}} class="button">
Cancel
</a>
{{else}}
<a href={{href-to cancelLink model.id "details"}} class="button">
Cancel
</a>
{{/if}}
</div>
</div>
{{#if (and (eq mode "edit") model.canDelete)}}
{{#confirm-action
buttonClasses="button is-ghost"
onConfirmAction=(action "deleteItem" model)
confirmMessage=(concat "Are you sure you want to delete " model.id "?")
data-test-entity-item-delete=true
}}
Delete
{{/confirm-action}}
{{/if}}
</div>
</form>

View File

@@ -7,12 +7,12 @@
</div>
<div class="level-right">
{{#if (eq identityType "entity")}}
<a href="{{href-to 'vault.cluster.access.identity.merge'}}" class="button has-icon-right is-ghost is-compact" data-test-entity-merge-link=true>
<a href="{{href-to 'vault.cluster.access.identity.merge' (pluralize identityType)}}" class="button has-icon-right is-ghost is-compact" data-test-entity-merge-link=true>
Merge {{pluralize identityType}}
{{i-con glyph="chevron-right" size=11}}
</a>
{{/if}}
<a href="{{href-to 'vault.cluster.access.identity.create'}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true>
<a href="{{href-to 'vault.cluster.access.identity.create' (pluralize identityType)}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true>
Create {{identityType}}
{{i-con glyph="chevron-right" size=11}}
</a>

View File

@@ -1,4 +1,4 @@
{{info-table-row label="Name" value=model.name }}
{{info-table-row label="Name" value=model.name data-test-alias-name=true}}
{{info-table-row label="ID" value=model.id }}
{{#info-table-row label=(if (eq model.identityType "entity-alias") "Entity ID" "Group ID") value=model.canonicalId}}
<a href={{href-to 'vault.cluster.access.identity.show' (if (eq model.identityType "entity-alias") "entities" "groups") model.canonicalId "details"}}

View File

@@ -8,6 +8,9 @@
{{value}}
</div>
<div class="column has-text-right">
{{#if model.canEdit}}
{{identity/popup-metadata params=(array model key)}}
{{/if}}
</div>
</div>
</div>

View File

@@ -18,6 +18,7 @@
<code class="has-text-grey is-size-8">{{item.mountAccessor}}</code>
</div>
<div class="column has-text-right">
{{identity/popup-alias params=(array item)}}
</div>
</div>
{{/linked-block}}

View File

@@ -1,4 +1,20 @@
{{info-table-row label="Name" value=model.name }}
{{#if model.disabled}}
<div class="box is-shadowless is-marginless">
{{#message-in-page type="warning" yieldWithoutColumn=true messageClass="message-body is-marginless" data-test-disabled-warning=true}}
<div class="column">
<strong>Attention</strong> This {{model.identityType}} is disabled. All associated tokens cannot be used, but are not revoked.
</div>
{{#if model.canEdit}}
<div class="column is-flex-v-centered is-narrow">
<button type="button" class="button is-orange box" {{action "enable" model}} data-test-enable=true>
Enable
</button>
</div>
{{/if}}
{{/message-in-page}}
</div>
{{/if}}
{{info-table-row label="Name" value=model.name data-test-identity-item-name=true}}
{{info-table-row label="Type" value=model.type }}
{{info-table-row label="ID" value=model.id }}
{{#info-table-row label="Merged Ids" value=model.mergedEntityIds }}

View File

@@ -1,21 +1,56 @@
{{#if model.hasMembers}}
{{#each model.memberGroupIds as |gid|}}
<a href={{href-to "vault.cluster.access.identity.show" "groups" gid "details" }}
class="box is-sideless is-marginless"
>{{i-con
glyph='folder'
size=14
class="has-text-grey-light"
}}{{gid}}</a>
{{#linked-block
"vault.cluster.access.identity.show"
"groups"
gid
details
class="box is-sideless is-marginless"
}}
<div class="columns is-mobile">
<div class="column is-10">
<a href={{href-to "vault.cluster.access.identity.show" "groups" gid "details" }}
class="is-block has-text-black has-text-weight-semibold"
>{{i-con
glyph='folder'
size=14
class="has-text-grey-light"
}}{{gid}}</a>
</div>
<div class="column has-text-right">
{{#if model.canEdit}}
{{identity/popup-members params=(array model "memberGroupIds" gid)}}
{{/if}}
</div>
</div>
{{/linked-block}}
{{/each}}
{{#each model.memberEntityIds as |gid|}}
<a href={{href-to "vault.cluster.access.identity.show" "entities" gid "details" }}
{{#linked-block
"vault.cluster.access.identity.show"
"groups"
gid
details
class="box is-sideless is-marginless"
>{{i-con
glyph='role'
size=14
class="has-text-grey-light"
}}{{gid}}</a>
}}
<div class="columns">
<div class="column is-10">
<a href={{href-to "vault.cluster.access.identity.show" "entities" gid "details" }}
class="is-block has-text-black has-text-weight-semibold"
>{{i-con
glyph='role'
size=14
class="has-text-grey-light"
}}{{gid}}</a>
</div>
<div class="column has-text-right">
{{#if model.canEdit}}
{{identity/popup-members params=(array model "memberEntityIds" gid)}}
{{/if}}
</div>
</div>
{{/linked-block}}
{{/each}}
{{else}}
<div class="box is-bottomless has-background-white-bis">

View File

@@ -8,6 +8,9 @@
{{value}}
</div>
<div class="column has-text-right">
{{#if model.canEdit}}
{{identity/popup-metadata params=(array model key)}}
{{/if}}
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
{{#each model.policies as |item|}}
{{#each model.policies as |policyName|}}
{{#linked-block
"vault.cluster.policy.show"
"acl"
@@ -7,12 +7,15 @@
}}
<div class="columns is-mobile">
<div class="column is-10">
<a href={{href-to "vault.cluster.policy.show" "acl" item}}
class="has-text-black has-text-weight-semibold"
><span class="is-underline">{{item}}</span>
<a href={{href-to "vault.cluster.policy.show" "acl" policyName}}
class="is-block has-text-black has-text-weight-semibold"
><span class="is-underline">{{policyName}}</span>
</a>
</div>
<div class="column has-text-right">
{{#if model.canEdit}}
{{identity/popup-policy params=(array model policyName)}}
{{/if}}
</div>
</div>
{{/linked-block}}

View File

@@ -0,0 +1,45 @@
{{#popup-menu name="alias-menu"}}
{{#with params.firstObject as |item|}}
<nav class="menu">
<ul class="menu-list">
<li class="action">
<a href={{href-to "vault.cluster.access.identity.aliases.show" (pluralize item.parentType) item.id "details" }}>
Details
</a>
</li>
{{#if item.updatePath.isPending}}
<li class="action">
<button disabled=true type="button" class="link button is-loading is-transparent">
loading
</button>
</li>
{{else}}
{{#if item.canEdit}}
<li class="action">
<a href={{href-to "vault.cluster.access.identity.aliases.edit" (pluralize item.parentType) item.id}}>
Edit
</a>
</li>
{{/if}}
{{#if item.canDelete}}
<li class="action">
{{#confirm-action
data-test-item-delete=true
confirmButtonClasses="button is-primary"
buttonClasses="link"
onConfirmAction=(action "performTransaction" item)
confirmMessage=(concat "Are you sure you want to delete " item.id "?")
showConfirm=(get this (concat "shouldDelete-" item.id))
class=(if (get this (concat "shouldDelete-" item.id)) "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Delete
{{/confirm-action}}
</li>
{{/if}}
{{/if}}
</ul>
</nav>
{{/with}}
{{/popup-menu}}

View File

@@ -0,0 +1,21 @@
{{#popup-menu name="member-edit-menu"}}
<nav class="menu">
<ul class="menu-list">
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
confirmButtonText="Remove"
buttonClasses="link"
onConfirmAction=(action "performTransaction" model groupArray memberId)
confirmMessage=(concat "Are you sure you want to remove " memberId "?")
showConfirm=(get this (concat "shouldDelete-" memberId))
class=(if (get this (concat "shouldDelete-" memberId)) "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Remove
{{/confirm-action}}
</li>
</ul>
</nav>
{{/popup-menu}}

View File

@@ -0,0 +1,21 @@
{{#popup-menu name="metadata-edit-menu"}}
<nav class="menu">
<ul class="menu-list">
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
confirmButtonText="Remove"
buttonClasses="link"
onConfirmAction=(action "performTransaction" model key)
confirmMessage=(concat "Are you sure you want to remove " key "?")
showConfirm=(get this (concat "shouldDelete-" key))
class=(if (get this (concat "shouldDelete-" key)) "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Remove
{{/confirm-action}}
</li>
</ul>
</nav>
{{/popup-menu}}

View File

@@ -0,0 +1,31 @@
{{#popup-menu name="policy-menu"}}
<nav class="menu">
<ul class="menu-list">
<li class="action">
<a href={{href-to "vault.cluster.policy.show" "acl" policyName }}>
View Policy
</a>
</li>
<li class="action">
<a href={{href-to "vault.cluster.policy.edit" "acl" policyName }}>
Edit Policy
</a>
</li>
<li class="action">
{{#confirm-action
confirmButtonClasses="button is-primary"
confirmButtonText="Remove"
buttonClasses="link"
onConfirmAction=(action "performTransaction" model policyName)
confirmMessage=(concat "Are you sure you want to remove " policyName "?")
showConfirm=(get this (concat "shouldDelete-" policyName))
class=(if (get this (concat "shouldDelete-" policyName)) "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Remove from {{model.identityType}}
{{/confirm-action}}
</li>
</ul>
</nav>
{{/popup-menu}}

View File

@@ -8,11 +8,15 @@
excludeIconClass=true
}}
</div>
<div class="column">
<p>
<strong>{{alertType.text}}</strong>
{{#if yieldWithoutColumn}}
{{yield}}
</p>
</div>
{{else}}
<div class="column">
<p>
<strong>{{alertType.text}}</strong>
{{yield}}
</p>
</div>
{{/if}}
</div>
</div>

View File

@@ -8,4 +8,4 @@
</div>
</header>
{{identity/edit-form model=model onSave=(perform navToShow)}}
{{identity/edit-form model=model onSave=(perform navAfterSave)}}

View File

@@ -8,4 +8,4 @@
</div>
</header>
{{identity/edit-form mode="edit" model=model onSave=(perform navToShow)}}
{{identity/edit-form mode="edit" model=model onSave=(perform navAfterSave)}}

View File

@@ -1,23 +1,33 @@
{{identity/entity-nav identityType=identityType}}
{{#if model.meta.total}}
{{#each model as |item|}}
<a href={{href-to
{{#linked-block
"vault.cluster.access.identity.aliases.show"
item.id
"details"
"details"
class="box is-sideless is-marginless"
data-test-identity-row=true
}}
class="is-flex box is-sideless is-marginless"
data-test-lease-link={{item.id}}
>
{{i-con
glyph="role"
size=14
class="has-text-grey-light"
}}
<span class="has-text-weight-semibold">
{{item.id}}
</span>
</a>
<div class="columns is-mobile">
<div class="column is-10">
<a href={{href-to
"vault.cluster.access.identity.aliases.show"
item.id
"details"
}}
class="is-block has-text-black has-text-weight-semibold"
data-test-identity-link={{item.id}}
>{{i-con
glyph="role"
size=14
class="has-text-grey-light"
}}<span class="has-text-weight-semibold">{{item.id}}</span></a>
</div>
<div class="column has-text-right">
{{identity/popup-alias params=(array item) onSuccess=(action "onDelete")}}
</div>
</div>
{{/linked-block}}
{{/each}}
{{else}}
<div class="box is-bottomless has-background-white-bis">

View File

@@ -16,7 +16,7 @@
</h1>
</div>
<div class="level-right">
<a href="{{href-to 'vault.cluster.access.identity.aliases.edit' model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true>
<a href="{{href-to 'vault.cluster.access.identity.aliases.edit' model.id}}" class="button has-icon-right is-ghost is-compact" data-test-alias-edit-link=true>
Edit {{lowercase (humanize model.identityType)}}
{{i-con glyph="chevron-right" size=11}}
</a>

View File

@@ -8,4 +8,4 @@
</div>
</header>
{{identity/edit-form model=model onSave=(perform navToShow)}}
{{identity/edit-form model=model onSave=(perform navAfterSave)}}

View File

@@ -8,4 +8,4 @@
</div>
</header>
{{identity/edit-form mode="edit" model=model onSave=(perform navToShow)}}
{{identity/edit-form mode="edit" model=model onSave=(perform navAfterSave)}}

View File

@@ -1,23 +1,103 @@
{{identity/entity-nav identityType=identityType}}
{{#if model.meta.total}}
{{#each model as |item|}}
<a href={{href-to
{{#linked-block
"vault.cluster.access.identity.show"
item.id
"details"
class="box is-sideless is-marginless"
data-test-identity-row=true
}}
class="is-flex box is-sideless is-marginless"
data-test-lease-link={{item.id}}
>
{{i-con
glyph="role"
size=14
class="has-text-grey-light"
}}
<span class="has-text-weight-semibold">
{{item.id}}
</span>
</a>
<div class="columns is-mobile">
<div class="column is-10">
<a href={{href-to
"vault.cluster.access.identity.show"
item.id
"details"
}}
class="is-block has-text-black has-text-weight-semibold"
data-test-identity-link=true
>{{i-con
glyph="role"
size=14
class="has-text-grey-light"
}}<span class="has-text-weight-semibold">{{item.id}}</span></a>
</div>
<div class="column has-text-right">
{{#popup-menu name="identity-item" onOpen=(action "reloadRecord" item)}}
<nav class="menu">
<ul class="menu-list">
<li class="action">
<a href={{href-to "vault.cluster.access.identity.show" item.id "details" }}>
Details
</a>
</li>
{{#if (or item.isReloading item.updatePath.isPending item.aliasPath.isPending)}}
<li class="action">
<button disabled=true type="button" class="link button is-loading is-transparent">
loading
</button>
</li>
{{else}}
{{#if item.canEdit}}
<li class="action">
<a href={{href-to 'vault.cluster.access.identity.edit' item.id}}>
Edit
</a>
</li>
<li class="action">
{{#if item.disabled}}
<button type="button" {{action "toggleDisabled" item}} class="link">
Enable
</button>
{{else}}
{{#confirm-action
confirmButtonClasses="button is-primary"
confirmButtonText="Disable"
buttonClasses="link"
onConfirmAction=(action "toggleDisabled" item)
confirmMessage=(concat "Are you sure you want to disable " item.id "?")
showConfirm=(get this (concat "shouldDisable-" item.id))
class=(if (get this (concat "shouldDisable-" item.id)) "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Disable
{{/confirm-action}}
{{/if}}
</li>
{{/if}}
{{#if item.canAddAlias}}
<li class="action">
<a href={{href-to 'vault.cluster.access.identity.aliases.add' (pluralize identityType) item.id}}>
Add alias
</a>
</li>
{{/if}}
{{#if item.canDelete}}
<li class="action">
{{#confirm-action
data-test-item-delete=true
confirmButtonClasses="button is-primary"
buttonClasses="link"
onConfirmAction=(action "delete" item)
confirmMessage=(concat "Are you sure you want to delete " item.id "?")
showConfirm=(get this (concat "shouldDelete-" item.id))
class=(if (get this (concat "shouldDelete-" item.id)) "message is-block is-warning is-outline")
containerClasses="message-body is-block"
messageClasses="is-block"
}}
Delete
{{/confirm-action}}
</li>
{{/if}}
{{/if}}
</ul>
</nav>
{{/popup-menu}}
</div>
</div>
{{/linked-block}}
{{/each}}
{{else}}
<div class="box is-bottomless has-background-white-bis">

View File

@@ -8,4 +8,4 @@
</div>
</header>
{{identity/edit-form mode="merge" model=model onSave=(perform navToShow)}}
{{identity/edit-form mode="merge" model=model onSave=(perform navAfterSave)}}

View File

@@ -17,12 +17,12 @@
</div>
<div class="level-right">
{{#unless (or (and (eq model.identityType "group") (eq model.type "internal")) model.alias)}}
<a href="{{href-to 'vault.cluster.access.identity.aliases.add' model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true>
<a href="{{href-to 'vault.cluster.access.identity.aliases.add' (pluralize model.identityType) model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true>
Add alias
{{i-con glyph="chevron-right" size=11}}
</a>
{{/unless}}
<a href="{{href-to 'vault.cluster.access.identity.edit' model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true>
<a href="{{href-to 'vault.cluster.access.identity.edit' (pluralize model.identityType) model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-edit-link=true>
Edit {{model.identityType}}
{{i-con glyph="chevron-right" size=11}}
</a>
@@ -34,7 +34,7 @@
<ul>
{{#each (tabs-for-identity-show model.identityType model.type) as |tab|}}
{{#link-to "vault.cluster.access.identity.show" model.id tab tagName="li"}}
<a href={{href-to "vault.cluster.access.identity.show" model.id tab }}>
<a href={{href-to "vault.cluster.access.identity.show" (pluralize model.identityType) model.id tab }}>
{{capitalize tab}}
</a>
{{/link-to}}

View File

@@ -92,7 +92,7 @@
</div>
{{#if (and (not-eq model.id "default") capabilities.canDelete)}}
{{#confirm-action
buttonClasses="button is-link is-outlined is-inverted"
buttonClasses="button is-ghost"
onConfirmAction=(action "deletePolicy" model)
confirmMessage=(concat "Are you sure you want to delete " model.id "?")
data-test-policy-delete=true

View File

@@ -0,0 +1,77 @@
import page from 'vault/tests/pages/access/identity/aliases/add';
import aliasIndexPage from 'vault/tests/pages/access/identity/aliases/index';
import aliasShowPage from 'vault/tests/pages/access/identity/aliases/show';
import createItemPage from 'vault/tests/pages/access/identity/create';
import showItemPage from 'vault/tests/pages/access/identity/show';
export const testAliasCRUD = (name, itemType, assert) => {
let itemID;
let aliasID;
if (itemType === 'groups') {
createItemPage.createItem(itemType, 'external');
} else {
createItemPage.createItem(itemType);
}
andThen(() => {
let idRow = showItemPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0];
itemID = idRow.rowValue;
page.visit({ item_type: itemType, id: itemID });
});
page.editForm.name(name).submit();
andThen(() => {
let idRow = aliasShowPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0];
aliasID = idRow.rowValue;
assert.equal(
currentRouteName(),
'vault.cluster.access.identity.aliases.show',
'navigates to the correct route'
);
assert.ok(
aliasShowPage.flashMessage.latestMessage.startsWith('Successfully saved', `${itemType}: shows a flash message`)
);
assert.ok(aliasShowPage.nameContains(name), `${itemType}: renders the name on the show page`);
});
aliasIndexPage.visit({ item_type: itemType });
andThen(() => {
assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 1, `${itemType}: lists the entity in the entity list`);
aliasIndexPage.items.filterBy('id', aliasID)[0].menu();
});
aliasIndexPage.delete().confirmDelete();
andThen(() => {
assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 0, `${itemType}: the row is deleted`);
aliasIndexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`);
});
};
export const testAliasDeleteFromForm = (name, itemType, assert) => {
let itemID;
let aliasID;
if (itemType === 'groups') {
createItemPage.createItem(itemType, 'external');
} else {
createItemPage.createItem(itemType);
}
andThen(() => {
let idRow = showItemPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0];
itemID = idRow.rowValue;
page.visit({ item_type: itemType, id: itemID });
});
page.editForm.name(name).submit();
andThen(() => {
let idRow = aliasShowPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0];
aliasID = idRow.rowValue;
});
aliasShowPage.edit();
andThen(() => {
assert.equal(currentRouteName(), 'vault.cluster.access.identity.aliases.edit', `${itemType}: navigates to edit on create`);
});
page.editForm.delete().confirmDelete();
andThen(() => {
assert.equal(currentRouteName(), 'vault.cluster.access.identity.aliases.index', `${itemType}: navigates to list page on delete`);
assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 0, `${itemType}: the row does not show in the list`);
aliasIndexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`);
});
};

View File

@@ -0,0 +1,51 @@
import page from 'vault/tests/pages/access/identity/create';
import showPage from 'vault/tests/pages/access/identity/show';
import indexPage from 'vault/tests/pages/access/identity/index';
export const testCRUD = (name, itemType, assert) => {
let id;
page.visit({ item_type: itemType });
page.editForm.name(name).submit();
andThen(() => {
let idRow = showPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0];
id = idRow.rowValue;
assert.equal(currentRouteName(), 'vault.cluster.access.identity.show', `${itemType}: navigates to show on create`);
assert.ok(
showPage.flashMessage.latestMessage.startsWith('Successfully saved', `${itemType}: shows a flash message`)
);
assert.ok(showPage.nameContains(name), `${itemType}: renders the name on the show page`);
});
indexPage.visit({ item_type: itemType });
andThen(() => {
assert.equal(indexPage.items.filterBy('id', id).length, 1, `${itemType}: lists the entity in the entity list`);
indexPage.items.filterBy('id', id)[0].menu();
});
indexPage.delete().confirmDelete();
andThen(() => {
assert.equal(indexPage.items.filterBy('id', id).length, 0, `${itemType}: the row is deleted`);
indexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`);
});
};
export const testDeleteFromForm = (name, itemType, assert) => {
let id;
page.visit({ item_type: itemType });
page.editForm.name(name).submit();
andThen(() => {
id = showPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0].rowValue
});
showPage.edit();
andThen(() => {
assert.equal(currentRouteName(), 'vault.cluster.access.identity.edit', `${itemType}: navigates to edit on create`);
});
page.editForm.delete().confirmDelete();
andThen(() => {
assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', `${itemType}: navigates to list page on delete`);
assert.equal(indexPage.items.filterBy('id', id).length, 0, `${itemType}: the row does not show in the list`);
indexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`);
});
};

View File

@@ -0,0 +1,21 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import { testAliasCRUD, testAliasDeleteFromForm } from '../../_shared-alias-tests';
moduleForAcceptance('Acceptance | /access/identity/entities/aliases/add', {
beforeEach() {
return authLogin();
},
});
test('it allows create, list, delete of an entity alias', function(assert) {
let name = `alias-${Date.now()}`;
testAliasCRUD(name, 'entities', assert);
});
test('it allows delete from the edit form', function(assert) {
let name = `alias-${Date.now()}`;
testAliasDeleteFromForm(name, 'entities', assert);
});

View File

@@ -0,0 +1,32 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import page from 'vault/tests/pages/access/identity/create';
import { testCRUD, testDeleteFromForm } from '../_shared-tests';
moduleForAcceptance('Acceptance | /access/identity/entities/create', {
beforeEach() {
return authLogin();
},
});
test('it visits the correct page', function(assert) {
page.visit({ item_type: 'entities' });
andThen(() => {
assert.equal(
currentRouteName(),
'vault.cluster.access.identity.create',
'navigates to the correct route'
);
});
});
test('it allows create, list, delete of an entity', function(assert) {
let name = `entity-${Date.now()}`;
testCRUD(name, 'entities', assert);
});
test('it can be deleted from the edit form', function(assert) {
let name = `entity-${Date.now()}`;
testDeleteFromForm(name, 'entities', assert);
});

View File

@@ -0,0 +1,23 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import page from 'vault/tests/pages/access/identity/index';
moduleForAcceptance('Acceptance | /access/identity/entities', {
beforeEach() {
return authLogin();
},
});
test('it renders the entities page', function(assert) {
page.visit({ item_type: 'entities' });
andThen(() => {
assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route');
});
});
test('it renders the groups page', function(assert) {
page.visit({ item_type: 'groups' });
andThen(() => {
assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route');
});
});

View File

@@ -0,0 +1,21 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import { testAliasCRUD, testAliasDeleteFromForm } from '../../_shared-alias-tests';
moduleForAcceptance('Acceptance | /access/identity/groups/aliases/add', {
beforeEach() {
return authLogin();
},
});
test('it allows create, list, delete of an entity alias', function(assert) {
let name = `alias-${Date.now()}`;
testAliasCRUD(name, 'groups', assert);
});
test('it allows delete from the edit form', function(assert) {
let name = `alias-${Date.now()}`;
testAliasDeleteFromForm(name, 'groups', assert);
});

View File

@@ -0,0 +1,31 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import page from 'vault/tests/pages/access/identity/create';
import { testCRUD, testDeleteFromForm } from '../_shared-tests';
moduleForAcceptance('Acceptance | /access/identity/groups/create', {
beforeEach() {
return authLogin();
},
});
test('it visits the correct page', function(assert) {
page.visit({ item_type: 'groups' });
andThen(() => {
assert.equal(
currentRouteName(),
'vault.cluster.access.identity.create',
'navigates to the correct route'
);
});
});
test('it allows create, list, delete of an group', function(assert) {
let name = `group-${Date.now()}`;
testCRUD(name, 'groups', assert);
});
test('it can be deleted from the group edit form', function(assert) {
let name = `group-${Date.now()}`;
testDeleteFromForm(name, 'groups', assert);
});

View File

@@ -1,16 +0,0 @@
import { test } from 'qunit';
import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance';
import page from 'vault/tests/pages/access/identity/index';
moduleForAcceptance('Acceptance | /access/identity/entities', {
beforeEach() {
return authLogin();
},
});
test('it renders the page', function(assert) {
page.visit({ item_type: 'entities' });
andThen(() => {
assert.ok(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route');
});
});

View File

@@ -87,7 +87,9 @@ test('replication', function(assert) {
find('[data-test-mount-config-mode]').text().trim().toLowerCase().includes(mode),
'show page renders the correct mode'
);
assert.dom('[data-test-mount-config-paths]').hasText(mountPath, 'show page renders the correct mount path');
assert
.dom('[data-test-mount-config-paths]')
.hasText(mountPath, 'show page renders the correct mount path');
});
// click edit
@@ -101,10 +103,12 @@ test('replication', function(assert) {
`/vault/replication/performance/secondaries`,
'redirects to the secondaries page'
);
assert.dom('[data-test-flash-message-body]:contains(The performance mount filter)').hasText(
`The performance mount filter config for the secondary ${secondaryName} was successfully deleted.`,
'renders success flash upon deletion'
);
assert
.dom('[data-test-flash-message-body]:contains(The performance mount filter)')
.hasText(
`The performance mount filter config for the secondary ${secondaryName} was successfully deleted.`,
'renders success flash upon deletion'
);
click('[data-test-flash-message-body]:contains(The performance mount filter)');
});
@@ -149,10 +153,9 @@ test('replication', function(assert) {
});
click('[data-test-replication-link="secondaries"]');
andThen(() => {
assert.dom('[data-test-secondary-name]').hasText(
secondaryName,
'it displays the secondary in the list of known secondaries'
);
assert
.dom('[data-test-secondary-name]')
.hasText(secondaryName, 'it displays the secondary in the list of known secondaries');
});
// disable dr replication

View File

@@ -51,7 +51,9 @@ test('it renders the show page', function(assert) {
'vault.cluster.access.leases.show',
'a lease for the secret is in the list'
);
assert.dom('[data-test-lease-renew-picker]').doesNotExist('non-renewable lease does not render a renew picker');
assert
.dom('[data-test-lease-renew-picker]')
.doesNotExist('non-renewable lease does not render a renew picker');
});
});
@@ -65,7 +67,9 @@ skip('it renders the show page with a picker', function(assert) {
'vault.cluster.access.leases.show',
'a lease for the secret is in the list'
);
assert.dom('[data-test-lease-renew-picker]').exists({ count: 1 }, 'renewable lease renders a renew picker');
assert
.dom('[data-test-lease-renew-picker]')
.exists({ count: 1 }, 'renewable lease renders a renew picker');
});
});
@@ -84,7 +88,9 @@ test('it removes leases upon revocation', function(assert) {
click(`[data-test-lease-link="${this.enginePath}/"]`);
click('[data-test-lease-link="data/"]');
andThen(() => {
assert.dom(`[data-test-lease-link="${this.enginePath}/data/${this.name}/"]`).doesNotExist('link to the lease was removed with revocation');
assert
.dom(`[data-test-lease-link="${this.enginePath}/data/${this.name}/"]`)
.doesNotExist('link to the lease was removed with revocation');
});
});
@@ -99,16 +105,17 @@ test('it removes branches when a prefix is revoked', function(assert) {
'vault.cluster.access.leases.list-root',
'it navigates back to the leases root on revocation'
);
assert.dom(`[data-test-lease-link="${this.enginePath}/"]`).doesNotExist('link to the prefix was removed with revocation');
assert
.dom(`[data-test-lease-link="${this.enginePath}/"]`)
.doesNotExist('link to the prefix was removed with revocation');
});
});
test('lease not found', function(assert) {
visit('/vault/access/leases/show/not-found');
andThen(() => {
assert.dom('[data-test-lease-error]').hasText(
'not-found is not a valid lease ID',
'it shows an error when the lease is not found'
);
assert
.dom('[data-test-lease-error]')
.hasText('not-found is not a valid lease ID', 'it shows an error when the lease is not found');
});
});

View File

@@ -46,7 +46,9 @@ test('policies', function(assert) {
});
click('[data-test-policy-list-link]');
andThen(function() {
assert.dom(`[data-test-policy-link="${policyLower}"]`).exists({ count: 1 }, 'new policy shown in the list');
assert
.dom(`[data-test-policy-link="${policyLower}"]`)
.exists({ count: 1 }, 'new policy shown in the list');
});
// policy deletion
@@ -56,7 +58,9 @@ test('policies', function(assert) {
click('[data-test-confirm-button]');
andThen(function() {
assert.equal(currentURL(), `/vault/policies/acl`, 'navigates to policy list on successful deletion');
assert.dom(`[data-test-policy-item="${policyLower}"]`).doesNotExist('deleted policy is not shown in the list');
assert
.dom(`[data-test-policy-item="${policyLower}"]`)
.doesNotExist('deleted policy is not shown in the list');
});
});

View File

@@ -46,10 +46,6 @@ test('settings', function(assert) {
});
andThen(() => {
assert.ok(
currentURL(),
'/vault/secrets/${path}/configuration',
'navigates to the config page'
);
assert.ok(currentURL(), '/vault/secrets/${path}/configuration', 'navigates to the config page');
});
});

View File

@@ -143,7 +143,9 @@ test('ssh backend', function(assert) {
click(`[data-test-confirm-button]`);
andThen(() => {
assert.dom(`[data-test-secret-link="${role.name}"]`).doesNotExist(`${role.type}: role is no longer in the list`);
assert
.dom(`[data-test-secret-link="${role.name}"]`)
.doesNotExist(`${role.type}: role is no longer in the list`);
});
});
});

View File

@@ -118,50 +118,46 @@ test('tools functionality', function(assert) {
click('[data-test-tools-b64-toggle="input"]');
click('[data-test-tools-submit]');
andThen(() => {
assert.dom('[data-test-tools-input="sum"]').hasValue(
'LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=',
'hashes the data, encodes input'
);
assert
.dom('[data-test-tools-input="sum"]')
.hasValue('LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=', 'hashes the data, encodes input');
});
click('[data-test-tools-back]');
fillIn('[data-test-tools-input="hash-input"]', 'e2RhdGE6ImZvbyJ9');
click('[data-test-tools-submit]');
andThen(() => {
assert.dom('[data-test-tools-input="sum"]').hasValue(
'JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=',
'hashes the data, passes b64 input through'
);
assert
.dom('[data-test-tools-input="sum"]')
.hasValue('JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=', 'hashes the data, passes b64 input through');
});
});
const AUTH_RESPONSE = {
"request_id": "39802bc4-235c-2f0b-87f3-ccf38503ac3e",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": null,
"wrap_info": null,
"warnings": null,
"auth": {
"client_token": "ecfc2758-588e-981d-50f4-a25883bbf03c",
"accessor": "6299780b-f2b2-1a3f-7b83-9d3d67629249",
"policies": [
"root"
],
"metadata": null,
"lease_duration": 0,
"renewable": false,
"entity_id": ""
}
request_id: '39802bc4-235c-2f0b-87f3-ccf38503ac3e',
lease_id: '',
renewable: false,
lease_duration: 0,
data: null,
wrap_info: null,
warnings: null,
auth: {
client_token: 'ecfc2758-588e-981d-50f4-a25883bbf03c',
accessor: '6299780b-f2b2-1a3f-7b83-9d3d67629249',
policies: ['root'],
metadata: null,
lease_duration: 0,
renewable: false,
entity_id: '',
},
};
test('ensure unwrap with auth block works properly', function(assert) {
this.server = new Pretender(function() {
this.post('/v1/sys/wrapping/unwrap', response => {
return [response, { 'Content-Type': 'application/json' }, JSON.stringify(AUTH_RESPONSE)];
});
this.server = new Pretender(function() {
this.post('/v1/sys/wrapping/unwrap', response => {
return [response, { 'Content-Type': 'application/json' }, JSON.stringify(AUTH_RESPONSE)];
});
});
visit('/vault/tools');
//unwrap
click('[data-test-tools-action-link="unwrap"]');

View File

@@ -101,17 +101,21 @@ const testEncryption = (assert, keyName) => {
);
},
assertBeforeDecrypt: key => {
assert.dom('[data-test-transit-input="context"]').hasValue(
'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=',
`${key}: the ui shows the base64-encoded context`
);
assert
.dom('[data-test-transit-input="context"]')
.hasValue(
'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=',
`${key}: the ui shows the base64-encoded context`
);
},
assertAfterDecrypt: key => {
assert.dom('[data-test-transit-input="plaintext"]').hasValue(
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
`${key}: the ui shows the base64-encoded plaintext`
);
assert
.dom('[data-test-transit-input="plaintext"]')
.hasValue(
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
`${key}: the ui shows the base64-encoded plaintext`
);
},
},
// raw bytes for plaintext, string for context
@@ -128,13 +132,17 @@ const testEncryption = (assert, keyName) => {
);
},
assertBeforeDecrypt: key => {
assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('context'), `${key}: the ui shows the input context`);
assert
.dom('[data-test-transit-input="context"]')
.hasValue(encodeString('context'), `${key}: the ui shows the input context`);
},
assertAfterDecrypt: key => {
assert.dom('[data-test-transit-input="plaintext"]').hasValue(
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
`${key}: the ui shows the base64-encoded plaintext`
);
assert
.dom('[data-test-transit-input="plaintext"]')
.hasValue(
'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=',
`${key}: the ui shows the base64-encoded plaintext`
);
},
},
// base64 input
@@ -151,10 +159,14 @@ const testEncryption = (assert, keyName) => {
);
},
assertBeforeDecrypt: key => {
assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('context'), `${key}: the ui shows the input context`);
assert
.dom('[data-test-transit-input="context"]')
.hasValue(encodeString('context'), `${key}: the ui shows the input context`);
},
assertAfterDecrypt: key => {
assert.dom('[data-test-transit-input="plaintext"]').hasValue('This is the secret', `${key}: the ui decodes plaintext`);
assert
.dom('[data-test-transit-input="plaintext"]')
.hasValue('This is the secret', `${key}: the ui decodes plaintext`);
},
},
@@ -173,11 +185,15 @@ const testEncryption = (assert, keyName) => {
);
},
assertBeforeDecrypt: key => {
assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`);
assert
.dom('[data-test-transit-input="context"]')
.hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`);
},
assertAfterDecrypt: key => {
assert.ok(findWithAssert('[data-test-transit-input="plaintext"]'), `${key}: plaintext box shows`);
assert.dom('[data-test-transit-input="plaintext"]').hasValue('There are many secrets 🤐', `${key}: the ui decodes plaintext`);
assert
.dom('[data-test-transit-input="plaintext"]')
.hasValue('There are many secrets 🤐', `${key}: the ui decodes plaintext`);
},
},
];
@@ -229,12 +245,16 @@ test('transit backend', function(assert) {
if (index === 0) {
click('[data-test-transit-link="versions"]');
andThen(() => {
assert.dom('[data-test-transit-key-version-row]').exists({ count: 1 }, `${key.name}: only one key version`);
assert
.dom('[data-test-transit-key-version-row]')
.exists({ count: 1 }, `${key.name}: only one key version`);
});
click('[data-test-transit-key-rotate] button');
click('[data-test-confirm-button]');
andThen(() => {
assert.dom('[data-test-transit-key-version-row]').exists({ count: 2 }, `${key.name}: two key versions after rotate`);
assert
.dom('[data-test-transit-key-version-row]')
.exists({ count: 2 }, `${key.name}: two key versions after rotate`);
});
}
click('[data-test-transit-key-actions-link]');
@@ -256,7 +276,9 @@ test('transit backend', function(assert) {
`${key.name}: exportable key has a link to export action`
);
} else {
assert.dom('[data-test-transit-action-link="export"]').doesNotExist(`${key.name}: non-exportable key does not link to export action`);
assert
.dom('[data-test-transit-action-link="export"]')
.doesNotExist(`${key.name}: non-exportable key does not link to export action`);
}
if (key.convergent && key.supportsEncryption) {
testEncryption(assert, key.name);

View File

@@ -0,0 +1,56 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { create } from 'ember-cli-page-object';
import itemDetails from 'vault/tests/pages/components/identity/item-details';
import Ember from 'ember';
const component = create(itemDetails);
const { getOwner } = Ember;
moduleForComponent('identity/item-details', 'Integration | Component | identity/item details', {
integration: true,
beforeEach() {
component.setContext(this);
getOwner(this).lookup('service:flash-messages').registerTypes(['success']);
},
afterEach() {
component.removeContext();
}
});
test('it renders the disabled warning', function(assert) {
let model = Ember.Object.create({
save() {
return Ember.RSVP.resolve();
},
disabled: true,
canEdit: true
});
sinon.spy(model, 'save');
this.set('model', model);
this.render(hbs`{{identity/item-details model=model}}`);
assert.dom('[data-test-disabled-warning]').exists();
component.enable();
assert.ok(model.save.calledOnce, 'clicking enable calls model save');
});
test('it does not render the button if canEdit is false', function(assert) {
let model = Ember.Object.create({
disabled: true
});
this.set('model', model);
this.render(hbs`{{identity/item-details model=model}}`);
assert.dom('[data-test-disabled-warning]').exists('shows the warning banner');
assert.dom('[data-test-enable]').doesNotExist('does not show the enable button');
});
test('it does not render the banner when item is enabled', function(assert) {
let model = Ember.Object.create();
this.set('model', model);
this.render(hbs`{{identity/item-details model=model}}`);
assert.dom('[data-test-disabled-warning]').doesNotExist('does not show the warning banner');
});

View File

@@ -0,0 +1,7 @@
import { create, visitable } from 'ember-cli-page-object';
import editForm from 'vault/tests/pages/components/identity/edit-form';
export default create({
visit: visitable('/vault/access/identity/:item_type/aliases/add/:id'),
editForm,
});

View File

@@ -0,0 +1,13 @@
import { create, clickable, text, visitable, collection } from 'ember-cli-page-object';
import flashMessage from 'vault/tests/pages/components/flash-message';
export default create({
visit: visitable('/vault/access/identity/:item_type/aliases'),
flashMessage,
items: collection('[data-test-identity-row]', {
menu: clickable('[data-test-popup-menu-trigger]'),
id: text('[data-test-identity-link]'),
}),
delete: clickable('[data-test-item-delete] [data-test-confirm-action-trigger]'),
confirmDelete: clickable('[data-test-item-delete] [data-test-confirm-button]'),
});

View File

@@ -0,0 +1,11 @@
import { create, clickable, collection, contains, visitable } from 'ember-cli-page-object';
import flashMessage from 'vault/tests/pages/components/flash-message';
import infoTableRow from 'vault/tests/pages/components/info-table-row';
export default create({
visit: visitable('/vault/access/identity/:item_type/aliases/:alias_id'),
flashMessage,
nameContains: contains('[data-test-alias-name]'),
rows: collection('[data-test-component="info-table-row"]', infoTableRow),
edit: clickable('[data-test-alias-edit-link]')
});

View File

@@ -0,0 +1,13 @@
import { create, visitable } from 'ember-cli-page-object';
import editForm from 'vault/tests/pages/components/identity/edit-form';
export default create({
visit: visitable('/vault/access/identity/:item_type/create'),
editForm,
createItem(item_type, type) {
if (type) {
return this.visit({item_type}).editForm.type(type).submit();
}
return this.visit({item_type}).editForm.submit();
}
});

View File

@@ -1,4 +1,13 @@
import { create, visitable } from 'ember-cli-page-object';
import { create, clickable, text, visitable, collection } from 'ember-cli-page-object';
import flashMessage from 'vault/tests/pages/components/flash-message';
export default create({
visit: visitable('/vault/access/identity/:item_type'),
flashMessage,
items: collection('[data-test-identity-row]', {
menu: clickable('[data-test-popup-menu-trigger]'),
id: text('[data-test-identity-link]'),
}),
delete: clickable('[data-test-item-delete] [data-test-confirm-action-trigger]'),
confirmDelete: clickable('[data-test-item-delete] [data-test-confirm-button]'),
});

View File

@@ -0,0 +1,11 @@
import { create, clickable, collection, contains, visitable } from 'ember-cli-page-object';
import flashMessage from 'vault/tests/pages/components/flash-message';
import infoTableRow from 'vault/tests/pages/components/info-table-row';
export default create({
visit: visitable('/vault/access/identity/:item_type/:item_id'),
flashMessage,
nameContains: contains('[data-test-identity-item-name]'),
rows: collection('[data-test-component="info-table-row"]', infoTableRow),
edit: clickable('[data-test-entity-edit-link]')
});

View File

@@ -0,0 +1,14 @@
import { clickable, fillable, attribute } from 'ember-cli-page-object';
import fields from '../form-field';
export default {
...fields,
cancelLinkHref: attribute('href', '[data-test-cancel-link]'),
cancelLink: clickable('[data-test-cancel-link]'),
name: fillable('[data-test-input="name"]'),
disabled: clickable('[data-test-input="disabled"]'),
type: fillable('[data-test-input="type"]'),
submit: clickable('[data-test-identity-submit]'),
delete: clickable('[data-test-confirm-action-trigger]'),
confirmDelete: clickable('[data-test-confirm-button]'),
};

View File

@@ -0,0 +1,5 @@
import { clickable } from 'ember-cli-page-object';
export default {
enable: clickable('[data-test-enable]'),
};

View File

@@ -0,0 +1,7 @@
import { text, isPresent } from 'ember-cli-page-object';
export default {
hasLabel: isPresent('[data-test-row-label]'),
rowLabel: text('[data-test-row-label]'),
rowValue: text('[data-test-row-value]'),
};

View File

@@ -0,0 +1,73 @@
import { moduleForComponent, test } from 'ember-qunit';
import sinon from 'sinon';
import Ember from 'ember';
moduleForComponent('identity/edit-form', 'Unit | Component | identity/edit-form', {
unit: true,
needs: ['service:auth', 'service:flash-messages'],
});
let testCases = [
{
identityType: 'entity',
mode: 'create',
expected: 'vault.cluster.access.identity',
},
{
identityType: 'entity',
mode: 'edit',
expected: 'vault.cluster.access.identity.show',
},
{
identityType: 'entity-merge',
mode: 'merge',
expected: 'vault.cluster.access.identity',
},
{
identityType: 'entity-alias',
mode: 'create',
expected: 'vault.cluster.access.identity.aliases',
},
{
identityType: 'entity-alias',
mode: 'edit',
expected: 'vault.cluster.access.identity.aliases.show',
},
{
identityType: 'group',
mode: 'create',
expected: 'vault.cluster.access.identity',
},
{
identityType: 'group',
mode: 'edit',
expected: 'vault.cluster.access.identity.show',
},
{
identityType: 'group-alias',
mode: 'create',
expected: 'vault.cluster.access.identity.aliases',
},
{
identityType: 'group-alias',
mode: 'edit',
expected: 'vault.cluster.access.identity.aliases.show',
},
];
testCases.forEach(function(testCase) {
let model = Ember.Object.create({
identityType: testCase.identityType,
rollbackAttributes: sinon.spy(),
});
test(`it computes cancelLink properly: ${testCase.identityType} ${testCase.mode}`, function(assert) {
let component = this.subject();
component.set('mode', testCase.mode);
component.set('model', model);
assert.equal(
component.get('cancelLink'),
testCase.expected,
'cancel link is correct'
);
});
});

View File

@@ -89,6 +89,7 @@ test('store.constructResponse', function(assert) {
});
test('store.fetchPage', function(assert) {
let done = assert.async(4);
const keys = ['zero', 'one', 'two', 'three', 'four', 'five', 'six'];
const data = {
data: {
@@ -106,11 +107,14 @@ test('store.fetchPage', function(assert) {
let result;
Ember.run(() => {
result = store.fetchPage('transit-key', query);
store.fetchPage('transit-key', query).then(r => {
result = r;
done();
});
});
assert.ok(result.get('length'), pageSize, 'returns the correct number of items');
assert.deepEqual(result.toArray().mapBy('id'), keys.slice(0, pageSize), 'returns the first page of items');
assert.deepEqual(result.mapBy('id'), keys.slice(0, pageSize), 'returns the first page of items');
assert.deepEqual(
result.get('meta'),
{
@@ -125,44 +129,54 @@ test('store.fetchPage', function(assert) {
);
Ember.run(() => {
result = store.fetchPage('transit-key', {
store.fetchPage('transit-key', {
size: pageSize,
page: 3,
responsePath: 'data.keys',
}).then(r => {
result = r;
done()
});
});
const pageThreeEnd = 3 * pageSize;
const pageThreeStart = pageThreeEnd - pageSize;
assert.deepEqual(
result.toArray().mapBy('id'),
result.mapBy('id'),
keys.slice(pageThreeStart, pageThreeEnd),
'returns the third page of items'
);
Ember.run(() => {
result = store.fetchPage('transit-key', {
store.fetchPage('transit-key', {
size: pageSize,
page: 99,
responsePath: 'data.keys',
}).then(r => {
result = r;
done();
});
});
assert.deepEqual(
result.toArray().mapBy('id'),
result.mapBy('id'),
keys.slice(keys.length - 1),
'returns the last page when the page value is beyond the of bounds'
);
Ember.run(() => {
result = store.fetchPage('transit-key', {
store.fetchPage('transit-key', {
size: pageSize,
page: 0,
responsePath: 'data.keys',
}).then(r => {
result = r;
done();
});
});
assert.deepEqual(
result.toArray().mapBy('id'),
result.mapBy('id'),
keys.slice(0, pageSize),
'returns the first page when page value is under the bounds'
);