From 905ee8785898eef16075fa0232b1b9e59b52174c Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Thu, 17 Sep 2020 14:39:15 -0400 Subject: [PATCH 01/18] feat: Display password account's loginname in list --- addons/api/addon/models/account.js | 19 ++++++++++++++++++- addons/api/addon/serializers/account.js | 7 +++++-- addons/api/tests/unit/models/account-test.js | 12 ++++++++++++ .../auth-method/accounts/index.hbs | 4 ++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/addons/api/addon/models/account.js b/addons/api/addon/models/account.js index b28558732..4a6ee08c2 100644 --- a/addons/api/addon/models/account.js +++ b/addons/api/addon/models/account.js @@ -1,13 +1,26 @@ 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 login_name attribute. + * @type {string} + */ + @computed('attributes.login_name') + get username() { + return this.attributes.login_name; + } + // =methods /** @@ -41,7 +54,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, diff --git a/addons/api/addon/serializers/account.js b/addons/api/addon/serializers/account.js index 416bc488e..01618a081 100644 --- a/addons/api/addon/serializers/account.js +++ b/addons/api/addon/serializers/account.js @@ -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; } diff --git a/addons/api/tests/unit/models/account-test.js b/addons/api/tests/unit/models/account-test.js index 554a72be5..b8ce36b45 100644 --- a/addons/api/tests/unit/models/account-test.js +++ b/addons/api/tests/unit/models/account-test.js @@ -9,4 +9,16 @@ module('Unit | Model | account', function (hooks) { let model = store.createRecord('account', {}); assert.ok(model); }); + + test('it has an username attribute', function (assert) { + assert.expect(2); + const store = this.owner.lookup('service:store'); + let model = store.createRecord('account', { + name: 'name', + type: 'password', + }); + assert.equal(model.username, null); + model.attributes.login_name = 'username'; + assert.equal(model.username, 'username'); + }); }); diff --git a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs index 8586ac7c0..83ed4fa1a 100644 --- a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs +++ b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs @@ -4,6 +4,7 @@ {{t 'form.label.name'}} + {{t 'form.label.identification'}} {{t 'form.label.id'}} @@ -19,6 +20,9 @@

{{account.description}}

+ + {{account.username}} + {{account.id}} From 9e482d643484e5a14bdd599543e1b1a19406303e Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 18 Sep 2020 10:16:40 -0400 Subject: [PATCH 02/18] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20initial=20s?= =?UTF-8?q?ession=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/generated/models/session.js | 30 ++++++++++++++++++++ addons/api/addon/models/session.js | 3 ++ addons/api/app/models/session.js | 1 + addons/api/tests/unit/models/session-test.js | 13 +++++++++ 4 files changed, 47 insertions(+) create mode 100644 addons/api/addon/generated/models/session.js create mode 100644 addons/api/addon/models/session.js create mode 100644 addons/api/app/models/session.js create mode 100644 addons/api/tests/unit/models/session-test.js diff --git a/addons/api/addon/generated/models/session.js b/addons/api/addon/generated/models/session.js new file mode 100644 index 000000000..c4dd740c9 --- /dev/null +++ b/addons/api/addon/generated/models/session.js @@ -0,0 +1,30 @@ +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.', + }) + 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; + +} diff --git a/addons/api/addon/models/session.js b/addons/api/addon/models/session.js new file mode 100644 index 000000000..a624fb6b0 --- /dev/null +++ b/addons/api/addon/models/session.js @@ -0,0 +1,3 @@ +import Model from '@ember-data/model'; + +export default class SessionModel extends Model { } diff --git a/addons/api/app/models/session.js b/addons/api/app/models/session.js new file mode 100644 index 000000000..3106229d6 --- /dev/null +++ b/addons/api/app/models/session.js @@ -0,0 +1 @@ +export { default } from 'api/models/session'; diff --git a/addons/api/tests/unit/models/session-test.js b/addons/api/tests/unit/models/session-test.js new file mode 100644 index 000000000..2d373a2f6 --- /dev/null +++ b/addons/api/tests/unit/models/session-test.js @@ -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); + }); +}); From 7a17ff3bfa3aa685d9ce4d97d180ce7156784663 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 18 Sep 2020 10:59:07 -0400 Subject: [PATCH 03/18] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20support=20fo?= =?UTF-8?q?r=20custom=20cancel=20session=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/generated/models/session.js | 5 +++ addons/api/addon/models/session.js | 28 +++++++++++++++-- addons/api/addon/serializers/session.js | 31 +++++++++++++++++++ addons/api/app/serializers/session.js | 1 + .../tests/unit/serializers/session-test.js | 31 +++++++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 addons/api/addon/serializers/session.js create mode 100644 addons/api/app/serializers/session.js create mode 100644 addons/api/tests/unit/serializers/session-test.js diff --git a/addons/api/addon/generated/models/session.js b/addons/api/addon/generated/models/session.js index c4dd740c9..ee751a988 100644 --- a/addons/api/addon/generated/models/session.js +++ b/addons/api/addon/generated/models/session.js @@ -27,4 +27,9 @@ export default class GeneratedSessionModel extends BaseModel { }) updated_time; + @attr('number', { + description: 'Current version number of this resource.', + }) + version; + } diff --git a/addons/api/addon/models/session.js b/addons/api/addon/models/session.js index a624fb6b0..b37f08fc2 100644 --- a/addons/api/addon/models/session.js +++ b/addons/api/addon/models/session.js @@ -1,3 +1,27 @@ -import Model from '@ember-data/model'; +import GeneratedSessionModel from '../generated/models/session'; -export default class SessionModel extends Model { } +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, + }, + }); + } + +} diff --git a/addons/api/addon/serializers/session.js b/addons/api/addon/serializers/session.js new file mode 100644 index 000000000..d8d5ea7f8 --- /dev/null +++ b/addons/api/addon/serializers/session.js @@ -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'), + }; + } +} diff --git a/addons/api/app/serializers/session.js b/addons/api/app/serializers/session.js new file mode 100644 index 000000000..407fe5c23 --- /dev/null +++ b/addons/api/app/serializers/session.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/session'; diff --git a/addons/api/tests/unit/serializers/session-test.js b/addons/api/tests/unit/serializers/session-test.js new file mode 100644 index 000000000..fde7c215f --- /dev/null +++ b/addons/api/tests/unit/serializers/session-test.js @@ -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, + }); + }); +}); From 4229658815795ed6b840a4e5c75159a598593be4 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 18 Sep 2020 12:54:31 -0400 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20sessions=20l?= =?UTF-8?q?ist=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rose/addon/styles/rose/_typography.scss | 4 ++ ui/core/app/formats.js | 9 +++ ui/core/app/helpers/format-date-iso-human.js | 7 +++ ui/core/app/helpers/format-date-iso.js | 5 ++ ui/core/app/router.js | 1 + .../scopes/scope/projects/project/sessions.js | 25 +++++++++ .../scope/projects/project/sessions/index.js | 4 ++ .../scope/projects/project/_sidebar.hbs | 3 + .../scope/projects/project/sessions.hbs | 3 + .../scope/projects/project/sessions/index.hbs | 56 +++++++++++++++++++ ui/core/mirage/config.js | 7 +++ ui/core/mirage/factories/session.js | 35 ++++++++++++ ui/core/mirage/generated/factories/session.js | 14 +++++ ui/core/mirage/models/session.js | 11 ++++ ui/core/mirage/scenarios/default.js | 1 + ui/core/mirage/serializers/session.js | 22 ++++++++ .../helpers/format-date-iso-human-test.js | 17 ++++++ .../helpers/format-date-iso-test.js | 17 ++++++ .../scope/projects/project/sessions-test.js | 11 ++++ .../projects/project/sessions/index-test.js | 11 ++++ ui/core/translations/en-us.yaml | 5 ++ 21 files changed, 268 insertions(+) create mode 100644 ui/core/app/helpers/format-date-iso-human.js create mode 100644 ui/core/app/helpers/format-date-iso.js create mode 100644 ui/core/app/routes/scopes/scope/projects/project/sessions.js create mode 100644 ui/core/app/routes/scopes/scope/projects/project/sessions/index.js create mode 100644 ui/core/app/templates/scopes/scope/projects/project/sessions.hbs create mode 100644 ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs create mode 100644 ui/core/mirage/factories/session.js create mode 100644 ui/core/mirage/generated/factories/session.js create mode 100644 ui/core/mirage/models/session.js create mode 100644 ui/core/mirage/serializers/session.js create mode 100644 ui/core/tests/integration/helpers/format-date-iso-human-test.js create mode 100644 ui/core/tests/integration/helpers/format-date-iso-test.js create mode 100644 ui/core/tests/unit/routes/scopes/scope/projects/project/sessions-test.js create mode 100644 ui/core/tests/unit/routes/scopes/scope/projects/project/sessions/index-test.js diff --git a/addons/rose/addon/styles/rose/_typography.scss b/addons/rose/addon/styles/rose/_typography.scss index b44914ce8..c5fda86e4 100644 --- a/addons/rose/addon/styles/rose/_typography.scss +++ b/addons/rose/addon/styles/rose/_typography.scss @@ -54,3 +54,7 @@ p { a { color: var(--action); } + +time { + font-variant-numeric: tabular-nums; +} diff --git a/ui/core/app/formats.js b/ui/core/app/formats.js index f5d1f1502..9ea07ca26 100644 --- a/ui/core/app/formats.js +++ b/ui/core/app/formats.js @@ -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', diff --git a/ui/core/app/helpers/format-date-iso-human.js b/ui/core/app/helpers/format-date-iso-human.js new file mode 100644 index 000000000..2744a4eeb --- /dev/null +++ b/ui/core/app/helpers/format-date-iso-human.js @@ -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/, ''); +}); diff --git a/ui/core/app/helpers/format-date-iso.js b/ui/core/app/helpers/format-date-iso.js new file mode 100644 index 000000000..52cd9687d --- /dev/null +++ b/ui/core/app/helpers/format-date-iso.js @@ -0,0 +1,5 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function formatDateIso(params/*, hash*/) { + return params[0].toISOString(); +}); diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 204ced768..0894289f9 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -52,6 +52,7 @@ Router.map(function () { }); this.route('new'); }); + this.route('sessions', function() {}); }); this.route('new'); }); diff --git a/ui/core/app/routes/scopes/scope/projects/project/sessions.js b/ui/core/app/routes/scopes/scope/projects/project/sessions.js new file mode 100644 index 000000000..ed7c699f0 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/projects/project/sessions.js @@ -0,0 +1,25 @@ +import Route from '@ember/routing/route'; +import { all, hash } from 'rsvp'; + +export default class ScopesScopeProjectsProjectSessionsRoute extends Route { + + // =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), + })) + ); + } + +} diff --git a/ui/core/app/routes/scopes/scope/projects/project/sessions/index.js b/ui/core/app/routes/scopes/scope/projects/project/sessions/index.js new file mode 100644 index 000000000..a8ae56692 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/projects/project/sessions/index.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeProjectsProjectSessionsIndexRoute extends Route { +} diff --git a/ui/core/app/templates/scopes/scope/projects/project/_sidebar.hbs b/ui/core/app/templates/scopes/scope/projects/project/_sidebar.hbs index ae4f79e0b..e29af0451 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/_sidebar.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/_sidebar.hbs @@ -1,4 +1,7 @@ + + {{t "resources.sessions"}} + {{t "resources.targets"}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/sessions.hbs b/ui/core/app/templates/scopes/scope/projects/project/sessions.hbs new file mode 100644 index 000000000..37fea125e --- /dev/null +++ b/ui/core/app/templates/scopes/scope/projects/project/sessions.hbs @@ -0,0 +1,3 @@ +{{title (t "resources.sessions")}} + +{{outlet}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs new file mode 100644 index 000000000..abbbbdfac --- /dev/null +++ b/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs @@ -0,0 +1,56 @@ + + + + + + + +

{{t "resources.sessions"}}

+
+ + + + {{#if @model}} + + + + + {{t "form.label.id"}} + {{t "resources.user"}} + {{t "resources.target"}} + {{t "form.label.started"}} + + + + {{#each @model as |sessionAggregate|}} + + + {{sessionAggregate.session.id}} + + {{sessionAggregate.user.displayName}} + {{sessionAggregate.target.displayName}} + + + + + {{/each}} + + + + {{else}} + + + + + {{t "descriptions.no-sessions"}} + + + + + {{/if}} + + + +
diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index e7044298f..844211c61 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -292,4 +292,11 @@ export default function() { } return target.update(updatedAttrs); }); + + // session + + this.get('/sessions', function ({ sessions }, { queryParams: { scope_id } }) { + return sessions.where(session => session.scopeId === scope_id); + }); + this.get('/sessions/:id'); } diff --git a/ui/core/mirage/factories/session.js b/ui/core/mirage/factories/session.js new file mode 100644 index 000000000..9e5e31913 --- /dev/null +++ b/ui/core/mirage/factories/session.js @@ -0,0 +1,35 @@ +import factory from '../generated/factories/target'; +import { trait } from 'ember-cli-mirage'; + +export default factory.extend({ + + id: (i) => `session-id-${i}`, + + /** + * 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 }); + } + } + } + }) + +}); diff --git a/ui/core/mirage/generated/factories/session.js b/ui/core/mirage/generated/factories/session.js new file mode 100644 index 000000000..25aca3759 --- /dev/null +++ b/ui/core/mirage/generated/factories/session.js @@ -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(), + +}); diff --git a/ui/core/mirage/models/session.js b/ui/core/mirage/models/session.js new file mode 100644 index 000000000..91a4da594 --- /dev/null +++ b/ui/core/mirage/models/session.js @@ -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(), + +}); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index 9492f92e3..5d1a85fe6 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -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'); }); } diff --git a/ui/core/mirage/serializers/session.js b/ui/core/mirage/serializers/session.js new file mode 100644 index 000000000..3e14a23e6 --- /dev/null +++ b/ui/core/mirage/serializers/session.js @@ -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; + } + +}); diff --git a/ui/core/tests/integration/helpers/format-date-iso-human-test.js b/ui/core/tests/integration/helpers/format-date-iso-human-test.js new file mode 100644 index 000000000..94eec92fa --- /dev/null +++ b/ui/core/tests/integration/helpers/format-date-iso-human-test.js @@ -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'); + }); +}); diff --git a/ui/core/tests/integration/helpers/format-date-iso-test.js b/ui/core/tests/integration/helpers/format-date-iso-test.js new file mode 100644 index 000000000..2954d3890 --- /dev/null +++ b/ui/core/tests/integration/helpers/format-date-iso-test.js @@ -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'); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions-test.js b/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions-test.js new file mode 100644 index 000000000..70769438e --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions-test.js @@ -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); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions/index-test.js b/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions/index-test.js new file mode 100644 index 000000000..a774bf223 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/projects/project/sessions/index-test.js @@ -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); + }); +}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index e458c2b50..2d13fa33e 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -77,6 +77,8 @@ resources: targets: Targets accounts: Accounts account: Account + sessions: Sessions + session: Session titles: iam: IAM new: New @@ -114,6 +116,7 @@ titles: accounts: Accounts account: Account accounts-welcome: Welcome to Accounts + no-sessions: No Active Sessions 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 +139,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 +161,7 @@ form: default_port: Default Port current_password: Current Password new_password: New Password + started: Started errors: titles: unauthenticated-error: You are not signed in From 3ae2db95625d86f5926fc833f0240ff6559e528e Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Fri, 18 Sep 2020 14:15:19 -0400 Subject: [PATCH 05/18] fix: Add type check for account username --- addons/api/addon/models/account.js | 8 +++++--- addons/api/tests/unit/models/account-test.js | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/addons/api/addon/models/account.js b/addons/api/addon/models/account.js index 4a6ee08c2..4f880e07d 100644 --- a/addons/api/addon/models/account.js +++ b/addons/api/addon/models/account.js @@ -13,12 +13,14 @@ export default class AccountModel extends GeneratedAccountModel { @fragment('fragment-account-attributes', { defaultValue: {} }) attributes; /** - * Convenience for getting login_name attribute. + * Convenience for getting username in account. * @type {string} */ - @computed('attributes.login_name') + @computed('attributes') get username() { - return this.attributes.login_name; + if(this.type === 'password') { + return this.attributes.login_name; + } } // =methods diff --git a/addons/api/tests/unit/models/account-test.js b/addons/api/tests/unit/models/account-test.js index b8ce36b45..f6cbfbcc5 100644 --- a/addons/api/tests/unit/models/account-test.js +++ b/addons/api/tests/unit/models/account-test.js @@ -10,7 +10,7 @@ module('Unit | Model | account', function (hooks) { assert.ok(model); }); - test('it has an username attribute', function (assert) { + 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', { @@ -21,4 +21,5 @@ module('Unit | Model | account', function (hooks) { model.attributes.login_name = 'username'; assert.equal(model.username, 'username'); }); + }); From 1c16f8fe19e36a55f421d6e9f6f7d847fb5469dc Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Fri, 18 Sep 2020 14:33:10 -0400 Subject: [PATCH 06/18] fix: Lint errors for account username return type --- addons/api/addon/models/account.js | 10 +++++++--- addons/api/tests/unit/models/account-test.js | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/addons/api/addon/models/account.js b/addons/api/addon/models/account.js index 4f880e07d..de5209c34 100644 --- a/addons/api/addon/models/account.js +++ b/addons/api/addon/models/account.js @@ -16,11 +16,15 @@ export default class AccountModel extends GeneratedAccountModel { * Convenience for getting username in account. * @type {string} */ - @computed('attributes') + @computed('attributes.{login_name}') get username() { - if(this.type === 'password') { - return this.attributes.login_name; + let username = ''; + switch (this.type) { + case 'password': + username = this.attributes.login_name; + break; } + return username; } // =methods diff --git a/addons/api/tests/unit/models/account-test.js b/addons/api/tests/unit/models/account-test.js index f6cbfbcc5..f937b3c26 100644 --- a/addons/api/tests/unit/models/account-test.js +++ b/addons/api/tests/unit/models/account-test.js @@ -17,7 +17,7 @@ module('Unit | Model | account', function (hooks) { name: 'name', type: 'password', }); - assert.equal(model.username, null); + assert.notOk(model.username); model.attributes.login_name = 'username'; assert.equal(model.username, 'username'); }); From 2aa330fd3612b4a84b3e5ccc93a80fe6a848dc87 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 18 Sep 2020 15:38:32 -0400 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20cancel=20ses?= =?UTF-8?q?sion=20feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../styles/rose/components/table/_table.scss | 4 ++++ .../scopes/scope/projects/project/sessions.js | 22 +++++++++++++++++++ .../scopes/scope/groups/group/members.hbs | 2 +- .../host-catalog/host-sets/host-set/hosts.hbs | 2 +- .../scope/projects/project/sessions/index.hbs | 6 +++++ .../project/targets/target/host-sets.hbs | 2 +- .../scopes/scope/roles/role/principals.hbs | 2 +- ui/core/mirage/config.js | 14 ++++++++++++ ui/core/translations/en-us.yaml | 2 ++ 9 files changed, 52 insertions(+), 4 deletions(-) diff --git a/addons/rose/addon/styles/rose/components/table/_table.scss b/addons/rose/addon/styles/rose/components/table/_table.scss index ba63d37aa..956ab5faa 100644 --- a/addons/rose/addon/styles/rose/components/table/_table.scss +++ b/addons/rose/addon/styles/rose/components/table/_table.scss @@ -34,4 +34,8 @@ padding: sizing.rems(l) sizing.rems(s); } } + + [class*="rose-button"] { + margin-bottom: 0; + } } diff --git a/ui/core/app/routes/scopes/scope/projects/project/sessions.js b/ui/core/app/routes/scopes/scope/projects/project/sessions.js index ed7c699f0..48bbb5cc0 100644 --- a/ui/core/app/routes/scopes/scope/projects/project/sessions.js +++ b/ui/core/app/routes/scopes/scope/projects/project/sessions.js @@ -1,8 +1,15 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; import { all, hash } from 'rsvp'; export default class ScopesScopeProjectsProjectSessionsRoute extends Route { + // =services + + @service intl; + @service notify; + // =methods /** @@ -22,4 +29,19 @@ export default class ScopesScopeProjectsProjectSessionsRoute extends Route { ); } + /** + * 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 }); + } + } + } diff --git a/ui/core/app/templates/scopes/scope/groups/group/members.hbs b/ui/core/app/templates/scopes/scope/groups/group/members.hbs index fb562164b..24a6e13d9 100644 --- a/ui/core/app/templates/scopes/scope/groups/group/members.hbs +++ b/ui/core/app/templates/scopes/scope/groups/group/members.hbs @@ -7,7 +7,7 @@ {{t 'form.label.name'}} {{t 'form.label.id'}} - {{t "form.label.actions"}} + {{t "titles.actions"}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.hbs index 437140df4..b0ef4a625 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/host-sets/host-set/hosts.hbs @@ -6,7 +6,7 @@ {{t "form.label.name"}} {{t "form.label.type"}} {{t "form.label.id"}} - {{t "form.label.actions"}} + {{t "titles.actions"}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs index abbbbdfac..83e480e5e 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs @@ -19,6 +19,7 @@ {{t "resources.user"}} {{t "resources.target"}} {{t "form.label.started"}} + {{t "titles.actions"}} @@ -34,6 +35,11 @@ {{format-date-iso-human sessionAggregate.session.created_time}} + + + {{t "actions.cancel"}} + + {{/each}} diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs index 40f12df7f..583a8df55 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target/host-sets.hbs @@ -8,7 +8,7 @@ {{t "form.label.name"}} {{t "form.label.type"}} {{t "form.label.id"}} - {{t "form.label.actions"}} + {{t "titles.actions"}} diff --git a/ui/core/app/templates/scopes/scope/roles/role/principals.hbs b/ui/core/app/templates/scopes/scope/roles/role/principals.hbs index b364b136f..160c91c7b 100644 --- a/ui/core/app/templates/scopes/scope/roles/role/principals.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role/principals.hbs @@ -8,7 +8,7 @@ {{t 'form.label.name'}} {{t 'form.label.type'}} {{t 'form.label.id'}} - {{t "form.label.actions"}} + {{t "titles.actions"}} diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 844211c61..fa23ee0e4 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -299,4 +299,18 @@ export default function() { return sessions.where(session => session.scopeId === scope_id); }); this.get('/sessions/:id'); + 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 = 'canceled'; + } + return session.update(updatedAttrs); + }); + } diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index 2d13fa33e..7fb4748de 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -117,6 +117,7 @@ titles: 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. @@ -180,3 +181,4 @@ notify: create-success: Created succesfully. save-success: Saved succesfully. delete-success: Deleted successfully. + session-canceled: Session canceled. From 5fb1b1465887ed0de1f2acf90a483c7b6b348f6a Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 18 Sep 2020 15:59:42 -0400 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20show=20session=20s?= =?UTF-8?q?tatus=20in=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scopes/scope/projects/project/sessions/index.hbs | 4 ++++ ui/core/mirage/config.js | 2 +- ui/core/mirage/factories/session.js | 7 +++++++ ui/core/translations/en-us.yaml | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs index 83e480e5e..6fe82d12b 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/sessions/index.hbs @@ -19,6 +19,7 @@ {{t "resources.user"}} {{t "resources.target"}} {{t "form.label.started"}} + {{t "form.label.status"}} {{t "titles.actions"}} @@ -35,6 +36,9 @@ {{format-date-iso-human sessionAggregate.session.created_time}} + + {{sessionAggregate.session.status}} + {{t "actions.cancel"}} diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index fa23ee0e4..87bf05300 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -308,7 +308,7 @@ export default function() { version: attrs.version }; if (method === 'cancel') { - updatedAttrs.status = 'canceled'; + updatedAttrs.status = 'canceling'; } return session.update(updatedAttrs); }); diff --git a/ui/core/mirage/factories/session.js b/ui/core/mirage/factories/session.js index 9e5e31913..efe1bd63a 100644 --- a/ui/core/mirage/factories/session.js +++ b/ui/core/mirage/factories/session.js @@ -1,9 +1,16 @@ import factory from '../generated/factories/target'; import { trait } from 'ember-cli-mirage'; +import { random } from 'faker'; export default factory.extend({ id: (i) => `session-id-${i}`, + status: () => random.arrayElement([ + 'pending', + 'active', + 'canceling', + 'terminated' + ]), /** * Sessions generated with this trait will automatically form diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index 7fb4748de..a37c9bfe3 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -163,6 +163,7 @@ form: current_password: Current Password new_password: New Password started: Started + status: Status errors: titles: unauthenticated-error: You are not signed in From b8257fe6dc4e4cc05046fca714b4d3e5802d42eb Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 18 Sep 2020 16:03:52 -0400 Subject: [PATCH 09/18] chore: status is read-only --- addons/api/addon/generated/models/session.js | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/api/addon/generated/models/session.js b/addons/api/addon/generated/models/session.js index ee751a988..a33426e4c 100644 --- a/addons/api/addon/generated/models/session.js +++ b/addons/api/addon/generated/models/session.js @@ -12,6 +12,7 @@ export default class GeneratedSessionModel extends BaseModel { @attr('string', { description: 'The name of the current status of this session.', + readOnly: true, }) status; From 711818216c42ad2bfdb8d59f54c3a3e0a55e3919 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Sat, 19 Sep 2020 09:36:48 -0400 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20session=20po?= =?UTF-8?q?lling=20to=20display=20changes=20to=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scopes/scope/projects/project/sessions.js | 38 +++++++++++++++++++ ui/core/mirage/config.js | 17 ++++++++- ui/core/mirage/factories/session.js | 16 +++++--- ui/core/package.json | 1 + yarn.lock | 9 +++++ 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/ui/core/app/routes/scopes/scope/projects/project/sessions.js b/ui/core/app/routes/scopes/scope/projects/project/sessions.js index 48bbb5cc0..b41d76d7a 100644 --- a/ui/core/app/routes/scopes/scope/projects/project/sessions.js +++ b/ui/core/app/routes/scopes/scope/projects/project/sessions.js @@ -2,6 +2,9 @@ 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 { @@ -10,6 +13,25 @@ export default class ScopesScopeProjectsProjectSessionsRoute extends Route { @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 /** @@ -29,6 +51,22 @@ export default class ScopesScopeProjectsProjectSessionsRoute extends Route { ); } + /** + * 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} diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 87bf05300..6803aac3d 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -1,5 +1,6 @@ import config from '../config/environment'; import { authHandler, deauthHandler } from './route-handlers/auth'; +import { pickRandomStatusString } from './factories/session'; //import { Response } from 'miragejs'; export default function() { @@ -295,10 +296,22 @@ export default function() { // 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 } }) { - return sessions.where(session => session.scopeId === 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.get('/sessions/:id'); this.post('/sessions/:idMethod', function ({ sessions }, { params: { idMethod } }) { const attrs = this.normalizedRequestAttrs(); const id = idMethod.split(':')[0]; diff --git a/ui/core/mirage/factories/session.js b/ui/core/mirage/factories/session.js index efe1bd63a..0100a4b75 100644 --- a/ui/core/mirage/factories/session.js +++ b/ui/core/mirage/factories/session.js @@ -2,15 +2,19 @@ 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: () => random.arrayElement([ - 'pending', - 'active', - 'canceling', - 'terminated' - ]), + status: pickRandomStatusString, /** * Sessions generated with this trait will automatically form diff --git a/ui/core/package.json b/ui/core/package.json index 258f13a98..212ce3416 100644 --- a/ui/core/package.json +++ b/ui/core/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 5304e5ace..658b62c2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8923,6 +8923,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" From b377181d229ec30cbc77f12371f8d14134c6aac2 Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Sun, 20 Sep 2020 14:48:18 -0400 Subject: [PATCH 11/18] test: Add accounts acceptance test --- ui/core/tests/acceptance/accounts-test.js | 308 ++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 ui/core/tests/acceptance/accounts-test.js diff --git a/ui/core/tests/acceptance/accounts-test.js b/ui/core/tests/acceptance/accounts-test.js new file mode 100644 index 000000000..5c014eecb --- /dev/null +++ b/ui/core/tests/acceptance/accounts-test.js @@ -0,0 +1,308 @@ +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.accounts); + // Find and click account creation dropdown link + await click('.rose-layout-page-actions .rose-dropdown summary'); + await click('.rose-layout-page-actions .rose-dropdown .rose-dropdown-content .rose-dropdown-link'); + 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 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"]', 'save 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('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.' + ); + }); + + 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"); + }); + 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()); + }); +}); From a4d22739548fe57d18412fa8e471734ea70b9eda Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Sun, 20 Sep 2020 15:14:33 -0400 Subject: [PATCH 12/18] test: Remove duplicate save test --- ui/core/tests/acceptance/accounts-test.js | 42 +---------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/ui/core/tests/acceptance/accounts-test.js b/ui/core/tests/acceptance/accounts-test.js index 5c014eecb..9412f115a 100644 --- a/ui/core/tests/acceptance/accounts-test.js +++ b/ui/core/tests/acceptance/accounts-test.js @@ -98,10 +98,7 @@ module('Acceptance | accounts', function (hooks) { test('can create a new account', async function (assert) { assert.expect(1); const accountsCount = this.server.db.accounts.length; - await visit(urls.accounts); - // Find and click account creation dropdown link - await click('.rose-layout-page-actions .rose-dropdown summary'); - await click('.rose-layout-page-actions .rose-dropdown .rose-dropdown-content .rose-dropdown-link'); + await visit(urls.newAccount); await fillIn('[name="name"]', 'Account name'); await fillIn('[name="description"]', 'description'); await fillIn('[name="login_name"]', 'username'); @@ -212,43 +209,6 @@ module('Acceptance | accounts', function (hooks) { ); }); - 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"]', 'save 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('saving an existing account with invalid fields displays error messages', async function (assert) { assert.expect(2); this.server.patch('/accounts/:id', () => { From c961d658fd07d2a5eecdf70a64d22a45bb8a4b8e Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Sun, 20 Sep 2020 16:04:20 -0400 Subject: [PATCH 13/18] test: Init hostsets target tests --- ui/core/tests/acceptance/targets-test.js | 236 +++++++++++++++++++++-- 1 file changed, 219 insertions(+), 17 deletions(-) diff --git a/ui/core/tests/acceptance/targets-test.js b/ui/core/tests/acceptance/targets-test.js index d7858be8a..1f3ca2e13 100644 --- a/ui/core/tests/acceptance/targets-test.js +++ b/ui/core/tests/acceptance/targets-test.js @@ -1,66 +1,267 @@ 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 | 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.hostSets = this.server.createList('host-set', 4, { + scope: instances.scopes.project, + }); + instances.target = this.server.create('target', { + scope: instances.scopes.project, + }); + // 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"]').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 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"]').textContent.trim(), + 'Oops.', + 'Displays primary error message.' + ); }); 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"]').textContent.trim(), + 'Oops.', + 'Displays primary error message.' + ); }); 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"]').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('viewing target host sets', async function (assert) { - assert.expect(0); + test('visiting target host sets', async function (assert) { + assert.expect(1); + await visit(urls.targetHostSets); + await a11yAudit(); + assert.equal(currentURL(), urls.targetHostSets); }); test('can delete a host sets', async function (assert) { @@ -69,6 +270,7 @@ module('Acceptance | targets', function (hooks) { test('select and save host sets to add', async function (assert) { assert.expect(0); + await visit(urls.targetAddHostSets); }); test('select and cancel host sets to add', async function (assert) { From 1f52f02727e58aa246ce92044d450c6f9cd483c6 Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Mon, 21 Sep 2020 10:45:45 -0400 Subject: [PATCH 14/18] fix: Add type computed property check to account username --- addons/api/addon/models/account.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/api/addon/models/account.js b/addons/api/addon/models/account.js index de5209c34..71b337f44 100644 --- a/addons/api/addon/models/account.js +++ b/addons/api/addon/models/account.js @@ -16,7 +16,7 @@ export default class AccountModel extends GeneratedAccountModel { * Convenience for getting username in account. * @type {string} */ - @computed('attributes.{login_name}') + @computed('type', 'attributes.{login_name}') get username() { let username = ''; switch (this.type) { From 2a4616e426b0559f6224dcc80eba0561a79f5137 Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Mon, 21 Sep 2020 11:18:44 -0400 Subject: [PATCH 15/18] test: Add set password error testcase --- ui/core/tests/acceptance/accounts-test.js | 58 ++++++++++++++++------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/ui/core/tests/acceptance/accounts-test.js b/ui/core/tests/acceptance/accounts-test.js index 9412f115a..a7e6a544c 100644 --- a/ui/core/tests/acceptance/accounts-test.js +++ b/ui/core/tests/acceptance/accounts-test.js @@ -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.' + ); + }); }); }); From 99aeb0007a1dc64fbe62484f539aa18a74d06591 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Mon, 21 Sep 2020 11:57:33 -0400 Subject: [PATCH 16/18] =?UTF-8?q?test:=20=F0=9F=92=8D=20add=20target=20hos?= =?UTF-8?q?t=20set=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/core/mirage/models/host-catalog.js | 5 ++- ui/core/tests/acceptance/targets-test.js | 51 +++++++++++++++++++----- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/ui/core/mirage/models/host-catalog.js b/ui/core/mirage/models/host-catalog.js index 08b1f5ad3..8c2757e71 100644 --- a/ui/core/mirage/models/host-catalog.js +++ b/ui/core/mirage/models/host-catalog.js @@ -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() }); diff --git a/ui/core/tests/acceptance/targets-test.js b/ui/core/tests/acceptance/targets-test.js index 1f3ca2e13..883fb8ae7 100644 --- a/ui/core/tests/acceptance/targets-test.js +++ b/ui/core/tests/acceptance/targets-test.js @@ -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 @@ -258,22 +260,53 @@ module('Acceptance | targets', function (hooks) { }); 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('select and save host sets to add', async function (assert) { - assert.expect(0); - await visit(urls.targetAddHostSets); + 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="submit"]'); + await visit(urls.targetHostSets); + assert.equal(findAll('tbody tr').length, targetHostSetCount); }); 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); }); }); From f728fb7087cffc3aa857949b11dc0184f5d28f02 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Mon, 21 Sep 2020 13:28:03 -0400 Subject: [PATCH 17/18] =?UTF-8?q?test:=20=F0=9F=92=8D=20add=20error=20test?= =?UTF-8?q?=20cases=20for=20target=20host=20sets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/core/tests/acceptance/targets-test.js | 88 ++++++++++++++---------- 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/ui/core/tests/acceptance/targets-test.js b/ui/core/tests/acceptance/targets-test.js index 883fb8ae7..822986f12 100644 --- a/ui/core/tests/acceptance/targets-test.js +++ b/ui/core/tests/acceptance/targets-test.js @@ -163,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) { @@ -192,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) { @@ -215,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) { @@ -247,16 +231,8 @@ 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) { @@ -278,20 +254,38 @@ module('Acceptance | targets', function (hooks) { assert.equal(findAll('tbody tr').length, targetHostSetCount - 1); }); - test('select and save host sets to add', async function (assert) { - assert.expect(4); + 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); - // 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); + assert.ok(find('[role="alert"]')); + }); + + test('select and save host sets to add', async function (assert) { + 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, targetHostSetCount); + assert.equal(findAll('tbody tr').length, 1); }); test('select and cancel host sets to add', async function (assert) { @@ -309,4 +303,28 @@ module('Acceptance | targets', function (hooks) { 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); + }); }); From 20cc45d57abfd4260fd85a679200dcb7c6db8f47 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Mon, 21 Sep 2020 15:19:14 -0400 Subject: [PATCH 18/18] fix: use inline "fill" attributes instead of embedded CSS for logo colors --- ui/core/public/logo-boundary.svg | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ui/core/public/logo-boundary.svg b/ui/core/public/logo-boundary.svg index ea71154fb..21465bdef 100644 --- a/ui/core/public/logo-boundary.svg +++ b/ui/core/public/logo-boundary.svg @@ -2,56 +2,55 @@ height="30" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 310 67"> -