Merge branch 'develop' into ICU-606-auth-method-acceptance-test

This commit is contained in:
Randall Morey authored and GitHub committed 2020-09-21 16:33:13 -04:00
commit e50d82fce1
43 files changed
+1164 -43

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,
+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,
},
});
}
}
+5 -2
View File
@@ -22,8 +22,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;
}
+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/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);
});
});
@@ -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,
});
});
});
@@ -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;
}
}
+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 {
}
@@ -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>
@@ -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|>
@@ -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>
@@ -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|>
@@ -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>
@@ -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|>
@@ -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|>
+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",
+11 -12
View File
@@ -2,56 +2,55 @@
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"
fill="#ffffff"
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"
fill="#ffffff"
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"
fill="#ffffff"
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"
fill="#ffffff"
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"
fill="#ffffff"
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"
fill="#ffffff"
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"
fill="#ffffff"
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"
fill="#ffffff"
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"
fill="#f04e54"
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"
fill="#f04e54"
x="6.98"
y="51.45"
width="4.85"
@@ -59,7 +58,7 @@
</rect>
<rect
class="cls-2"
fill="#f04e54"
x="1.82"
y="59.25"
width="4.85"

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

+294
View File
@@ -0,0 +1,294 @@
import { module, test } from 'qunit';
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 { Response } from 'miragejs';
import {
authenticateSession,
// // These are left here intentionally for future reference.
// //currentSession,
// //invalidateSession,
} from 'ember-simple-auth/test-support';
module('Acceptance | accounts', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
const instances = {
scopes: {
global: null,
org: null,
},
authMethods: null,
account: null,
};
const urls = {
orgScope: null,
authMethods: null,
accounts: null,
newAccount: null,
account: null,
setAccountPassword: null,
};
hooks.beforeEach(function () {
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,
});
instances.account = this.server.create('account', {
scope: instances.scopes.org,
authMethod: instances.authMethod,
});
// Generate route URLs for resources
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.authMethods = `${urls.orgScope}/auth-methods`;
urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`;
urls.accounts = `${urls.authMethod}/accounts`;
urls.newAccount = `${urls.accounts}/new`;
urls.account = `${urls.accounts}/${instances.account.id}`;
urls.setAccountPassword = `${urls.accounts}/${instances.account.id}/set-password`;
});
hooks.afterEach(async function() {
const notification = find('.rose-notification');
if(notification) {
await click('.rose-notification-dismiss');
}
});
test('visiting accounts', async function (assert) {
assert.expect(1);
await visit(urls.accounts);
await a11yAudit();
assert.equal(currentURL(), urls.accounts);
});
test('can navigate to an account form', async function (assert) {
assert.expect(1);
await visit(urls.accounts);
await click('main tbody .rose-table-header-cell:nth-child(1) a');
await a11yAudit();
assert.equal(currentURL(), urls.account);
});
test('can update an account and save changes', async function (assert) {
assert.expect(1);
await visit(urls.account);
await fillIn('[name="name"]', 'update name');
await click('form [type="submit"]:not(:disabled)');
assert.equal(this.server.db.accounts[0].name, 'update name');
});
test('can update an account and cancel changes', async function (assert) {
assert.expect(1);
await visit(urls.account)
await fillIn('[name="name"]', 'update name');
await click('form button:not([type="submit"])');
assert.notEqual(this.server.db.accounts[0].name, 'update name');
});
test('can create a new account', async function (assert) {
assert.expect(1);
const accountsCount = this.server.db.accounts.length;
await visit(urls.newAccount);
await fillIn('[name="name"]', 'Account name');
await fillIn('[name="description"]', 'description');
await fillIn('[name="login_name"]', 'username');
await fillIn('[name="password"]', 'password');
await click('form [type="submit"]:not(:disabled)');
assert.equal(this.server.db.accounts.length, accountsCount + 1);
});
test('can cancel a new account creation', async function (assert) {
assert.expect(2);
const accountsCount = this.server.db.accounts.length;
await visit(urls.newAccount);
await fillIn('[name="name"]', 'Account name');
await click('form button:not([type="submit"])');
assert.equal(this.server.db.accounts.length, accountsCount);
assert.equal(currentURL(), urls.accounts);
});
test('can delete an account', async function (assert) {
assert.expect(1);
const accountsCount = this.server.db.accounts.length;
await visit(urls.account);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(this.server.db.accounts.length, accountsCount - 1);
});
test('saving a new account with invalid fields displays error messages', async function (assert) {
assert.expect(2);
this.server.post('/accounts', () => {
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.newAccount);
await fillIn('[name="name"]', 'new account');
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 account fails', async function (assert) {
assert.expect(1);
this.server.patch('/accounts/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.account);
await fillIn('[name="name"]', 'save account');
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 account fails', async function (assert) {
assert.expect(1);
this.server.del('/accounts/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.account);
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 an existing account with invalid fields displays error messages', async function (assert) {
assert.expect(2);
this.server.patch('/accounts/: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.account);
await fillIn('[name="name"]', 'existing account');
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.'
);
});
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();
});
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.'
);
});
});
});
+273 -20
View File
@@ -1,77 +1,330 @@
import { module, test } from 'qunit';
//import { visit, currentURL } 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';
// 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 | targets', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
const instances = {
scopes: {
global: null,
org: null,
project: null,
},
target: null,
hostSets: null,
};
const urls = {
orgScope: null,
projects: null,
project: null,
targets: null,
newTarget: null,
target: null,
targetHostSets: null,
targetAddHostSets: 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.scopes.project = this.server.create('scope', {
type: 'project',
scope: { id: instances.scopes.org.id, type: 'org' },
});
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
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.projects = `${urls.orgScope}/projects`;
urls.project = `${urls.projects}/${instances.scopes.project.id}`;
urls.targets = `${urls.project}/targets`;
urls.newTarget = `${urls.targets}/new`;
urls.target = `${urls.targets}/${instances.target.id}`;
urls.targetHostSets = `${urls.target}/host-sets`;
urls.targetAddHostSets = `${urls.target}/add-host-sets`;
});
hooks.afterEach(async function() {
const notification = find('.rose-notification');
if(notification) {
await click('.rose-notification-dismiss');
}
});
test('visiting targets', async function (assert) {
assert.expect(0);
assert.expect(1);
await visit(urls.targets);
await a11yAudit();
assert.equal(currentURL(), urls.targets);
});
test('can navigate to a target form', async function (assert) {
assert.expect(0);
assert.expect(1);
await visit(urls.targets);
await click('main tbody .rose-table-header-cell:nth-child(1) a');
await a11yAudit();
assert.equal(currentURL(), urls.target);
});
test('can update a target and save changes', async function (assert) {
assert.expect(0);
assert.expect(2);
await visit(urls.target);
await fillIn('[name="name"]', 'update name');
await fillIn('[name="default_port"]', '1234');
await click('form [type="submit"]:not(:disabled)');
assert.equal(this.server.db.targets[0].name, 'update name');
assert.equal(this.server.db.targets[0].attributes.default_port, '1234');
});
test('can update a target and cancel changes', async function (assert) {
assert.expect(0);
assert.expect(2);
await visit(urls.target);
await fillIn('[name="name"]', 'update name');
await fillIn('[name="default_port"]', '1234');
await click('form button:not([type="submit"])');
assert.notEqual(this.server.db.targets[0].name, 'update name');
assert.notEqual(this.server.db.targets[0].port, '1234');
});
test('can create new target', async function (assert) {
assert.expect(0);
assert.expect(1);
const targetsCount = this.server.db.targets.length;
await visit(urls.newTarget);
await fillIn('[name="name"]', 'Target name');
await fillIn('[name="description"]', 'description');
await fillIn('[name="default_port"]', '1234');
await click('form [type="submit"]:not(:disabled)');
assert.equal(this.server.db.targets.length, targetsCount + 1);
});
test('can cancel new target creation', async function (assert) {
assert.expect(0);
assert.expect(2);
const targetsCount = this.server.db.targets.length;
await visit(urls.newTarget);
await fillIn('[name="name"]', 'Target Name');
await click('form button:not([type="submit"])');
assert.equal(this.server.db.targets.length, targetsCount);
assert.equal(currentURL(), urls.targets);
});
test('can delete a target', async function(assert) {
assert.expect(1);
const targetsCount = this.server.db.targets.length;
await visit(urls.target);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(this.server.db.targets.length, targetsCount - 1);
});
test('saving a new target with invalid fields displays error messages', async function (assert) {
assert.expect(0);
assert.expect(2);
this.server.post('/targets', () => {
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.newTarget);
await fillIn('[name="name"]', 'new target');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(find('[role="alert"]'));
assert.ok(find('.rose-form-error-message'));
});
test('errors are displayed when save on target fails', async function (assert) {
assert.expect(0);
assert.expect(1);
this.server.patch('/targets/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.target);
await fillIn('[name="name"]', 'save target');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(find('[role="alert"]'));
});
test('errors are displayed when delete on a target fails', async function (assert) {
assert.expect(0);
assert.expect(1);
this.server.del('/targets/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.target);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
await a11yAudit();
assert.ok(find('[role="alert"]'));
});
test('saving an existing target with invalid fields displays error messages', async function (assert) {
assert.expect(0);
assert.expect(2);
this.server.patch('/targets/: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.target);
await fillIn('[name="name"]', 'existing target');
await click('form [type="submit"]');
await a11yAudit();
assert.ok(find('[role="alert"]'));
assert.ok(find('.rose-form-error-message'));
});
test('viewing target host sets', async function (assert) {
assert.expect(0);
test('visiting target host sets', async function (assert) {
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);
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);
});
});
+8
View File
@@ -77,6 +77,8 @@ resources:
targets: Targets
accounts: Accounts
account: Account
sessions: Sessions
session: Session
titles:
iam: IAM
new: New
@@ -114,6 +116,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 +140,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 +162,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 +187,4 @@ notify:
create-success: Created succesfully.
save-success: Saved succesfully.
delete-success: Deleted successfully.
session-canceled: Session canceled.
+9
View File
@@ -8918,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"