Merge remote-tracking branch 'origin/develop' into ICU-606-role-acceptance-test

This commit is contained in:
Susmitha Girumala committed 2020-09-22 16:12:40 -04:00
commit 3f35255652
84 files changed
+1112 -226

No files matched your search

@@ -0,0 +1,36 @@
import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';
export default class GeneratedSessionModel extends BaseModel {
// =attributes
@attr('string') user_id;
@attr('string') target_id;
@attr('string') host_id;
@attr('string') host_set_id;
@attr('string', {
description: 'The name of the current status of this session.',
readOnly: true,
})
status;
@attr('date', {
description: 'The time this resource was created\nOutput only.',
readOnly: true,
})
created_time;
@attr('date', {
description: 'The time this resource was last updated\nOutput only.',
readOnly: true,
})
updated_time;
@attr('number', {
description: 'Current version number of this resource.',
})
version;
}
+24 -1
View File
@@ -1,13 +1,32 @@
import GeneratedAccountModel from '../generated/models/account';
import { fragment } from 'ember-data-model-fragments/attributes';
import { computed } from '@ember/object';
export default class AccountModel extends GeneratedAccountModel {
// =attributes
/**
* Attributes of this resource, if any, represented as a JSON fragment.
* @type {FragmentAccountAttributesModel}
*/
@fragment('fragment-account-attributes', { defaultValue: {} }) attributes;
/**
* Convenience for getting username in account.
* @type {string}
*/
@computed('type', 'attributes.{login_name}')
get username() {
let username = '';
switch (this.type) {
case 'password':
username = this.attributes.login_name;
break;
}
return username;
}
// =methods
/**
@@ -41,7 +60,11 @@ export default class AccountModel extends GeneratedAccountModel {
* @param {object} options.adapterOptions
* @return {Promise}
*/
changePassword(currentPassword, newPassword, options = { adapterOptions: {} }) {
changePassword(
currentPassword,
newPassword,
options = { adapterOptions: {} }
) {
const defaultAdapterOptions = {
method: 'change-password',
currentPassword,
+1
View File
@@ -26,6 +26,7 @@ import { get } from '@ember/object';
* 2) Or as an ID: `record.scopeID`.
*/
export default class BaseModel extends Model {
// =relationships
/**
+1
View File
@@ -2,6 +2,7 @@ import GeneratedHostSetModel from '../generated/models/host-set';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
export default class HostSetModel extends GeneratedHostSetModel {
// =attributes
/**
+2
View File
@@ -2,9 +2,11 @@ import GeneratedHostModel from '../generated/models/host';
import { fragment } from 'ember-data-model-fragments/attributes';
export default class HostModel extends GeneratedHostModel {
/**
* Attributes of this resource, if any, represented as a JSON fragment.
* @type {FragmentHostAttributesModel}
*/
@fragment('fragment-host-attributes', { defaultValue: {} }) attributes;
}
+27
View File
@@ -0,0 +1,27 @@
import GeneratedSessionModel from '../generated/models/session';
export default class SessionModel extends GeneratedSessionModel {
// =methods
/**
* Cancels the session via the `cancel` method.
* See serializer and adapter for more information.
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
cancelSession(options = { adapterOptions: {} }) {
const defaultAdapterOptions = {
method: 'cancel',
};
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
}
+13 -2
View File
@@ -1,6 +1,14 @@
import ApplicationSerializer from './application';
export default class AccountSerializer extends ApplicationSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = false;
// =methods
/**
@@ -22,8 +30,11 @@ export default class AccountSerializer extends ApplicationSerializer {
}
if (snapshot?.adapterOptions?.method === 'change-password') {
const { currentPassword, newPassword } = snapshot?.adapterOptions;
serialized =
this.serializeForChangePassword(snapshot, currentPassword, newPassword);
serialized = this.serializeForChangePassword(
snapshot,
currentPassword,
newPassword
);
}
return serialized;
}
+12 -2
View File
@@ -7,6 +7,14 @@ import { copy } from 'ember-copy';
* Manages serialization/normalization of data to/from the API.
*/
export default class ApplicationSerializer extends RESTSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = true;
// =methods
/**
@@ -71,8 +79,10 @@ export default class ApplicationSerializer extends RESTSerializer {
// Delete `scope` field
delete serialized.scope;
// And serialize `scope_id`
const scope_id = snapshot?.attr('scope')?.attr('scope_id');
if (scope_id) serialized.scope_id = scope_id;
if(this.serializeScopeID) {
const scope_id = snapshot?.attr('scope')?.attr('scope_id');
if (scope_id) serialized.scope_id = scope_id;
}
return serialized;
}
+8
View File
@@ -1,6 +1,14 @@
import ApplicationSerializer from './application';
export default class HostSetSerializer extends ApplicationSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = false;
// =methods
/**
+12
View File
@@ -0,0 +1,12 @@
import ApplicationSerializer from './application';
export default class HostSerializer extends ApplicationSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = false;
}
+31
View File
@@ -0,0 +1,31 @@
import ApplicationSerializer from './application';
export default class SessionSerializer extends ApplicationSerializer {
// =methods
/**
* If `adapterOptions.method` is `cancel`, the serialization should
* include the version field only.
* @override
* @param {Snapshot} snapshot
* @return {object}
*/
serialize(snapshot) {
let serialized = super.serialize(...arguments);
if (snapshot?.adapterOptions?.method === 'cancel') {
serialized = this.serializeForCancel(snapshot);
}
return serialized;
}
/**
* Returns a payload containing only version
* @param {Snapshot} snapshot
* @return {object}
*/
serializeForCancel(snapshot) {
return {
version: snapshot.attr('version'),
};
}
}
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/models/session';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/host';
+1
View File
@@ -0,0 +1 @@
export { default } from 'api/serializers/session';
@@ -9,4 +9,17 @@ module('Unit | Model | account', function (hooks) {
let model = store.createRecord('account', {});
assert.ok(model);
});
test('it has an username attribute for password account', function (assert) {
assert.expect(2);
const store = this.owner.lookup('service:store');
let model = store.createRecord('account', {
name: 'name',
type: 'password',
});
assert.notOk(model.username);
model.attributes.login_name = 'username';
assert.equal(model.username, 'username');
});
});
@@ -0,0 +1,13 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | session', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('session', {});
assert.ok(model);
});
});
@@ -30,7 +30,7 @@ module('Unit | Serializer | application', function (hooks) {
});
});
test('it serializes scope_id when scope is set', function (assert) {
test('it serializes scope_id', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const record = store.createRecord('user', {
@@ -49,6 +49,27 @@ module('Unit | Serializer | application', function (hooks) {
});
});
test('it does not serialize scope_id when serializeScopeID is false', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('user');
const record = store.createRecord('user', {
name: 'User',
description: 'Description',
scope: {
scope_id: 'global',
type: 'global',
},
});
const snapshot = record._createSnapshot();
serializer.serializeScopeID = false;
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
name: 'User',
description: 'Description'
});
});
test('it serializes non-nullish version fields', function (assert) {
assert.expect(2);
const store = this.owner.lookup('service:store');
@@ -0,0 +1,23 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Serializer | host', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor('host');
assert.ok(serializer);
});
test('it serializes records', function(assert) {
let store = this.owner.lookup('service:store');
let record = store.createRecord('host', {});
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
});
@@ -0,0 +1,31 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Serializer | session', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor('session');
assert.ok(serializer);
});
test('it serializes only version when `adapterOptions.method` is `cancel`', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('session');
const record = store.createRecord('session', {
version: 1,
});
const snapshot = record._createSnapshot();
snapshot.adapterOptions = {
method: 'cancel',
};
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
version: 1,
});
});
});
@@ -1,7 +0,0 @@
import Component from '@ember/component';
import layout from '../../../templates/components/rose/card/link';
export default Component.extend({
layout,
tagName: '',
});
@@ -1,7 +0,0 @@
import Component from '@ember/component';
import layout from '../../templates/components/rose/cards';
export default Component.extend({
layout,
tagName: '',
});
@@ -54,3 +54,7 @@ p {
a {
color: var(--action);
}
time {
font-variant-numeric: tabular-nums;
}
@@ -34,4 +34,8 @@
padding: sizing.rems(l) sizing.rems(s);
}
}
[class*="rose-button"] {
margin-bottom: 0;
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ var mergeTrees = require('broccoli-merge-trees');
module.exports = {
name: require('./package').name,
included(app) {
this._super.included(app);
this._super.included.apply(this, arguments);
this.includeStyles(app);
this.includeIcons(app);
+1 -1
View File
@@ -61,7 +61,7 @@
"ember-qunit": "^4.6.0",
"ember-radio-button": "^2.0.1",
"ember-resolver": "^7.0.0",
"ember-source": "~3.12.0",
"ember-source": "~3.16.0",
"ember-source-channel-url": "^2.0.1",
"ember-try": "^1.4.0",
"ember-uuid": "^2.1.0",
+9
View File
@@ -7,6 +7,15 @@ export default {
},
},
date: {
default: {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: false
},
hhmmss: {
hour: 'numeric',
minute: 'numeric',
@@ -0,0 +1,7 @@
import { helper } from '@ember/component/helper';
export default helper(function formatDateIsoHuman(params/*, hash*/) {
return params[0].toISOString()
.replace('T', ' ')
.replace(/\.\d*Z/, '');
});
+5
View File
@@ -0,0 +1,5 @@
import { helper } from '@ember/component/helper';
export default helper(function formatDateIso(params/*, hash*/) {
return params[0].toISOString();
});
+1
View File
@@ -52,6 +52,7 @@ Router.map(function () {
});
this.route('new');
});
this.route('sessions', function() {});
});
this.route('new');
});
@@ -0,0 +1,85 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { all, hash } from 'rsvp';
import { task, timeout } from 'ember-concurrency';
const POLL_TIMEOUT_SECONDS = 2.5;
export default class ScopesScopeProjectsProjectSessionsRoute extends Route {
// =services
@service intl;
@service notify;
// =attributes
/**
* A simple Ember Concurrency-based polling task that refreshes the route
* every POLL_TIMEOUT_SECONDS seconds. This is necessary to display changes
* to session `status` that may occur.
*
* NOTE: tasks are sort of attributes and sort of methods, but they are not
* language-level constructs. Thus we annotate this task as if it
* is an attribute.
* @type {Task}
*/
@task(function * () {
while(true) {
yield timeout(POLL_TIMEOUT_SECONDS * 1000);
yield this.refresh();
}
}).drop() poller;
// =methods
/**
* Loads all sessions under the current scope and encapsulates them into
* an array of objects containing their associated users and targets.
* @return {Promise{[{session: SessionModel, user: UserModel, target: TargetModel}]}}
*/
async model() {
const { id: scope_id } = this.modelFor('scopes.scope.projects.project');
const sessions = await this.store.query('session', { scope_id });
return all(
sessions.map(session => hash({
session,
user: this.store.findRecord('user', session.user_id),
target: this.store.findRecord('target', session.target_id),
}))
);
}
/**
* When this route is activated (entered), begin polling for changes.
*/
activate() {
this.poller.perform();
}
/**
* When this route is deactivated (exited), stop polling for changes.
*/
deactivate() {
this.poller.cancelAll();
}
// =actions
/**
* Cancels the specified session and notifies user of success or error.
* @param {SessionModel}
*/
@action
async cancelSession(session) {
try {
await session.cancelSession();
this.notify.success(this.intl.t('notify.session-canceled'));
} catch (error) {
// TODO: replace with translated strings
this.notify.error(error.message, { closeAfter: null });
}
}
}
@@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
export default class ScopesScopeProjectsProjectSessionsIndexRoute extends Route {
}
@@ -10,6 +10,22 @@ export default class ScopesScopeRolesRoleGrantsRoute extends Route {
// =methods
/**
* Renders the grants-specific header template.
* Empties the actions and navigation outlets and renders a custom empty header.
* @override
*/
renderTemplate() {
super.renderTemplate(...arguments);
this.render('scopes/scope/roles/role/grants/-header', {
into: 'scopes/scope/roles/role',
outlet: 'header',
});
}
// =actions
/**
* Adds a new grant to the role at the beginning of the grants list.
* Grant creation is not immediately permanent; users may rollback the change
@@ -39,6 +39,20 @@ export default class ScopesScopeRolesRolePrincipalsRoute extends Route {
return all(users.concat(groups));
}
/**
* Renders the principals-specific header template.
* Empties the actions and navigation outlets and renders a custom empty header.
* @override
*/
renderTemplate() {
super.renderTemplate(...arguments);
this.render('scopes/scope/roles/role/principals/-header', {
into: 'scopes/scope/roles/role',
outlet: 'header',
});
}
// =actions
/**
@@ -57,4 +71,5 @@ export default class ScopesScopeRolesRolePrincipalsRoute extends Route {
this.notify.error(error.message, { closeAfter: null });
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
<layout.header>
<Rose::Header as |header|>
<header.brand @logo="logo-boundary" @text={{app-name}} />
<header.brand @logo="logo-hashi" @text={{app-name}} />
{{#if this.session.isAuthenticated}}
<header.utilities as |utilities|>
@@ -1 +1,2 @@
<h2>{{t "resources.auth-method"}}</h2>
<p>{{t "resources.descriptions.auth-method"}}</p>
@@ -4,6 +4,7 @@
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.label.name'}}</row.headerCell>
<row.headerCell>{{t 'form.label.identification'}}</row.headerCell>
<row.headerCell>{{t 'form.label.id'}}</row.headerCell>
</header.row>
</table.header>
@@ -19,6 +20,9 @@
</LinkTo>
<p>{{account.description}}</p>
</row.headerCell>
<row.cell>
{{account.username}}
</row.cell>
<row.cell>
<code>{{account.id}}</code>
</row.cell>
@@ -1,6 +1,7 @@
<Rose::Layout::Page as |page|>
<page.header>
<h2>{{t "resources.auth-methods"}}</h2>
<p>{{t "resources.descriptions.auth-methods"}}</p>
</page.header>
<page.breadcrumbs>
@@ -1 +1,2 @@
<h2>{{t "resources.group"}}</h2>
<p>{{t "resources.descriptions.group"}}</p>
@@ -7,7 +7,7 @@
<header.row as |row|>
<row.headerCell>{{t 'form.label.name'}}</row.headerCell>
<row.headerCell>{{t 'form.label.id'}}</row.headerCell>
<row.headerCell>{{t "form.label.actions"}}</row.headerCell>
<row.headerCell>{{t "titles.actions"}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
@@ -6,6 +6,7 @@
<page.header>
<h2>{{t "resources.groups"}}</h2>
<p>{{t "resources.descriptions.groups"}}</p>
</page.header>
<page.actions>
@@ -6,6 +6,7 @@
<page.header>
<h2>{{t "resources.orgs"}}</h2>
<p>{{t "resources.descriptions.orgs"}}</p>
</page.header>
<page.actions>
@@ -6,6 +6,7 @@
<page.header>
<h2>{{t "resources.projects"}}</h2>
<p>{{t "resources.descriptions.projects"}}</p>
</page.header>
<page.actions>
@@ -1,4 +1,7 @@
<Rose::Nav::Sidebar @title={{t "titles.resources"}} as |nav|>
<nav.link @route="scopes.scope.projects.project.sessions">
{{t "resources.sessions"}}
</nav.link>
<nav.link @route="scopes.scope.projects.project.targets">
{{t "resources.targets"}}
</nav.link>
@@ -1 +1,2 @@
<h2>{{t "resources.host-catalog"}}</h2>
<p>{{t "resources.descriptions.host-catalog"}}</p>
@@ -1 +1,2 @@
<h2>{{t "resources.host-set"}}</h2>
<h2>{{t "resources.host-set"}}</h2>
<p>{{t "resources.descriptions.host-set"}}</p>
@@ -6,7 +6,7 @@
<row.headerCell>{{t "form.label.name"}}</row.headerCell>
<row.headerCell>{{t "form.label.type"}}</row.headerCell>
<row.headerCell>{{t "form.label.id"}}</row.headerCell>
<row.headerCell>{{t "form.label.actions"}}</row.headerCell>
<row.headerCell>{{t "titles.actions"}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
@@ -1 +1,2 @@
<h2>{{t "resources.host"}}</h2>
<h2>{{t "resources.host"}}</h2>
<p>{{t "resources.descriptions.host"}}</p>
@@ -6,7 +6,7 @@
<page.header>
<h2>{{t "resources.host-catalogs"}}</h2>
<p>{{t "descriptions.host-catalogs"}}</p>
<p>{{t "resources.descriptions.host-catalogs"}}</p>
</page.header>
<page.actions>
@@ -6,6 +6,7 @@
<page.header>
<h2>{{t "resources.project"}}</h2>
<p>{{t "resources.descriptions.project"}}</p>
</page.header>
<page.navigation>
@@ -0,0 +1,3 @@
{{title (t "resources.sessions")}}
{{outlet}}
@@ -0,0 +1,66 @@
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<BreadCrumbs />
</page.breadcrumbs>
<page.header>
<h2>{{t "resources.sessions"}}</h2>
</page.header>
<page.body>
{{#if @model}}
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t "form.label.id"}}</row.headerCell>
<row.headerCell>{{t "resources.user"}}</row.headerCell>
<row.headerCell>{{t "resources.target"}}</row.headerCell>
<row.headerCell>{{t "form.label.started"}}</row.headerCell>
<row.headerCell>{{t "form.label.status"}}</row.headerCell>
<row.headerCell>{{t "titles.actions"}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @model as |sessionAggregate|}}
<body.row as |row|>
<row.headerCell>
<code>{{sessionAggregate.session.id}}</code>
</row.headerCell>
<row.cell>{{sessionAggregate.user.displayName}}</row.cell>
<row.cell>{{sessionAggregate.target.displayName}}</row.cell>
<row.cell>
<time datetime={{format-date-iso sessionAggregate.session.created_time}}>
{{format-date-iso-human sessionAggregate.session.created_time}}
</time>
</row.cell>
<row.cell>
<Rose::Badge>{{sessionAggregate.session.status}}</Rose::Badge>
</row.cell>
<row.cell>
<Rose::Button @style="secondary" {{on "click" (route-action "cancelSession" sessionAggregate.session)}}>
{{t "actions.cancel"}}
</Rose::Button>
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
{{else}}
<Rose::Layout::Centered>
<Rose::Message @title={{t "titles.no-sessions"}} as |message|>
<message.description>
{{t "descriptions.no-sessions"}}
</message.description>
</Rose::Message>
</Rose::Layout::Centered>
{{/if}}
</page.body>
</Rose::Layout::Page>
@@ -6,6 +6,7 @@
<page.header>
<h2>{{t "resources.targets"}}</h2>
<p>{{t "resources.descriptions.targets"}}</p>
</page.header>
<page.actions>
@@ -1 +1,2 @@
<h2>{{t "resources.target"}}</h2>
<p>{{t "resources.descriptions.target"}}</p>
@@ -8,7 +8,7 @@
<row.headerCell>{{t "form.label.name"}}</row.headerCell>
<row.headerCell>{{t "form.label.type"}}</row.headerCell>
<row.headerCell>{{t "form.label.id"}}</row.headerCell>
<row.headerCell>{{t "form.label.actions"}}</row.headerCell>
<row.headerCell>{{t "titles.actions"}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
@@ -6,6 +6,7 @@
<page.header>
<h2>{{t "resources.roles"}}</h2>
<p>{{t "resources.descriptions.roles"}}</p>
</page.header>
<page.actions>
@@ -1 +1,2 @@
<h2>{{t "resources.role"}}</h2>
<p>{{t "resources.descriptions.role"}}</p>
@@ -1 +1 @@
<h2>{{t "actions.add-principals"}}</h2>
<h2>{{t "actions.add-principals"}}</h2>
@@ -0,0 +1,2 @@
<h2>{{t "resources.role"}}</h2>
<p>{{t "resources.descriptions.grants"}}</p>
@@ -8,7 +8,7 @@
<row.headerCell>{{t 'form.label.name'}}</row.headerCell>
<row.headerCell>{{t 'form.label.type'}}</row.headerCell>
<row.headerCell>{{t 'form.label.id'}}</row.headerCell>
<row.headerCell>{{t "form.label.actions"}}</row.headerCell>
<row.headerCell>{{t "titles.actions"}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
@@ -0,0 +1,2 @@
<h2>{{t "resources.role"}}</h2>
<p>{{t "resources.descriptions.principals"}}</p>
@@ -6,6 +6,7 @@
<page.header>
<h2>{{t "resources.users"}}</h2>
<p>{{t "resources.descriptions.users"}}</p>
</page.header>
<page.actions>
@@ -6,6 +6,7 @@
<page.header>
<h2>{{t "resources.user"}}</h2>
<p>{{t "resources.descriptions.user"}}</p>
</page.header>
<page.actions>
+34 -2
View File
@@ -1,7 +1,7 @@
import config from '../config/environment';
import { authHandler, deauthHandler } from './route-handlers/auth';
// Uncomment to use in simulating error responses
// import { Response } from 'miragejs';
import { pickRandomStatusString } from './factories/session';
//import { Response } from 'miragejs';
export default function() {
@@ -294,6 +294,38 @@ export default function() {
return target.update(updatedAttrs);
});
// session
// To simulate changes to `session.status` that may occur in the backend,
// we quietly randomize the value of the field on GET.
this.get('/sessions', function ({ sessions }, { queryParams: { scope_id } }) {
sessions.where(session => session.scopeId === scope_id)
.models
.forEach(session => session.update({
status: pickRandomStatusString()
}));
return sessions.where(session => session.scopeId === scope_id)
});
this.get('/sessions/:id', function ({ sessions }, { params: { id } }) {
const session = sessions.find(id);
return session.update({
status: pickRandomStatusString()
});
});
this.post('/sessions/:idMethod', function ({ sessions }, { params: { idMethod } }) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const session = sessions.find(id);
const updatedAttrs = {
version: attrs.version
};
if (method === 'cancel') {
updatedAttrs.status = 'canceling';
}
return session.update(updatedAttrs);
});
/* Uncomment the following line and the Response import above
* Then change the response code to simulate error responses.
* this.get('/scopes', () => new Response(505));
+46
View File
@@ -0,0 +1,46 @@
import factory from '../generated/factories/target';
import { trait } from 'ember-cli-mirage';
import { random } from 'faker';
const statusStrings = [
'pending',
'active',
'canceling',
'terminated'
];
export const pickRandomStatusString = () => random.arrayElement(statusStrings);
export default factory.extend({
id: (i) => `session-id-${i}`,
status: pickRandomStatusString,
/**
* Sessions generated with this trait will automatically form
* associations to user, target, host set, and host (if available) within
* the current scope.
*/
withAssociations: trait({
afterCreate(record, server) {
const scopeId = record.scopeId;
if (record.scope.type === 'project') {
const orgScopeId = record.scope?.scope.id;
const user =
server.schema.users.where({ scopeId: orgScopeId }).models[0];
if (user) record.update({ user });
}
const target = server.schema.targets.where({ scopeId }).models[0];
if (target) {
record.update({ target });
const hostSet = target.hostSets.models[0];
if (hostSet) {
record.update({ hostSet });
const host = hostSet.hosts.models[0];
if (host) record.update({ host });
}
}
}
})
});
@@ -0,0 +1,14 @@
import { Factory } from 'ember-cli-mirage';
import { random, date } from 'faker';
/**
* GeneratedSessionModelFactory
*/
export default Factory.extend({
type: 'tcp',
created_time: () => date.recent(),
updated_time: () => date.recent(),
version: () => random.number(),
});
+3 -2
View File
@@ -1,7 +1,8 @@
import { Model, belongsTo } from 'ember-cli-mirage';
import { Model, belongsTo, hasMany } from 'ember-cli-mirage';
export default Model.extend({
scope: belongsTo()
scope: belongsTo(),
hostSets: hasMany()
});
+11
View File
@@ -0,0 +1,11 @@
import { Model, belongsTo } from 'ember-cli-mirage';
export default Model.extend({
scope: belongsTo(),
user: belongsTo(),
target: belongsTo(),
host: belongsTo(),
hostSet: belongsTo(),
});
+1
View File
@@ -26,6 +26,7 @@ export default function(server) {
server.schema.scopes.where({type: 'project'}).models.forEach(scope => {
server.createList('host-catalog', 2, { scope }, 'withChildren');
server.createList('target', 2, { scope }, 'withRandomHostSets');
server.createList('session', 4, { scope }, 'withAssociations');
});
}
+22
View File
@@ -0,0 +1,22 @@
import ApplicationSerializer from './application';
import { underscore } from '@ember/string';
export default ApplicationSerializer.extend({
modelName: 'session',
keyForRelationshipId(relationshipName) {
return `${this._container.inflector.singularize(
underscore(relationshipName)
)}_id`;
},
_hashForModel(model) {
const json = ApplicationSerializer.prototype._hashForModel.apply(this, arguments);
json.user_id = model.userId;
json.target_id = model.targetId;
json.host_set_id = model.hostSetId;
json.host_id = model.hostId;
return json;
}
});
+1
View File
@@ -54,6 +54,7 @@
"ember-cli-sri": "^2.1.1",
"ember-cli-template-lint": "^1.0.0-beta.3",
"ember-cli-uglify": "^3.0.0",
"ember-concurrency": "^1.3.0",
"ember-data": "~3.16.0",
"ember-data-model-fragments": "5.0.0-beta.0",
"ember-exam": "^5.0.1",
-69
View File
@@ -1,69 +0,0 @@
<svg
height="30"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 310 67">
<defs><style>.cls-1{fill:#fff;}.cls-2{fill:#f04e54;}</style></defs>
<g id="LOGOS">
<path
class="cls-1"
d="M104.58,32.17c3.08-1.33,4.48-3.21,4.48-6.54V21.45c0-5.44-3.09-8.59-10.72-8.59H82.5v40.3H99.44c7.32,0,11-3.57,11-9.26V39.61C110.45,35.67,108.27,33,104.58,32.17Zm-14.46-13h7.32c2.78,0,4,1,4,3.27V26c0,2.36-1.08,3.76-4.05,3.76H90.12Zm12.71,23.78c0,3-1.46,3.94-4.91,3.94h-7.8v-11h8.47c3,0,4.24,1.33,4.24,4Z">
</path>
<path
class="cls-1"
d="M126.48,23.09c-10.1,0-12.82,5.56-12.82,11.62v7.44c0,6.05,2.72,11.61,12.82,11.61s12.83-5.56,12.83-11.61V34.71C139.31,28.65,136.58,23.09,126.48,23.09Zm5.45,19.3c0,3.33-1.51,5.08-5.45,5.08S121,45.72,121,42.39V34.47c0-3.34,1.51-5.09,5.44-5.09s5.45,1.75,5.45,5.09Z">
</path>
<path
class="cls-1"
d="M161.46,23.69V44.15c-2.67,1.39-5.88,2.48-7.69,2.48s-2.36-.79-2.36-2.37V23.69H144V45.05c0,5.27,1.75,8.71,6.65,8.71a29.59,29.59,0,0,0,11.8-3.08l.73,2.48h5.63V23.69Z">
</path>
<path
class="cls-1"
d="M193.34,23.09a29.59,29.59,0,0,0-11.8,3.08l-.72-2.48h-5.63V53.16h7.38V32.71c2.66-1.39,5.87-2.48,7.68-2.48s2.36.79,2.36,2.36V53.16H200V31.8C200,26.54,198.24,23.09,193.34,23.09Z">
</path>
<path
class="cls-1"
d="M222.44,11.66V23.94a41,41,0,0,0-7.87-.85c-6.83,0-9.73,3.87-9.73,10.41v10c0,6.72,3.14,10.22,9.07,10.22a15.48,15.48,0,0,0,9.32-3.08l.72,2.48h5.87V10.63Zm0,33.15a10.43,10.43,0,0,1-6.59,2.66c-2.73,0-3.63-1.33-3.63-3.75V33.32c0-2.67,1-3.94,3.69-3.94a33,33,0,0,1,6.53.79Z">
</path>
<path
class="cls-1"
d="M245.74,23.09a38.76,38.76,0,0,0-10.11,1.39l.91,5.63a40.74,40.74,0,0,1,8.17-.91c4.71,0,5.62,1.15,5.62,4.42V36.7h-7.07c-6,0-8.6,2.3-8.6,8.29,0,5.09,2.3,8.77,7.69,8.77a16.06,16.06,0,0,0,8.77-2.6l.54,2h6.06V33.74C257.72,26.35,255,23.09,245.74,23.09Zm4.59,23.29a11.57,11.57,0,0,1-5.56,1.51c-2.3,0-3-.9-3-3,0-2.24.67-2.9,3.09-2.9h5.44Z">
</path>
<path
class="cls-1"
d="M277.92,23.09A33,33,0,0,0,270.18,27l-.48-3.27h-6.24V53.16h7.39v-19a56,56,0,0,1,7.8-4.29Z">
</path>
<path
class="cls-1"
d="M300.61,23.69l-6.77,22.39-6.72-22.39h-7.56l9.32,29.47h2.59l-3.68,11.92h7l3.93-11.92,9.44-29.47Z">
</path>
<polygon
class="cls-2"
points="9.89 64.1 9.89 59.28 19.9 59.28 19.9 56.3 15.05 56.3 15.05 51.45 37.97 51.45 27.32 33.01 37.97 14.57 14.47 14.57 14.47 40.15 1.82 40.15 1.82 1.92 45.34 1.92 52.61 14.5 41.92 33.01 52.83 51.9 45.78 64.1 9.89 64.1">
</polygon>
<rect
class="cls-2"
x="6.98"
y="51.45"
width="4.85"
height="4.85">
</rect>
<rect
class="cls-2"
x="1.82"
y="59.25"
width="4.85"
height="4.85">
</rect>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

+42 -16
View File
@@ -246,23 +246,49 @@ module('Acceptance | accounts', function (hooks) {
);
});
test('can set a new password for account', async function (assert) {
assert.expect(1);
this.server.post('/accounts/:id', (_, { requestBody }) => {
const attrs = JSON.parse(requestBody);
assert.equal(attrs.password, 'update password', "attributes don't match the expected ones");
module('password', function() {
test('can set a new password for account', async function (assert) {
assert.expect(1);
this.server.post('/accounts/:id', (_, { requestBody }) => {
const attrs = JSON.parse(requestBody);
assert.equal(attrs.password, 'update password', 'new password is set');
});
await visit(urls.setAccountPassword);
await fillIn('[name="password"]', 'update password');
await click('form [type="submit"]:not(:disabled)');
await a11yAudit();
});
await visit(urls.setAccountPassword);
await fillIn('[name="password"]', 'update password');
await click('form [type="submit"]:not(:disabled)');
await a11yAudit();
});
test('can cancel setting new password', async function (assert) {
assert.expect(1);
await visit(urls.setAccountPassword);
await fillIn('[name="password"]', 'update password');
await click('form button:not([type="submit"])');
assert.notOk(find('[name="password"]').textContent.trim());
test('can cancel setting new password', async function (assert) {
assert.expect(1);
await visit(urls.setAccountPassword);
await fillIn('[name="password"]', 'update password');
await click('form button:not([type="submit"])');
assert.notOk(find('[name="password"]').textContent.trim());
});
test('errors are displayed when setting password fails', async function (assert) {
assert.expect(1);
this.server.post('/accounts/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.setAccountPassword);
await fillIn('[name="password"]', 'update password');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
'Displays primary error message.'
);
});
});
});
+216 -17
View File
@@ -1,69 +1,268 @@
import { module, test } from 'qunit';
//import { visit, currentURL } from '@ember/test-helpers';
import { visit, currentURL, click, find, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
//import a11yAudit from 'ember-a11y-testing/test-support/audit';
// import {
// authenticateSession,
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import {
authenticateSession,
// // These are left here intentionally for future reference.
// //currentSession,
// //invalidateSession,
// } from 'ember-simple-auth/test-support';
} from 'ember-simple-auth/test-support';
module('Acceptance | auth methods', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
const instances = {
scopes: {
global: null,
org: null,
},
authMethod: null,
};
const urls = {
orgScope: null,
authMethods: null,
newAuthMethod: null,
authMethod: null,
};
hooks.beforeEach(function () {
// Setup Mirage mock resources for this test
authenticateSession({});
instances.scopes.global = this.server.create('scope', { id: 'global' });
instances.scopes.org = this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
instances.authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
});
// Generate route URLs for resources
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.authMethods = `${urls.orgScope}/auth-methods`;
urls.newAuthMethod = `${urls.authMethods}/new`;
urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`;
});
test('visiting auth methods', async function (assert) {
assert.expect(0);
assert.expect(1);
await visit(urls.authMethods);
await a11yAudit();
assert.equal(currentURL(), urls.authMethods);
});
test('can navigate to an auth method form', async function (assert) {
assert.expect(0);
assert.expect(1);
await visit(urls.authMethods);
await click('main tbody .rose-table-header-cell:nth-child(1) a');
await a11yAudit();
assert.equal(currentURL(), urls.authMethod);
});
test('can update an auth method and save changes', async function (assert) {
assert.expect(0);
assert.expect(1);
await visit(urls.authMethod);
await fillIn('[name="name"]', 'update name');
await click('form [type="submit"]:not(:disabled)');
assert.equal(this.server.db.authMethods[0].name, 'update name');
});
test('can update an auth method and cancel changes', async function (assert) {
assert.expect(0);
assert.expect(1);
await visit(urls.authMethod)
await fillIn('[name="name"]', 'update name');
await click('form button:not([type="submit"])');
assert.notEqual(this.server.db.authMethods[0].name, 'update name');
});
test('can create new auth method', async function (assert) {
assert.expect(0);
assert.expect(1);
const authMethodsCount = this.server.db.authMethods.length;
await visit(urls.newAuthMethod);
await fillIn('[name="name"]', 'AuthMethod name');
await fillIn('[name="description"]', 'description');
await click('form [type="submit"]:not(:disabled)');
assert.equal(this.server.db.authMethods.length, authMethodsCount + 1);
});
test('can cancel new auth method creation', async function (assert) {
assert.expect(0);
assert.expect(2);
const authMethodsCount = this.server.db.authMethods.length;
await visit(urls.newAuthMethod);
await fillIn('[name="name"]', 'AuthMethod name');
await fillIn('[name="description"]', 'description');
await click('form button:not([type="submit"])');
assert.equal(this.server.db.authMethods.length, authMethodsCount);
assert.equal(currentURL(), urls.authMethods);
});
test('can delete an auth method', async function (assert) {
assert.expect(0);
assert.expect(1);
const authMethodsCount = this.server.db.authMethods.length;
await visit(urls.authMethod);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(this.server.db.authMethods.length, authMethodsCount - 1);
});
test('saving a new auth method with invalid fields displays error messages', async function (assert) {
assert.expect(0);
assert.expect(2);
this.server.post('/auth-methods', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {
request_fields: [
{
name: 'name',
description: 'Name is required.',
},
],
},
}
);
});
await visit(urls.newAuthMethod);
await fillIn('[name="name"]', 'new auth method');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'The request was invalid.',
'Displays primary error message.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
'Name is required.',
'Displays field-level errors.'
);
});
test('errors are displayed when save on auth method fails', async function (assert) {
assert.expect(0);
assert.expect(1);
this.server.patch('/auth-methods/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.authMethod);
await fillIn('[name="name"]', 'save auth method');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
'Displays primary error message.'
);
});
test('errors are displayed when delete on an auth method fails', async function (assert) {
assert.expect(0);
assert.expect(1);
this.server.del('/auth-methods/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.authMethod);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
'Displays primary error message.'
);
});
test('saving a new auth method with invalid fields displays error messages', async function (assert) {
assert.expect(0);
assert.expect(2);
this.server.patch('/auth-methods/:id', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {
request_fields: [
{
name: 'name',
description: 'Name is required.',
},
],
},
}
);
});
await visit(urls.authMethod);
await fillIn('[name="name"]', 'existing auth method');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'The request was invalid.',
'Displays primary error message.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
'Name is required.',
'Displays field-level errors.'
);
});
test('saving an existing auth method with invalid fields displays error messages', async function (assert) {
assert.expect(0);
assert.expect(2);
this.server.patch('/auth-methods/:id', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {
request_fields: [
{
name: 'name',
description: 'Name is required.',
},
],
},
}
);
});
await visit(urls.authMethod);
await fillIn('[name="name"]', 'existing auth method');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'The request was invalid.',
'Displays primary error message.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
'Name is required.',
'Displays field-level errors.'
);
});
});
+90 -39
View File
@@ -1,5 +1,5 @@
import { module, test } from 'qunit';
import { visit, currentURL, click, find, fillIn } from '@ember/test-helpers';
import { visit, currentURL, click, find, findAll, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
@@ -47,11 +47,13 @@ module('Acceptance | targets', function (hooks) {
type: 'project',
scope: { id: instances.scopes.org.id, type: 'org' },
});
instances.hostSets = this.server.createList('host-set', 4, {
scope: instances.scopes.project,
});
instances.hostCatalog = this.server.create('host-catalog', {
scope: instances.scopes.project
}, 'withChildren');
instances.target = this.server.create('target', {
scope: instances.scopes.project,
hostSets: instances.hostCatalog.hostSets
});
// Generate route URLs for resources
@@ -161,16 +163,8 @@ module('Acceptance | targets', function (hooks) {
await fillIn('[name="name"]', 'new target');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'The request was invalid.',
'Displays primary error message.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
'Name is required.',
'Displays field-level errors.'
);
assert.ok(find('[role="alert"]'));
assert.ok(find('.rose-form-error-message'));
});
test('errors are displayed when save on target fails', async function (assert) {
@@ -190,11 +184,7 @@ module('Acceptance | targets', function (hooks) {
await fillIn('[name="name"]', 'save target');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
'Displays primary error message.'
);
assert.ok(find('[role="alert"]'));
});
test('errors are displayed when delete on a target fails', async function (assert) {
@@ -213,11 +203,7 @@ module('Acceptance | targets', function (hooks) {
await visit(urls.target);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'Oops.',
'Displays primary error message.'
);
assert.ok(find('[role="alert"]'));
});
test('saving an existing target with invalid fields displays error messages', async function (assert) {
@@ -245,35 +231,100 @@ module('Acceptance | targets', function (hooks) {
await fillIn('[name="name"]', 'existing target');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(
find('[role="alert"]').textContent.trim(),
'The request was invalid.',
'Displays primary error message.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
'Name is required.',
'Displays field-level errors.'
);
assert.ok(find('[role="alert"]'));
assert.ok(find('.rose-form-error-message'));
});
test('visiting target host sets', async function (assert) {
assert.expect(1);
assert.expect(3);
const targetHostSetCount = instances.target.hostSets.length;
await visit(urls.targetHostSets);
await a11yAudit();
assert.equal(currentURL(), urls.targetHostSets);
assert.ok(targetHostSetCount);
assert.equal(findAll('tbody tr').length, targetHostSetCount);
});
test('can delete a host sets', async function (assert) {
assert.expect(0);
assert.expect(2);
const targetHostSetCount = instances.target.hostSets.length;
await visit(urls.targetHostSets);
assert.equal(findAll('tbody tr').length, targetHostSetCount);
await click('tbody tr .rose-dropdown-button-danger');
assert.equal(findAll('tbody tr').length, targetHostSetCount - 1);
});
test('shows error message on host set remove', async function (assert) {
assert.expect(2);
this.server.post('/targets/:idMethod', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {},
}
);
});
const targetHostSetCount = instances.target.hostSets.length;
await visit(urls.targetHostSets);
assert.equal(findAll('tbody tr').length, targetHostSetCount);
await click('tbody tr .rose-dropdown-button-danger');
assert.ok(find('[role="alert"]'));
});
test('select and save host sets to add', async function (assert) {
assert.expect(0);
await visit(urls.targetAddHostSets);
assert.expect(3);
instances.target.update({ hostSetIds: [] });
await visit(urls.targetHostSets);
assert.equal(findAll('tbody tr').length, 0);
await click('.rose-layout-page-actions a')
assert.equal(currentURL(), urls.targetAddHostSets);
await click('tbody label');
await click('form [type="submit"]');
await visit(urls.targetHostSets);
assert.equal(findAll('tbody tr').length, 1);
});
test('select and cancel host sets to add', async function (assert) {
assert.expect(0);
assert.expect(4);
const targetHostSetCount = instances.target.hostSets.length;
await visit(urls.targetHostSets);
assert.equal(findAll('tbody tr').length, targetHostSetCount);
// first, remove a target host set (otherwise none would be available to add)
await click('tbody tr .rose-dropdown-button-danger');
assert.equal(findAll('tbody tr').length, targetHostSetCount - 1);
await click('.rose-layout-page-actions a')
assert.equal(currentURL(), urls.targetAddHostSets);
await click('tbody label');
await click('form [type="button"]');
await visit(urls.targetHostSets);
assert.equal(findAll('tbody tr').length, targetHostSetCount - 1);
});
test('shows error message on host set add', async function (assert) {
assert.expect(4);
this.server.post('/targets/:idMethod', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {},
}
);
});
instances.target.update({ hostSetIds: [] });
await visit(urls.targetAddHostSets);
assert.equal(instances.target.hostSets.length, 0);
assert.equal(currentURL(), urls.targetAddHostSets);
await click('tbody label');
await click('form [type="submit"]');
assert.equal(currentURL(), urls.targetAddHostSets);
assert.equal(instances.target.hostSets.length, 0);
});
});
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Helper | format-date-iso-human', function(hooks) {
setupRenderingTest(hooks);
// Replace this with your real tests.
test('it renders', async function(assert) {
this.set('inputValue', new Date('2020-01-01T00:00:00.999Z'));
await render(hbs`{{format-date-iso-human inputValue}}`);
assert.equal(this.element.textContent.trim(), '2020-01-01 00:00:00');
});
});
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Helper | format-date-iso', function(hooks) {
setupRenderingTest(hooks);
// Replace this with your real tests.
test('it renders', async function(assert) {
this.set('inputValue', new Date('2020-01-01T00:00:00.999Z'));
await render(hbs`{{format-date-iso inputValue}}`);
assert.equal(this.element.textContent.trim(), '2020-01-01T00:00:00.999Z');
});
});
@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | scopes/scope/projects/project/sessions', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:scopes/scope/projects/project/sessions');
assert.ok(route);
});
});
@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | scopes/scope/projects/project/sessions/index', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:scopes/scope/projects/project/sessions/index');
assert.ok(route);
});
});
+35
View File
@@ -77,6 +77,35 @@ resources:
targets: Targets
accounts: Accounts
account: Account
sessions: Sessions
session: Session
descriptions:
user: A User inherits from Principal ("is-a" Principal) allowing it to be associated with zero or more Roles.
users: A user is any entity authorized to access Boundary using authentication credentials specific to one of the configured Authentication Methods. A User is owned by one and only one Organization. A User is associated with zero or one Authentication Methods. A User not associated with an Authentication Method can not log in to the system. A User can belong to zero or more Groups. A User is deleted when the Organization it belongs to is deleted. The lifecycle of a User is not tied to the lifecycle of any Authentication Method, Group or Role, but rather to its Organization only.
role: A Role can be assigned to zero or more Principals. A Principal assigned a Role receives all capabilities granted by any Direct Grant owned by the Role. A Role is deleted when the Scope it belongs to is deleted. All Direct Grants owned by a Role are deleted when the Role is deleted.
roles: A role is a collection of capabilities granted to any Principal the Role is assigned to. A Role belongs to one and only one Scope. A Role owns zero or more Direct Grants. The lifecycle of a Role is not tied to the lifecycle of any Principal.
grants: A grant represents a set of capabilities granted to a role. It couples a set of Actions to either a set of Resource Types or an individual Resource. A Direct Grant belongs exclusively to one and only one Role. However, equivalent Direct Grants may exist across different Roles. A Direct Grant references one or more Actions and either one or more Resource types or one specific Resource. A Direct Grant is deleted when the Role it belongs to is deleted. A Direct Grant is also deleted if it is associated with a specific Resource and that Resource itself is deleted. The lifecycle of a Direct Grant is not tied to the lifecycle of any Action or Resource Type.
principals: A principal is any entity which can be assigned capabilities. Principal is abstract. User, Group, and Project are concrete Principals. A Principal can be assigned to zero or more Roles.
group: All Users in a Group are granted the capabilities of all Roles the Group is associated with. A Group is deleted when the Scope it belongs to is deleted.
groups: Groups are collections of Users used only for access control purposes. A Group is owned by one and only one Scope. A Group can contain zero or more Users. A Group inherits from Principal ("is-a" Principal) allowing it to be associated with zero or more Roles. The lifecycle of a Group is not tied to the lifecycle of any User or Role.
org: When an Organization is deleted, all resources owned by it are also deleted.
orgs: An Organization is a top-level container, and owns zero to many Projects and zero to many Authentication Methods. An Organization inherits from Scope ("is-a" Scope) allowing it to own zero to many Groups, Roles, Targets, or Host Catalogs. The lifecycle of an Organization is not tied to anything else. A Role at the Organization level can grant permissions at the Project level.
project: When a Project is deleted, all resources owned by it are also deleted. The lifecycle of a Project is not tied to any resource it contains.
projects: A project is a child scope of an organization and is owned by an Organization. A Project inherits from Scope ("is-a" Scope) allowing it to own zero to many Groups, Roles, Targets, or Host Catalogs. A Project also inherits from Principal ("is-a" Principal) allowing it to be associated with zero or more Roles. A Project has no additional relationships beyond those defined in Scope and Principal. A Project is deleted when the Organization it belongs to is deleted.
host-catalog: A host catalog is a permission boundary modeled as a container.
host-catalogs: A host catalog is a permission boundary modeled as a container.
auth-method: An Authentication Method creates and references zero or more Users. An Authentication Method is deleted when the Organization it belongs to is deleted. The lifecycle of an Authentication Method is not tied to the lifecycle of any User.
auth-methods: An authentication method is a method configured for an Organization to authenticate Users. An Authentication Method is owned by one and only one Organization.
host-set: Host Set is abstract. A Host Set is deleted when the Host Catalog it belongs to is deleted.
host-sets: A host set is a resource which provides a subset of Hosts from the set of Hosts of the Host Catalog it belongs to. A Host Set belongs to one and only one Host Catalog. A Host Set can contain zero or more Hosts. A Host Set can be contained by zero or more Targets. The lifecycle of a Host Set is not tied to the lifecycle of any Targets or Hosts.
host: Host is abstract. A Host is deleted when the Host Catalog it belongs to is deleted.
hosts: A host is a computing element with a network address reachable from Boundary. A Host belongs to one and only one Host Catalog. A Host can be contained by zero or more Host Sets. The lifecycle of a Host is not tied to the lifecycle of any Host Sets.
target: Target is abstract. A Target is deleted when the Scope it belongs to is deleted.
targets: A target is a networked service a User can connect to and interact with through Boundary. A Target does not directly contain any secrets. A Target contains a collection of Hosts. A Target belongs to one and only one Scope. A Target can contain zero or more Host Sets. The lifecycle of a Target is not tied to the lifecycle of any Host Set.
accounts: Accounts
account: Account
sessions: Sessions
session: Session
titles:
iam: IAM
new: New
@@ -114,6 +143,8 @@ titles:
accounts: Accounts
account: Account
accounts-welcome: Welcome to Accounts
no-sessions: No Active Sessions
actions: Actions
descriptions:
host-catalogs: Overview of collections of hosts and host sets.
empty-set: There are no items to display yet. You may be able to add items or try back later.
@@ -136,6 +167,7 @@ descriptions:
no-available-users: No users are available to add to this group.
account-set-password: Set a password for the account.
accounts-welcome: No accounts are available in this auth method.
no-sessions: There are no active sessions at this time.
states:
info: Info
success: Success
@@ -157,6 +189,8 @@ form:
default_port: Default Port
current_password: Current Password
new_password: New Password
started: Started
status: Status
errors:
401:
title: You are not signed in
@@ -180,3 +214,4 @@ notify:
create-success: Created succesfully.
save-success: Saved succesfully.
delete-success: Deleted successfully.
session-canceled: Session canceled.
+12 -50
View File
@@ -4295,11 +4295,6 @@ ast-types@0.13.3:
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.3.tgz#50da3f28d17bdbc7969a3a2d83a0e4a72ae755a7"
integrity sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==
ast-types@0.9.6:
version "0.9.6"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
integrity sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=
astral-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
@@ -8434,7 +8429,7 @@ ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.11.0, ember-cli-babel@^6.12.0,
ember-cli-version-checker "^2.1.2"
semver "^5.5.0"
ember-cli-babel@^7.1.2, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.4.1, ember-cli-babel@^7.5.0, ember-cli-babel@^7.7.0, ember-cli-babel@^7.7.3:
ember-cli-babel@^7.1.2, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.4.1, ember-cli-babel@^7.5.0, ember-cli-babel@^7.7.3:
version "7.20.0"
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.20.0.tgz#aa77eadd15036a053534fc68f7933bb97c5a1e25"
integrity sha512-DwV5Cbn8wlRFpG8zQXxYPRb0NRnnaj/2qVDODeUEjM/doDhoDDbTSSaEI5Pf0kIlkVIimyGB8qpGv1fkgj2IfQ==
@@ -8923,6 +8918,15 @@ ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-co
ember-cli-version-checker "^2.1.1"
semver "^5.4.1"
ember-concurrency@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ember-concurrency/-/ember-concurrency-1.3.0.tgz#66f90fb792687470bcee1172adc0ebf33f5e8b9c"
integrity sha512-DwGlfWFpYyAkTwsedlEtK4t1DznJSculAW6Vq5S1C0shVPc5b6tTpHB2FFYisannSYkm+wpm1f1Pd40qiNPtOQ==
dependencies:
ember-cli-babel "^7.7.3"
ember-compatibility-helpers "^1.2.0"
ember-maybe-import-regenerator "^0.1.6"
ember-cookies@^0.5.0, ember-cookies@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/ember-cookies/-/ember-cookies-0.5.2.tgz#06e33463f2f83254fefaf224cc944dec3fb9d3ba"
@@ -9280,13 +9284,6 @@ ember-route-action-helper@^2.0.8:
ember-cli-babel "^6.8.1"
ember-getowner-polyfill "^2.0.0"
ember-router-generator@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-1.2.3.tgz#8ed2ca86ff323363120fc14278191e9e8f1315ee"
integrity sha1-jtLKhv8yM2MSD8FCeBkeno8TFe4=
dependencies:
recast "^0.11.3"
ember-router-generator@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-2.0.0.tgz#d04abfed4ba8b42d166477bbce47fccc672dbde0"
@@ -9324,26 +9321,6 @@ ember-source-channel-url@^2.0.1:
dependencies:
got "^8.0.1"
ember-source@~3.12.0:
version "3.12.3"
resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.12.3.tgz#112462b8c1dfa124c0d021ca7eeeb04b5a5488ca"
integrity sha512-uzyoOVg1ygTY/InjwP0sgIkHiMiRylDauMY9K+vM15WkmmfrMaetklxxnqWThvsL4onBRRW8MKCj+q41Ra/NQQ==
dependencies:
broccoli-funnel "^2.0.2"
broccoli-merge-trees "^3.0.2"
chalk "^2.4.2"
ember-cli-babel "^7.7.0"
ember-cli-get-component-path-option "^1.0.0"
ember-cli-is-package-missing "^1.0.0"
ember-cli-normalize-entity-name "^1.0.0"
ember-cli-path-utils "^1.0.0"
ember-cli-string-utils "^1.1.0"
ember-cli-version-checker "^3.1.3"
ember-router-generator "^1.2.3"
inflection "^1.12.0"
jquery "^3.4.1"
resolve "^1.11.1"
ember-source@~3.16.0:
version "3.16.8"
resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.16.8.tgz#83f1a55a2b9835b272d7ec5b121a7af490c86725"
@@ -9930,11 +9907,6 @@ esprima@~3.0.0:
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.0.0.tgz#53cf247acda77313e551c3aa2e73342d3fb4f7d9"
integrity sha1-U88kes2ncxPlUcOqLnM0LT+099k=
esprima@~3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
esquery@^1.0.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57"
@@ -15667,7 +15639,7 @@ prismjs@~1.17.0:
optionalDependencies:
clipboard "^2.0.0"
private@^0.1.6, private@^0.1.8, private@~0.1.5:
private@^0.1.6, private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
@@ -16488,16 +16460,6 @@ realpath-native@^1.1.0:
dependencies:
util.promisify "^1.0.0"
recast@^0.11.3:
version "0.11.23"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3"
integrity sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=
dependencies:
ast-types "0.9.6"
esprima "~3.1.0"
private "~0.1.5"
source-map "~0.5.0"
recast@^0.17.3:
version "0.17.6"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.17.6.tgz#64ae98d0d2dfb10ff92ff5fb9ffb7371823b69fa"
@@ -17786,7 +17748,7 @@ source-map@0.4.x, source-map@^0.4.2:
dependencies:
amdefine ">=0.0.4"
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1:
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=