diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js
index ad976e8e7..2fd0d4ea1 100644
--- a/addons/api/mirage/config.js
+++ b/addons/api/mirage/config.js
@@ -413,9 +413,12 @@ export default function () {
);
this.get('/sessions/:id', function ({ sessions }, { params: { id } }) {
const session = sessions.find(id);
- return session.update({
- status: pickRandomStatusString(),
- });
+ if (!Ember.testing) {
+ session.update({
+ status: pickRandomStatusString(),
+ });
+ }
+ return session;
});
this.post(
'/sessions/:idMethod',
diff --git a/ui/desktop/package.json b/ui/desktop/package.json
index 7efe8ade6..22a0205cb 100644
--- a/ui/desktop/package.json
+++ b/ui/desktop/package.json
@@ -50,6 +50,7 @@
"clipboard": "^2.0.6",
"core": "*",
"doctoc": "^1.4.0",
+ "ember-a11y-testing": "^4.0.2",
"ember-auto-import": "^1.10.1",
"ember-autofocus-modifier": "^1.1.0",
"ember-cli": "~3.24.0",
diff --git a/ui/desktop/tests/acceptance/authentication-test.js b/ui/desktop/tests/acceptance/authentication-test.js
index a436b4b54..fa34de896 100644
--- a/ui/desktop/tests/acceptance/authentication-test.js
+++ b/ui/desktop/tests/acceptance/authentication-test.js
@@ -2,7 +2,7 @@ import { module, test } from 'qunit';
import {
visit,
currentURL,
- //fillIn,
+ fillIn,
click,
find,
//findAll,
@@ -12,8 +12,8 @@ import {
import { run, later } from '@ember/runloop';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
-//import { Response } from 'miragejs';
-//import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import { Response } from 'miragejs';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import {
currentSession,
authenticateSession,
@@ -37,8 +37,6 @@ module('Acceptance | authentication', function (hooks) {
global: null,
org: null,
},
- hostCatalog: null,
- target: null,
};
const stubs = {
@@ -60,6 +58,7 @@ module('Acceptance | authentication', function (hooks) {
},
},
targets: null,
+ sessions: null,
};
const setDefaultOrigin = (test) => {
@@ -93,21 +92,13 @@ module('Acceptance | authentication', function (hooks) {
scope: instances.scopes.org,
});
- instances.hostCatalog = this.server.create(
- 'host-catalog',
- { scope: instances.scopes.project },
- 'withChildren'
- );
- instances.target = this.server.create(
- 'target',
- { scope: instances.scopes.project },
- 'withRandomHostSets'
- );
-
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
urls.authenticate.global = `${urls.scopes.global}/authenticate`;
urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
+ urls.projects = `${urls.scopes.global}/projects`;
+ urls.targets = `${urls.projects}/targets`;
+ urls.sessions = `${urls.projects}/sessions`;
class MockIPC {
origin = null;
@@ -147,7 +138,7 @@ module('Acceptance | authentication', function (hooks) {
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
assert.expect(2);
await visit(urls.index);
- //await a11yAudit();
+ await a11yAudit();
assert.notOk(currentSession().isAuthenticated);
assert.equal(currentURL(), urls.authenticate.methods.global);
});
@@ -156,11 +147,89 @@ module('Acceptance | authentication', function (hooks) {
assert.expect(2);
instances.authMethods.global.destroy();
await visit(urls.authenticate.global);
- //await a11yAudit();
+ await a11yAudit();
assert.equal(currentURL(), urls.authenticate.global);
assert.ok(find('.rose-message'));
});
+ test('visiting authenticate route without origin redirects to origin index', async function (assert) {
+ assert.expect(1);
+ this.owner.lookup('service:origin').rendererOrigin = null;
+ await visit(urls.authenticate.global);
+ await a11yAudit();
+ assert.equal(currentURL(), urls.origin);
+ });
+
+ test('visiting authenticate route when the scope cannot be loaded is allowed', async function (assert) {
+ assert.expect(1);
+ this.server.get('/scopes', () => {
+ return new Response(404);
+ });
+ await visit(urls.authenticate.global);
+ await a11yAudit();
+ assert.equal(currentURL(), urls.authenticate.methods.global);
+ });
+
+ test('failed authentication shows a notification message', async function (assert) {
+ assert.expect(3);
+ await visit(urls.authenticate.methods.global);
+ assert.notOk(currentSession().isAuthenticated);
+ await fillIn('[name="identification"]', 'error');
+ await click('[type="submit"]');
+ assert.ok(find('.rose-notification.is-error'));
+ assert.notOk(currentSession().isAuthenticated);
+ });
+
+ test('can reset origin before authentication', async function (assert) {
+ assert.expect(1);
+ await visit(urls.authenticate.methods.global);
+ await click('.change-origin a');
+ assert.equal(currentURL(), urls.origin);
+ });
+
+ test('deauthentication redirects to first global authenticate method', async function (assert) {
+ assert.expect(3);
+ await visit(urls.authenticate.methods.global);
+ await fillIn('[name="identification"]', 'test');
+ await fillIn('[name="password"]', 'test');
+ later(async () => {
+ run.cancelTimers();
+ assert.ok(currentSession().isAuthenticated);
+ await click('.rose-header-utilities .rose-dropdown summary');
+ assert.equal(
+ find(
+ '.rose-header-utilities .rose-dropdown-content button'
+ ).textContent.trim(),
+ 'Deauthenticate'
+ );
+ await click('.rose-header-utilities .rose-dropdown-content button');
+ assert.notOk(currentSession().isAuthenticated);
+ }, 750);
+ await click('[type="submit"]');
+ });
+
+ test('401 responses result in deauthentication', async function (assert) {
+ assert.expect(3);
+ await visit(urls.authenticate.methods.global);
+ await fillIn('[name="identification"]', 'test');
+ await fillIn('[name="password"]', 'test');
+ later(async () => {
+ run.cancelTimers();
+ assert.ok(
+ currentSession().isAuthenticated,
+ 'Session begins authenticated, before encountering 401'
+ );
+ assert.ok(currentURL(), urls.targets);
+ this.server.get('/sessions', () => new Response(401));
+ await visit(urls.sessions);
+ assert.notOk(
+ currentSession().isAuthenticated,
+ 'Session is unauthenticated, after encountering 401'
+ );
+ }, 750);
+ await click('[type="submit"]');
+ });
+
test('color theme is applied from session data', async function (assert) {
assert.expect(12);
authenticateSession({
diff --git a/ui/desktop/tests/acceptance/origin-test.js b/ui/desktop/tests/acceptance/origin-test.js
index 8830cc483..4d7469c45 100644
--- a/ui/desktop/tests/acceptance/origin-test.js
+++ b/ui/desktop/tests/acceptance/origin-test.js
@@ -2,24 +2,27 @@ import { module, test } from 'qunit';
import {
visit,
currentURL,
- //fillIn,
- //click,
- //find,
+ fillIn,
+ click,
+ find,
//findAll,
//getRootElement
//setupOnerror,
} from '@ember/test-helpers';
+import { run, later } from '@ember/runloop';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
//import { Response } from 'miragejs';
-//import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import sinon from 'sinon';
import {
- //currentSession,
- //authenticateSession,
+ currentSession,
+ // authenticateSession,
invalidateSession,
} from 'ember-simple-auth/test-support';
+import config from '../../config/environment';
-module('Acceptance | authentication', function (hooks) {
+module('Acceptance | origin', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
@@ -43,6 +46,7 @@ module('Acceptance | authentication', function (hooks) {
const stubs = {
global: null,
org: null,
+ ipcService: null,
};
const urls = {
@@ -54,8 +58,11 @@ module('Acceptance | authentication', function (hooks) {
},
authenticate: {
global: null,
- org: null,
+ methods: {
+ global: null,
+ },
},
+ projects: null,
targets: null,
};
@@ -76,13 +83,9 @@ module('Acceptance | authentication', function (hooks) {
});
stubs.project = { id: instances.scopes.project.id, type: 'project' };
- // create other resources
instances.authMethods.global = this.server.create('auth-method', {
scope: instances.scopes.global,
});
- instances.authMethods.org = this.server.create('auth-method', {
- scope: instances.scopes.org,
- });
instances.hostCatalog = this.server.create(
'host-catalog',
@@ -96,9 +99,10 @@ module('Acceptance | authentication', function (hooks) {
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
- urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
- urls.authenticate.global = `${urls.scopes.global}/authenticate/${instances.authMethods.global.id}`;
- urls.authenticate.org = `${urls.scopes.global}/authenticate/${instances.authMethods.org.id}`;
+ urls.authenticate.global = `${urls.scopes.global}/authenticate`;
+ urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
+ urls.projects = `${urls.scopes.global}/projects`;
+ urls.targets = `${urls.projects}/targets`;
class MockIPC {
origin = null;
@@ -115,6 +119,8 @@ module('Acceptance | authentication', function (hooks) {
this.origin = origin;
return this.origin;
}
+
+ resetOrigin() {}
}
mockIPC = new MockIPC();
@@ -132,13 +138,92 @@ module('Acceptance | authentication', function (hooks) {
hooks.afterEach(function () {
window.removeEventListener('message', messageHandler);
+ sinon.restore();
+ });
+
+ test('visiting index', async function (assert) {
+ assert.expect(1);
+ await visit(urls.origin);
+ await a11yAudit();
+ assert.equal(currentURL(), urls.origin);
});
test('visiting index without an origin specified redirects to origin route', async function (assert) {
assert.expect(2);
await visit(urls.index);
- //await a11yAudit();
+ await a11yAudit();
assert.notOk(mockIPC.origin);
assert.equal(currentURL(), urls.origin);
});
+
+ test('can set origin', async function (assert) {
+ assert.expect(2);
+ assert.notOk(mockIPC.origin);
+ await visit(urls.origin);
+ await a11yAudit();
+ await fillIn('[name="host"]', window.location.origin);
+ await click('[type="submit"]');
+ assert.equal(mockIPC.origin, window.location.origin);
+ });
+
+ test('captures error on origin update', async function (assert) {
+ assert.expect(2);
+ assert.notOk(mockIPC.origin);
+ sinon.stub(this.owner.lookup('service:origin'), 'setOrigin').throws();
+ await visit(urls.origin);
+ await a11yAudit();
+ await fillIn('[name="host"]', window.location.origin);
+ await click('[type="submit"]');
+ assert.ok(find('.rose-notification.is-error'));
+ });
+
+ test('can reset origin before authentication', async function (assert) {
+ assert.expect(4);
+ assert.notOk(mockIPC.origin);
+ await visit(urls.origin);
+ await fillIn('[name="host"]', window.location.origin);
+ await click('[type="submit"]');
+ assert.equal(mockIPC.origin, window.location.origin);
+ assert.equal(currentURL(), urls.authenticate.methods.global);
+ await click('.change-origin a');
+ assert.equal(currentURL(), urls.origin);
+ });
+
+ test('origin set automatically in dev mode', async function (assert) {
+ assert.expect(1);
+ config.autoOrigin = true;
+ await visit(urls.origin);
+ await fillIn('[name="host"]', window.location.origin);
+ await click('[type="submit"]');
+ assert.equal(
+ this.owner.lookup('controller:origin').origin,
+ window.location.origin
+ );
+ config.autoOrigin = false;
+ });
+
+ test('can reset origin on error', async function (assert) {
+ assert.expect(4);
+ await visit(urls.origin);
+ await a11yAudit();
+ await fillIn('[name="host"]', window.location.origin);
+ await click('[type="submit"]');
+ await fillIn('[name="identification"]', 'test');
+ await fillIn('[name="password"]', 'test');
+ this.server.get('/targets', () => new Response(500));
+ await later(async () => {
+ run.cancelTimers();
+ assert.ok(currentSession().isAuthenticated);
+ assert.equal(
+ find('main section button').textContent.trim(),
+ 'Disconnect'
+ );
+ }, 750);
+ await click('[type="submit"]');
+ later(async () => {
+ assert.notOk(currentSession().isAuthenticated);
+ assert.equal(currentURL(), urls.authenticate.methods.global);
+ }, 750);
+ await click('main section button');
+ });
});
diff --git a/ui/desktop/tests/acceptance/projects-test.js b/ui/desktop/tests/acceptance/projects-test.js
index b3298cca9..d7978c12f 100644
--- a/ui/desktop/tests/acceptance/projects-test.js
+++ b/ui/desktop/tests/acceptance/projects-test.js
@@ -12,7 +12,7 @@ import {
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
//import { Response } from 'miragejs';
-//import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
import {
currentSession,
authenticateSession,
@@ -142,7 +142,7 @@ module('Acceptance | projects', function (hooks) {
invalidateSession();
assert.expect(2);
await visit(urls.projects);
- //await a11yAudit();
+ await a11yAudit();
assert.notOk(currentSession().isAuthenticated);
assert.equal(currentURL(), urls.authenticate.methods.global);
});
@@ -150,7 +150,7 @@ module('Acceptance | projects', function (hooks) {
test('visiting index', async function (assert) {
assert.expect(1);
await visit(urls.projects);
- //await a11yAudit();
+ await a11yAudit();
assert.equal(currentURL(), urls.projects);
});
});
diff --git a/ui/desktop/tests/acceptance/projects/sessions-test.js b/ui/desktop/tests/acceptance/projects/sessions-test.js
new file mode 100644
index 000000000..eb3e0bab1
--- /dev/null
+++ b/ui/desktop/tests/acceptance/projects/sessions-test.js
@@ -0,0 +1,261 @@
+import { module, test } from 'qunit';
+import {
+ visit,
+ currentURL,
+ //fillIn,
+ click,
+ find,
+ findAll,
+ //getRootElement
+ //setupOnerror,
+} from '@ember/test-helpers';
+import { run, later } from '@ember/runloop';
+import { setupApplicationTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { Response } from 'miragejs';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import {
+ currentSession,
+ authenticateSession,
+ invalidateSession,
+} from 'ember-simple-auth/test-support';
+
+module('Acceptance | projects | sessions', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ let mockIPC;
+ let messageHandler;
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ authMethods: {
+ global: null,
+ },
+ user: null,
+ session: null,
+ };
+
+ const stubs = {
+ global: null,
+ org: null,
+ };
+
+ const urls = {
+ index: '/',
+ origin: '/origin',
+ scopes: {
+ global: null,
+ org: null,
+ },
+ authenticate: {
+ global: null,
+ methods: {
+ global: null,
+ },
+ },
+ projects: null,
+ sessions: null,
+ };
+
+ const setDefaultOrigin = (test) => {
+ const windowOrigin = window.location.origin;
+ const origin = test.owner.lookup('service:origin');
+ origin.rendererOrigin = windowOrigin;
+ };
+
+ hooks.beforeEach(function () {
+ instances.user = this.server.create('user', {
+ scope: instances.scopes.global,
+ });
+
+ authenticateSession({ user_id: instances.user.id });
+
+ // create scopes
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ stubs.global = { id: 'global', type: 'global' };
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: stubs.global,
+ });
+ stubs.org = { id: instances.scopes.org.id, type: 'org' };
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: stubs.org,
+ });
+ stubs.project = { id: instances.scopes.project.id, type: 'project' };
+
+ instances.authMethods.global = this.server.create('auth-method', {
+ scope: instances.scopes.global,
+ });
+ instances.target = this.server.create(
+ 'target',
+ { scope: instances.scopes.project },
+ 'withRandomHostSets'
+ );
+ instances.session = this.server.create(
+ 'session',
+ {
+ scope: instances.scopes.project,
+ target: instances.target,
+ status: 'active',
+ user: instances.user,
+ },
+ 'withAssociations'
+ );
+
+ urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
+ urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
+ urls.authenticate.global = `${urls.scopes.global}/authenticate`;
+ urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
+ urls.projects = `${urls.scopes.org}/projects`;
+ urls.sessions = `${urls.projects}/sessions`;
+
+ class MockIPC {
+ origin = null;
+
+ invoke(method, payload) {
+ return this[method](payload);
+ }
+
+ getOrigin() {
+ return this.origin;
+ }
+
+ setOrigin(origin) {
+ this.origin = origin;
+ return this.origin;
+ }
+ }
+
+ mockIPC = new MockIPC();
+ messageHandler = async function (event) {
+ if (event.origin !== window.location.origin) return;
+ const { method, payload } = event.data;
+ if (method) {
+ const response = await mockIPC.invoke(method, payload);
+ event.ports[0].postMessage(response);
+ }
+ };
+
+ window.addEventListener('message', messageHandler);
+ setDefaultOrigin(this);
+ });
+
+ hooks.afterEach(function () {
+ window.removeEventListener('message', messageHandler);
+ });
+
+ test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
+ invalidateSession();
+ assert.expect(2);
+ await visit(urls.sessions);
+ await a11yAudit();
+ assert.notOk(currentSession().isAuthenticated);
+ assert.equal(currentURL(), urls.authenticate.methods.global);
+ });
+
+ test('visiting index', async function (assert) {
+ assert.expect(2);
+ const sessionsCount = this.server.schema.sessions.all().models.length;
+ // This later/cancelTimers technique allows us to test a page with
+ // active polling. Normally an acceptance test waits for all runloop timers
+ // to stop before returning from an awaited test, but polling means that
+ // runloop timers exist indefinitely. We thus schedule a cancelation before
+ // proceeding with our tests.
+ later(async () => {
+ run.cancelTimers();
+ // await a11yAudit();
+ assert.equal(currentURL(), urls.sessions);
+ assert.equal(findAll('tbody tr').length, sessionsCount);
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('visiting empty sessions', async function (assert) {
+ assert.expect(1);
+ this.server.get('/sessions', () => new Response(200));
+ later(async () => {
+ run.cancelTimers();
+ await a11yAudit();
+ assert.ok(
+ find('.rose-message-title').textContent.trim(),
+ 'No Sessions Available'
+ );
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('visiting sessions without targets is OK', async function (assert) {
+ assert.expect(2);
+ instances.session.update({ targetId: undefined });
+ const sessionsCount = this.server.schema.sessions.all().models.length;
+ later(async () => {
+ run.cancelTimers();
+ await a11yAudit();
+ assert.equal(currentURL(), urls.sessions);
+ assert.equal(findAll('tbody tr').length, sessionsCount);
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('can identify active sessions', async function (assert) {
+ assert.expect(1);
+ later(async () => {
+ run.cancelTimers();
+ assert.ok(
+ find('tbody tr:first-child th:first-child .session-status-active')
+ );
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('cannot identify pending sessions', async function (assert) {
+ assert.expect(1);
+ instances.session.update({ status: 'pending' });
+ later(async () => {
+ run.cancelTimers();
+ assert.notOk(
+ find('tbody tr:first-child th:first-child .session-status-active')
+ );
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('cannot identify terminated sessions', async function (assert) {
+ assert.expect(1);
+ instances.session.update({ status: 'terminated' });
+ later(async () => {
+ run.cancelTimers();
+ assert.notOk(
+ find('tbody tr:first-child th:first-child .session-status-active')
+ );
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('cancelling a session', async function (assert) {
+ assert.expect(1);
+ later(async () => {
+ run.cancelTimers();
+ await click('tbody tr:first-child td:last-child button');
+ assert.ok(find('[role="alert"].is-success'));
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('cancelling a session with error shows notification', async function (assert) {
+ assert.expect(1);
+ this.server.post('/sessions/:id_method', () => new Response(400));
+ later(async () => {
+ run.cancelTimers();
+ await click('tbody tr:first-child td:last-child button');
+ assert.ok(find('[role="alert"].is-error'));
+ }, 750);
+ await visit(urls.sessions);
+ });
+});
diff --git a/ui/desktop/tests/acceptance/projects/targets-test.js b/ui/desktop/tests/acceptance/projects/targets-test.js
new file mode 100644
index 000000000..ebda82cc1
--- /dev/null
+++ b/ui/desktop/tests/acceptance/projects/targets-test.js
@@ -0,0 +1,307 @@
+import { module, test } from 'qunit';
+import {
+ visit,
+ currentURL,
+ //fillIn,
+ click,
+ find,
+ findAll,
+ //getRootElement
+ //setupOnerror,
+} from '@ember/test-helpers';
+import { run, later } from '@ember/runloop';
+import { setupApplicationTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { Response } from 'miragejs';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import sinon from 'sinon';
+import {
+ currentSession,
+ authenticateSession,
+ invalidateSession,
+} from 'ember-simple-auth/test-support';
+
+module('Acceptance | projects | targets', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ let mockIPC;
+ let messageHandler;
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ authMethods: {
+ global: null,
+ },
+ target: null,
+ session: null,
+ };
+
+ const stubs = {
+ global: null,
+ org: null,
+ ipsService: null,
+ };
+
+ const urls = {
+ index: '/',
+ origin: '/origin',
+ scopes: {
+ global: null,
+ org: null,
+ },
+ authenticate: {
+ global: null,
+ methods: {
+ global: null,
+ },
+ },
+ projects: null,
+ targets: null,
+ target: null,
+ sessions: null,
+ };
+
+ const setDefaultOrigin = (test) => {
+ const windowOrigin = window.location.origin;
+ const origin = test.owner.lookup('service:origin');
+ origin.rendererOrigin = windowOrigin;
+ };
+
+ hooks.beforeEach(function () {
+ authenticateSession();
+
+ // create scopes
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ stubs.global = { id: 'global', type: 'global' };
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: stubs.global,
+ });
+ stubs.org = { id: instances.scopes.org.id, type: 'org' };
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: stubs.org,
+ });
+ stubs.project = { id: instances.scopes.project.id, type: 'project' };
+
+ instances.authMethods.global = this.server.create('auth-method', {
+ scope: instances.scopes.global,
+ });
+
+ instances.hostCatalog = this.server.create(
+ 'host-catalog',
+ { scope: instances.scopes.project },
+ 'withChildren'
+ );
+ instances.target = this.server.create(
+ 'target',
+ { scope: instances.scopes.project },
+ 'withRandomHostSets'
+ );
+
+ instances.session = this.server.create(
+ 'session',
+ {
+ scope: instances.scopes.project,
+ status: 'active',
+ },
+ 'withAssociations'
+ );
+
+ urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
+ urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
+ urls.authenticate.global = `${urls.scopes.global}/authenticate`;
+ urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
+ urls.projects = `${urls.scopes.org}/projects`;
+ urls.targets = `${urls.projects}/targets`;
+ urls.target = `${urls.targets}/${instances.target.id}`;
+ urls.sessions = `${urls.target}/sessions`;
+
+ class MockIPC {
+ origin = null;
+
+ invoke(method, payload) {
+ return this[method](payload);
+ }
+
+ getOrigin() {
+ return this.origin;
+ }
+
+ setOrigin(origin) {
+ this.origin = origin;
+ return this.origin;
+ }
+ }
+
+ mockIPC = new MockIPC();
+ messageHandler = async function (event) {
+ if (event.origin !== window.location.origin) return;
+ const { method, payload } = event.data;
+ if (method) {
+ const response = await mockIPC.invoke(method, payload);
+ event.ports[0].postMessage(response);
+ }
+ };
+
+ window.addEventListener('message', messageHandler);
+ setDefaultOrigin(this);
+
+ const ipcService = this.owner.lookup('service:ipc');
+ stubs.ipcService = sinon.stub(ipcService, 'invoke');
+ });
+
+ hooks.afterEach(function () {
+ window.removeEventListener('message', messageHandler);
+ });
+
+ test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
+ invalidateSession();
+ assert.expect(2);
+ await visit(urls.targets);
+ await a11yAudit();
+ assert.notOk(currentSession().isAuthenticated);
+ assert.equal(currentURL(), urls.authenticate.methods.global);
+ });
+
+ test('visiting index', async function (assert) {
+ assert.expect(2);
+ const targetsCount = this.server.schema.targets.all().models.length;
+ // This later/cancelTimers technique allows us to test a page with
+ // active polling. Normally an acceptance test waits for all runloop timers
+ // to stop before returning from an awaited test, but polling means that
+ // runloop timers exist indefinitely. We thus schedule a cancelation before
+ // proceeding with our tests.
+ later(async () => {
+ run.cancelTimers();
+ await a11yAudit();
+ assert.equal(currentURL(), urls.targets);
+ assert.equal(findAll('tbody tr').length, targetsCount);
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('visiting a target', async function (assert) {
+ assert.expect(1);
+ later(async () => {
+ run.cancelTimers();
+ await click('tbody tr th a');
+ assert.equal(currentURL(), urls.sessions);
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('visiting empty targets', async function (assert) {
+ assert.expect(1);
+ this.server.get('/targets', () => new Response(200));
+ later(async () => {
+ run.cancelTimers();
+ assert.equal(
+ find('.rose-message-title').textContent.trim(),
+ 'No Targets Available'
+ );
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('connecting to a target', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').returns({
+ session_id: instances.session.id,
+ address: 'a_123',
+ port: 'p_123',
+ protocol: 'tcp',
+ });
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-success'), 'Success dialog');
+ assert.equal(findAll('.rose-dialog-footer button').length, 1);
+ assert.equal(
+ find('.rose-dialog-footer button').textContent.trim(),
+ 'OK',
+ 'Cannot retry'
+ );
+ assert.equal(
+ find('.rose-dialog-body .copyable-content').textContent.trim(),
+ 'Local proxy address (tcp): a_123:p_123'
+ );
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('handles cli error on connect', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-error'), 'Error dialog');
+ const dialogButtons = findAll('.rose-dialog-footer button');
+ assert.equal(dialogButtons.length, 2);
+ assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
+ assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('handles connect error', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').rejects();
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-error'), 'Error dialog');
+ const dialogButtons = findAll('.rose-dialog-footer button');
+ assert.equal(dialogButtons.length, 2);
+ assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
+ assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('can retry on error', async function (assert) {
+ assert.expect(1);
+ stubs.ipcService.withArgs('cliExists').rejects();
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ const firstErrorDialog = find('.rose-dialog');
+ await click('.rose-dialog footer .rose-button-primary', 'Retry');
+ const secondErrorDialog = find('.rose-dialog');
+ assert.notEqual(secondErrorDialog.id, firstErrorDialog.id);
+ }, 750);
+ await visit(urls.targets);
+ });
+});
diff --git a/ui/desktop/tests/acceptance/projects/targets/hosts-test.js b/ui/desktop/tests/acceptance/projects/targets/hosts-test.js
new file mode 100644
index 000000000..de1d7162c
--- /dev/null
+++ b/ui/desktop/tests/acceptance/projects/targets/hosts-test.js
@@ -0,0 +1,298 @@
+import { module, test } from 'qunit';
+import {
+ visit,
+ currentURL,
+ //fillIn,
+ click,
+ find,
+ findAll,
+ //getRootElement
+ //setupOnerror,
+} from '@ember/test-helpers';
+import { run, later } from '@ember/runloop';
+import { setupApplicationTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+// import { Response } from 'miragejs';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import sinon from 'sinon';
+import {
+ currentSession,
+ authenticateSession,
+ invalidateSession,
+} from 'ember-simple-auth/test-support';
+
+module('Acceptance | projects | targets | hosts', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ let mockIPC;
+ let messageHandler;
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ authMethods: {
+ global: null,
+ },
+ user: null,
+ target: null,
+ session: null,
+ };
+
+ const stubs = {
+ global: null,
+ org: null,
+ ipcService: null,
+ };
+
+ const urls = {
+ index: '/',
+ origin: '/origin',
+ scopes: {
+ global: null,
+ org: null,
+ },
+ authenticate: {
+ global: null,
+ methods: {
+ global: null,
+ },
+ },
+ projects: null,
+ targets: null,
+ target: null,
+ hosts: null,
+ };
+
+ const setDefaultOrigin = (test) => {
+ const windowOrigin = window.location.origin;
+ const origin = test.owner.lookup('service:origin');
+ origin.rendererOrigin = windowOrigin;
+ };
+
+ hooks.beforeEach(function () {
+ instances.user = this.server.create('user', {
+ scope: instances.scopes.global,
+ });
+
+ authenticateSession({ user_id: instances.user.id });
+
+ // create scopes
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ stubs.global = { id: 'global', type: 'global' };
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: stubs.global,
+ });
+ stubs.org = { id: instances.scopes.org.id, type: 'org' };
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: stubs.org,
+ });
+ stubs.project = { id: instances.scopes.project.id, type: 'project' };
+
+ instances.authMethods.global = this.server.create('auth-method', {
+ scope: instances.scopes.global,
+ });
+ 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,
+ });
+ instances.session = this.server.create(
+ 'session',
+ {
+ scope: instances.scopes.project,
+ status: 'active',
+ },
+ 'withAssociations'
+ );
+
+ urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
+ urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
+ urls.authenticate.global = `${urls.scopes.global}/authenticate`;
+ urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
+ urls.projects = `${urls.scopes.org}/projects`;
+ urls.targets = `${urls.projects}/targets`;
+ urls.target = `${urls.targets}/${instances.target.id}`;
+ urls.hosts = `${urls.target}/hosts`;
+
+ class MockIPC {
+ origin = null;
+
+ invoke(method, payload) {
+ return this[method](payload);
+ }
+
+ getOrigin() {
+ return this.origin;
+ }
+
+ setOrigin(origin) {
+ this.origin = origin;
+ return this.origin;
+ }
+ }
+
+ mockIPC = new MockIPC();
+ messageHandler = async function (event) {
+ if (event.origin !== window.location.origin) return;
+ const { method, payload } = event.data;
+ if (method) {
+ const response = await mockIPC.invoke(method, payload);
+ event.ports[0].postMessage(response);
+ }
+ };
+
+ window.addEventListener('message', messageHandler);
+ setDefaultOrigin(this);
+
+ const ipcService = this.owner.lookup('service:ipc');
+ stubs.ipcService = sinon.stub(ipcService, 'invoke');
+ });
+
+ hooks.afterEach(function () {
+ window.removeEventListener('message', messageHandler);
+ });
+
+ test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
+ invalidateSession();
+ assert.expect(2);
+ await visit(urls.hosts);
+ await a11yAudit();
+ assert.notOk(currentSession().isAuthenticated);
+ assert.equal(currentURL(), urls.authenticate.methods.global);
+ });
+
+ test('visiting index', async function (assert) {
+ assert.expect(2);
+ const hostsCount = this.server.schema.hosts.all().models.length;
+ // This later/cancelTimers technique allows us to test a page with
+ // active polling. Normally an acceptance test waits for all runloop timers
+ // to stop before returning from an awaited test, but polling means that
+ // runloop timers exist indefinitely. We thus schedule a cancelation before
+ // proceeding with our tests.
+ later(async () => {
+ run.cancelTimers();
+ // await a11yAudit();
+ assert.equal(currentURL(), urls.hosts);
+ assert.equal(findAll('tbody tr').length, hostsCount);
+ }, 750);
+ await visit(urls.hosts);
+ });
+
+ test('visiting empty hosts', async function (assert) {
+ assert.expect(1);
+ instances.target.update({ hostSets: [] });
+ later(async () => {
+ run.cancelTimers();
+ assert.ok(
+ find('.rose-message-title').textContent.trim(),
+ 'No Hosts Available'
+ );
+ }, 750);
+ await visit(urls.hosts);
+ });
+
+ test('connecting to a host', async function (assert) {
+ assert.expect(3);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').returns({
+ session_id: instances.session.id,
+ address: 'a_123',
+ port: 'p_123',
+ protocol: 'tcp',
+ });
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-success'), 'Success dialog');
+ assert.equal(
+ find('.rose-dialog-footer .rose-button-primary').textContent.trim(),
+ 'OK',
+ 'Cannot retry'
+ );
+ assert.equal(
+ find('.rose-dialog-body .copyable-content').textContent.trim(),
+ 'Local proxy address (tcp): a_123:p_123'
+ );
+ }, 750);
+ await visit(urls.hosts);
+ });
+
+ test('handles cli error on connect', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(false);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-error'), 'Error dialog');
+ const dialogButtons = findAll('.rose-dialog-footer button');
+ assert.equal(dialogButtons.length, 2);
+ assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
+ assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
+ }, 750);
+ await visit(urls.hosts);
+ });
+
+ test('handles connect error', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').rejects();
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-error'), 'Error dialog');
+ const dialogButtons = findAll('.rose-dialog-footer button');
+ assert.equal(dialogButtons.length, 2);
+ assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
+ assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
+ }, 750);
+ await visit(urls.hosts);
+ });
+
+ test('can retry on error', async function (assert) {
+ assert.expect(1);
+ stubs.ipcService.withArgs('cliExists').rejects();
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ const firstErrorDialog = find('.rose-dialog');
+ await click('.rose-dialog footer .rose-button-primary', 'Retry');
+ const secondErrorDialog = find('.rose-dialog');
+ assert.notEqual(secondErrorDialog.id, firstErrorDialog.id);
+ }, 750);
+ await visit(urls.hosts);
+ });
+});
diff --git a/ui/desktop/tests/acceptance/projects/targets/sessions-test.js b/ui/desktop/tests/acceptance/projects/targets/sessions-test.js
new file mode 100644
index 000000000..a28682950
--- /dev/null
+++ b/ui/desktop/tests/acceptance/projects/targets/sessions-test.js
@@ -0,0 +1,381 @@
+import { module, test } from 'qunit';
+import {
+ visit,
+ currentURL,
+ //fillIn,
+ click,
+ find,
+ findAll,
+ //getRootElement
+ //setupOnerror,
+} from '@ember/test-helpers';
+import { run, later } from '@ember/runloop';
+import { setupApplicationTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { Response } from 'miragejs';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import sinon from 'sinon';
+import {
+ currentSession,
+ authenticateSession,
+ invalidateSession,
+} from 'ember-simple-auth/test-support';
+
+module('Acceptance | projects | targets | sessions', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ let mockIPC;
+ let messageHandler;
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ authMethods: {
+ global: null,
+ },
+ user: null,
+ target: null,
+ session: null,
+ };
+
+ const stubs = {
+ global: null,
+ org: null,
+ ipcService: null,
+ };
+
+ const urls = {
+ index: '/',
+ origin: '/origin',
+ scopes: {
+ global: null,
+ org: null,
+ },
+ authenticate: {
+ global: null,
+ methods: {
+ global: null,
+ },
+ },
+ projects: null,
+ targets: null,
+ target: null,
+ sessions: null,
+ };
+
+ const setDefaultOrigin = (test) => {
+ const windowOrigin = window.location.origin;
+ const origin = test.owner.lookup('service:origin');
+ origin.rendererOrigin = windowOrigin;
+ };
+
+ hooks.beforeEach(function () {
+ instances.user = this.server.create('user', {
+ scope: instances.scopes.global,
+ });
+
+ authenticateSession({ user_id: instances.user.id });
+
+ // create scopes
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ stubs.global = { id: 'global', type: 'global' };
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: stubs.global,
+ });
+ stubs.org = { id: instances.scopes.org.id, type: 'org' };
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: stubs.org,
+ });
+ stubs.project = { id: instances.scopes.project.id, type: 'project' };
+
+ instances.authMethods.global = this.server.create('auth-method', {
+ scope: instances.scopes.global,
+ });
+
+ instances.hostCatalog = this.server.create(
+ 'host-catalog',
+ { scope: instances.scopes.project },
+ 'withChildren'
+ );
+ instances.target = this.server.create(
+ 'target',
+ { scope: instances.scopes.project },
+ 'withRandomHostSets'
+ );
+
+ instances.session = this.server.create(
+ 'session',
+ {
+ scope: instances.scopes.project,
+ target: instances.target,
+ status: 'active',
+ user: instances.user,
+ },
+ 'withAssociations'
+ );
+
+ urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
+ urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
+ urls.authenticate.global = `${urls.scopes.global}/authenticate`;
+ urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
+ urls.projects = `${urls.scopes.org}/projects`;
+ urls.targets = `${urls.projects}/targets`;
+ urls.target = `${urls.targets}/${instances.target.id}`;
+ urls.sessions = `${urls.target}/sessions`;
+
+ class MockIPC {
+ origin = null;
+
+ invoke(method, payload) {
+ return this[method](payload);
+ }
+
+ getOrigin() {
+ return this.origin;
+ }
+
+ setOrigin(origin) {
+ this.origin = origin;
+ return this.origin;
+ }
+ }
+
+ mockIPC = new MockIPC();
+ messageHandler = async function (event) {
+ if (event.origin !== window.location.origin) return;
+ const { method, payload } = event.data;
+ if (method) {
+ const response = await mockIPC.invoke(method, payload);
+ event.ports[0].postMessage(response);
+ }
+ };
+
+ window.addEventListener('message', messageHandler);
+ setDefaultOrigin(this);
+
+ const ipcService = this.owner.lookup('service:ipc');
+ stubs.ipcService = sinon.stub(ipcService, 'invoke');
+ });
+
+ hooks.afterEach(function () {
+ window.removeEventListener('message', messageHandler);
+ });
+
+ test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
+ invalidateSession();
+ assert.expect(2);
+ await visit(urls.sessions);
+ await a11yAudit();
+ assert.notOk(currentSession().isAuthenticated);
+ assert.equal(currentURL(), urls.authenticate.methods.global);
+ });
+
+ test('visiting index', async function (assert) {
+ assert.expect(2);
+ const sessionsCount = this.server.schema.sessions.all().models.length;
+ // This later/cancelTimers technique allows us to test a page with
+ // active polling. Normally an acceptance test waits for all runloop timers
+ // to stop before returning from an awaited test, but polling means that
+ // runloop timers exist indefinitely. We thus schedule a cancelation before
+ // proceeding with our tests.
+ later(async () => {
+ run.cancelTimers();
+ // await a11yAudit();
+ assert.equal(currentURL(), urls.sessions);
+ assert.equal(findAll('tbody tr').length, sessionsCount);
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('visiting index redirects to sessions', async function (assert) {
+ assert.expect(1);
+ later(async () => {
+ run.cancelTimers();
+ // await a11yAudit();
+ assert.equal(currentURL(), urls.sessions);
+ }, 750);
+ await visit(urls.target);
+ });
+
+ test('visiting empty sessions', async function (assert) {
+ assert.expect(1);
+ this.server.get('/sessions', () => new Response(200));
+ later(async () => {
+ run.cancelTimers();
+ assert.ok(
+ find('.rose-message-title').textContent.trim(),
+ 'No Sessions Available'
+ );
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('can identify target with active sessions', async function (assert) {
+ assert.expect(1);
+ later(async () => {
+ run.cancelTimers();
+ assert.ok(find('.rose-layout-page-header .rose-badge-success'));
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('can identify target with pending sessions', async function (assert) {
+ assert.expect(1);
+ instances.session.update({ status: 'pending' });
+ later(async () => {
+ run.cancelTimers();
+ assert.ok(find('.rose-layout-page-header .rose-badge-success'));
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('cannot identify target with terminated sessions', async function (assert) {
+ assert.expect(1);
+ instances.session.update({ status: 'terminated' });
+ later(async () => {
+ run.cancelTimers();
+ assert.notOk(find('.rose-layout-page-header .rose-badge-success'));
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('cancelling a session', async function (assert) {
+ assert.expect(2);
+ const sessionsCount = this.server.schema.sessions.all().models.length;
+ later(async () => {
+ run.cancelTimers();
+ await click('tbody tr:first-child td:last-child button');
+ assert.ok(find('[role="alert"].is-success'));
+ assert.equal(findAll('tbody tr').length, sessionsCount - 1);
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('cancelling a session with error shows notification', async function (assert) {
+ assert.expect(1);
+ this.server.post('/sessions/:id_method', () => new Response(400));
+ later(async () => {
+ run.cancelTimers();
+ await click('tbody tr:first-child td:last-child button');
+ assert.ok(find('[role="alert"].is-error'));
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('connecting to a target', async function (assert) {
+ assert.expect(3);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').returns({
+ session_id: instances.session.id,
+ address: 'a_123',
+ port: 'p_123',
+ protocol: 'tcp',
+ });
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click('.rose-layout-page-actions button', 'Activate connect mode');
+ assert.ok(find('.rose-dialog-success'), 'Success dialog');
+ assert.equal(
+ find('.rose-dialog-footer .rose-button-primary').textContent.trim(),
+ 'OK',
+ 'Cannot retry'
+ );
+ assert.equal(
+ find('.rose-dialog-body .copyable-content').textContent.trim(),
+ 'Local proxy address (tcp): a_123:p_123'
+ );
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('connect details in session', async function (assert) {
+ assert.expect(2);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').returns({
+ session_id: instances.session.id,
+ address: 'a_123',
+ port: 'p_123',
+ protocol: 'tcp',
+ });
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ instances.session.update('status', 'active');
+ await click('.rose-layout-page-actions button', 'Activate connect mode');
+ assert.ok(find('.rose-dialog-success'), 'Success dialog');
+ await click('.rose-dialog-dismiss');
+ assert.equal(
+ find(
+ 'tbody tr:first-child td:nth-child(2) .copyable-content'
+ ).textContent.trim(),
+ 'a_123:p_123'
+ );
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('handles cli error on connect', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(false);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click('.rose-layout-page-actions button', 'Activate connect mode');
+ assert.ok(find('.rose-dialog-error'), 'Error dialog');
+ const dialogButtons = findAll('.rose-dialog-footer button');
+ assert.equal(dialogButtons.length, 2);
+ assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
+ assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('handles connect error', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').rejects();
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click('.rose-layout-page-actions button', 'Activate connect mode');
+ assert.ok(find('.rose-dialog-error'), 'Error dialog');
+ const dialogButtons = findAll('.rose-dialog-footer button');
+ assert.equal(dialogButtons.length, 2);
+ assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
+ assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
+ }, 750);
+ await visit(urls.sessions);
+ });
+
+ test('can retry on error', async function (assert) {
+ assert.expect(1);
+ stubs.ipcService.withArgs('cliExists').rejects();
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click('.rose-layout-page-actions button', 'Activate connect mode');
+ const firstErrorDialog = find('.rose-dialog');
+ await click('.rose-dialog footer .rose-button-primary', 'Retry');
+ const secondErrorDialog = find('.rose-dialog');
+ assert.notEqual(secondErrorDialog.id, firstErrorDialog.id);
+ }, 750);
+ await visit(urls.sessions);
+ });
+});
diff --git a/ui/desktop/tests/acceptance/scopes-test.js b/ui/desktop/tests/acceptance/scopes-test.js
new file mode 100644
index 000000000..cb15c0af9
--- /dev/null
+++ b/ui/desktop/tests/acceptance/scopes-test.js
@@ -0,0 +1,348 @@
+import { module, test } from 'qunit';
+import {
+ visit,
+ currentURL,
+ //fillIn,
+ click,
+ find,
+ findAll,
+ //getRootElement
+ //setupOnerror,
+} from '@ember/test-helpers';
+import { run, later } from '@ember/runloop';
+import { setupApplicationTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { Response } from 'miragejs';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import sinon from 'sinon';
+import {
+ currentSession,
+ authenticateSession,
+ invalidateSession,
+} from 'ember-simple-auth/test-support';
+
+module('Acceptance | scopes', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ let mockIPC;
+ let messageHandler;
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ authMethods: {
+ global: null,
+ },
+ target: null,
+ session: null,
+ };
+
+ const stubs = {
+ global: null,
+ org: null,
+ ipcService: null,
+ };
+
+ const urls = {
+ index: '/',
+ origin: '/origin',
+ scopes: {
+ global: null,
+ org: null,
+ org2: null,
+ },
+ authenticate: {
+ global: null,
+ methods: {
+ global: null,
+ },
+ },
+ projects: null,
+ org2Projects: null,
+ globalProjects: null,
+ targets: null,
+ org2Targets: null,
+ globalTargets: null,
+ target: null,
+ targetSessions: null,
+ };
+
+ const setDefaultOrigin = (test) => {
+ const windowOrigin = window.location.origin;
+ const origin = test.owner.lookup('service:origin');
+ origin.rendererOrigin = windowOrigin;
+ };
+
+ hooks.beforeEach(function () {
+ authenticateSession();
+
+ // create scopes
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ stubs.global = { id: 'global', type: 'global' };
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: stubs.global,
+ });
+ instances.scopes.org2 = this.server.create('scope', {
+ type: 'org',
+ scope: stubs.global,
+ });
+ stubs.org = { id: instances.scopes.org.id, type: 'org' };
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: stubs.org,
+ });
+ stubs.project = { id: instances.scopes.project.id, type: 'project' };
+
+ instances.authMethods.global = this.server.create('auth-method', {
+ scope: instances.scopes.global,
+ });
+
+ instances.hostCatalog = this.server.create(
+ 'host-catalog',
+ { scope: instances.scopes.project },
+ 'withChildren'
+ );
+ instances.target = this.server.create(
+ 'target',
+ { scope: instances.scopes.project },
+ 'withRandomHostSets'
+ );
+
+ instances.session = this.server.create(
+ 'session',
+ {
+ scope: instances.scopes.project,
+ status: 'active',
+ },
+ 'withAssociations'
+ );
+
+ urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
+ urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
+ urls.scopes.org2 = `/scopes/${instances.scopes.org2.id}`;
+ urls.authenticate.global = `${urls.scopes.global}/authenticate`;
+ urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
+ urls.projects = `${urls.scopes.org}/projects`;
+ urls.org2Projects = `${urls.scopes.org2}/projects`;
+ urls.globalProjects = `${urls.scopes.global}/projects`;
+ urls.targets = `${urls.projects}/targets`;
+ urls.org2Targets = `${urls.org2Projects}/targets`;
+ urls.globalTargets = `${urls.globalProjects}/targets`;
+ urls.target = `${urls.targets}/${instances.target.id}`;
+ urls.targetSessions = `${urls.target}/sessions`;
+
+ class MockIPC {
+ origin = null;
+
+ invoke(method, payload) {
+ return this[method](payload);
+ }
+
+ getOrigin() {
+ return this.origin;
+ }
+
+ setOrigin(origin) {
+ this.origin = origin;
+ return this.origin;
+ }
+ }
+
+ mockIPC = new MockIPC();
+ messageHandler = async function (event) {
+ if (event.origin !== window.location.origin) return;
+ const { method, payload } = event.data;
+ if (method) {
+ const response = await mockIPC.invoke(method, payload);
+ event.ports[0].postMessage(response);
+ }
+ };
+
+ window.addEventListener('message', messageHandler);
+ setDefaultOrigin(this);
+
+ const ipcService = this.owner.lookup('service:ipc');
+ stubs.ipcService = sinon.stub(ipcService, 'invoke');
+ });
+
+ hooks.afterEach(function () {
+ window.removeEventListener('message', messageHandler);
+ });
+
+ test('visiting index', async function (assert) {
+ assert.expect(2);
+ const targetsCount = this.server.schema.targets.all().models.length;
+ // This later/cancelTimers technique allows us to test a page with
+ // active polling. Normally an acceptance test waits for all runloop timers
+ // to stop before returning from an awaited test, but polling means that
+ // runloop timers exist indefinitely. We thus schedule a cancelation before
+ // proceeding with our tests.
+ later(async () => {
+ run.cancelTimers();
+ await a11yAudit();
+ assert.equal(currentURL(), urls.targets);
+ assert.equal(findAll('tbody tr').length, targetsCount);
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('visiting global scope', async function (assert) {
+ assert.expect(1);
+ later(async () => {
+ run.cancelTimers();
+ await a11yAudit();
+ assert.equal(currentURL(), urls.globalTargets);
+ }, 750);
+ await visit(urls.scopes.global);
+ });
+
+ // TODO: this probably shouldn't be the case, but was setup to enable
+ // authentication when the global scope couldn't be loaded.
+ // In order to resolve this, we might hoist authentication routes up from
+ // under scopes.
+ test('visiting global scope is not successful when the global scope cannot be fetched', async function (assert) {
+ assert.expect(1);
+ this.server.get('/scopes/:id', ({ scopes }, { params: { id } }) => {
+ const scope = scopes.find(id);
+ const response = id === 'global' ? new Response(404) : scope;
+ return response;
+ });
+ later(async () => {
+ run.cancelTimers();
+ await a11yAudit();
+ assert.equal(currentURL(), urls.globalTargets);
+ }, 750);
+ await visit(urls.scopes.global);
+ });
+
+ test('visiting org scope', async function (assert) {
+ assert.expect(1);
+ later(async () => {
+ run.cancelTimers();
+ await a11yAudit();
+ assert.equal(currentURL(), urls.targets);
+ }, 750);
+ await visit(urls.scopes.org);
+ });
+
+ test('can navigate among org scopes via header navigation', async function (assert) {
+ assert.expect(3);
+ await later(async () => run.cancelTimers(), 750);
+ await visit(urls.targets);
+ await later(async () => assert.equal(currentURL(), urls.targets), 750);
+ await click('.rose-header-nav .rose-dropdown a:nth-of-type(2)');
+ await later(async () => assert.equal(currentURL(), urls.org2Targets), 750);
+ await click('.rose-header-nav .rose-dropdown a:nth-of-type(3)');
+ later(async () => assert.equal(currentURL(), urls.globalTargets), 750);
+ await click('.rose-header-nav .rose-dropdown a:nth-of-type(1)');
+ });
+
+ test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
+ invalidateSession();
+ assert.expect(2);
+ await visit(urls.targets);
+ await a11yAudit();
+ assert.notOk(currentSession().isAuthenticated);
+ assert.equal(currentURL(), urls.authenticate.methods.global);
+ });
+
+ test('visiting a target', async function (assert) {
+ assert.expect(1);
+ later(async () => {
+ run.cancelTimers();
+ await click('tbody tr th a');
+ assert.equal(currentURL(), urls.targetSessions);
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('visiting empty targets', async function (assert) {
+ assert.expect(1);
+ this.server.get('/targets', () => new Response(200));
+ later(async () => {
+ run.cancelTimers();
+ assert.ok(
+ find('.rose-message-title').textContent.trim(),
+ 'No Targets Available'
+ );
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('connecting to a target', async function (assert) {
+ assert.expect(3);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').returns({
+ session_id: instances.session.id,
+ address: 'a_123',
+ port: 'p_123',
+ protocol: 'tcp',
+ });
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-success'), 'Success dialog');
+ assert.equal(findAll('.rose-dialog-footer button').length, 1);
+ assert.equal(
+ find('.rose-dialog-footer button').textContent.trim(),
+ 'OK',
+ 'Cannot retry'
+ );
+ }, 750);
+ await visit(urls.targets);
+ });
+
+ test('handles cli error on connect', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(false);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-error'), 'Error dialog');
+ const dialogButtons = findAll('.rose-dialog-footer button');
+ assert.equal(dialogButtons.length, 2);
+ assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
+ assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
+ }, 750);
+ await visit(urls.scopes.global);
+ });
+
+ test('handles connect error', async function (assert) {
+ assert.expect(4);
+ stubs.ipcService.withArgs('cliExists').returns(true);
+ stubs.ipcService.withArgs('connect').rejects();
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+
+ later(async () => {
+ run.cancelTimers();
+ await click(
+ 'tbody tr:first-child td:last-child button',
+ 'Activate connect mode'
+ );
+ assert.ok(find('.rose-dialog-error'), 'Error dialog');
+ const dialogButtons = findAll('.rose-dialog-footer button');
+ assert.equal(dialogButtons.length, 2);
+ assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry');
+ assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel');
+ }, 750);
+ await visit(urls.targets);
+ });
+});
diff --git a/ui/desktop/tests/integration/components/branded-card-test.js b/ui/desktop/tests/integration/components/branded-card-test.js
index 019103ec5..cc537772d 100644
--- a/ui/desktop/tests/integration/components/branded-card-test.js
+++ b/ui/desktop/tests/integration/components/branded-card-test.js
@@ -1,26 +1,27 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
-import { render } from '@ember/test-helpers';
+import { render, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | branded-card', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
- // Set any properties with this.set('myProperty', 'value');
- // Handle any actions with this.set('myAction', function(val) { ... });
-
await render(hbs``);
+ assert.ok(find('.branded-card'));
+ assert.ok(find('.branded-card-title'));
+ assert.ok(find('.branded-card-description'));
+ });
- assert.equal(this.element.textContent.trim(), '');
-
- // Template block usage:
- await render(hbs`
-
- template block text
-
- `);
-
- assert.equal(this.element.textContent.trim(), 'template block text');
+ test('it renders with content', async function (assert) {
+ await render(hbs``);
+ assert.equal(find('.branded-card-title').textContent.trim(), 'title');
+ assert.equal(
+ find('.branded-card-description').textContent.trim(),
+ 'description'
+ );
});
});
diff --git a/ui/desktop/tests/unit/services/origin-test.js b/ui/desktop/tests/unit/services/origin-test.js
index 1355719a2..4d8703cf6 100644
--- a/ui/desktop/tests/unit/services/origin-test.js
+++ b/ui/desktop/tests/unit/services/origin-test.js
@@ -1,12 +1,27 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
+import sinon from 'sinon';
module('Unit | Service | origin', function (hooks) {
setupTest(hooks);
- // TODO: Replace this with your real tests.
- test('it exists', function (assert) {
- let service = this.owner.lookup('service:origin');
- assert.ok(service);
+ let service, ipcService;
+
+ hooks.beforeEach(function () {
+ service = this.owner.lookup('service:origin');
+ ipcService = this.owner.lookup('service:ipc');
+ });
+
+ test('resets origin on error', async function (assert) {
+ assert.expect(4);
+ const ipcServiceStubbed = sinon.stub(ipcService, 'invoke');
+ await service.setOrigin(window.location.origin);
+ assert.equal(service.rendererOrigin, window.location.origin);
+ assert.equal(service.adapter.host, window.location.origin);
+ ipcServiceStubbed.withArgs('setOrigin').rejects();
+ service.setOrigin('invalid-origin').catch(() => {
+ assert.notOk(service.rendererOrigin);
+ assert.equal(service.adapter.host, window.location.origin);
+ });
});
});
diff --git a/yarn.lock b/yarn.lock
index 768f6d845..aeaef9f2a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4174,6 +4174,11 @@ axe-core@3.5.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227"
integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==
+axe-core@^4.1.2:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.3.tgz#64a4c85509e0991f5168340edc4bedd1ceea6966"
+ integrity sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ==
+
babel-code-frame@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@@ -6495,9 +6500,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001157, caniuse-lite@^1.0.30001181:
- version "1.0.30001187"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001187.tgz#5706942631f83baa5a0218b7dfa6ced29f845438"
- integrity sha512-w7/EP1JRZ9552CyrThUnay2RkZ1DXxKe/Q2swTC4+LElLh9RRYrL1Z+27LlakB8kzY0fSmHw9mc7XYDUKAKWMA==
+ version "1.0.30001204"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz#256c85709a348ec4d175e847a3b515c66e79f2aa"
+ integrity sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==
capture-exit@^2.0.0:
version "2.0.0"
@@ -7736,6 +7741,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+date-and-time@^0.14.1:
+ version "0.14.2"
+ resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.14.2.tgz#a4266c3dead460f6c231fe9674e585908dac354e"
+ integrity sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==
+
date-time@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/date-time/-/date-time-2.1.0.tgz#0286d1b4c769633b3ca13e1e62558d2dbdc2eba2"
@@ -7785,7 +7795,7 @@ debug@~4.1.0:
dependencies:
ms "^2.1.1"
-debuglog@^1.0.1:
+debuglog@*, debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -8350,9 +8360,9 @@ electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.649:
integrity sha512-ojGDupQ3UMkvPWcTICe4JYe17+o9OLiFMPoduoR72Zp2ILt1mRVeqnxBEd6s/ptekrnsFU+0A4lStfBe/wyG/A==
electron-to-chromium@^1.3.564:
- version "1.3.687"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.687.tgz#c336184b7ab70427ffe2ee79eaeaedbc1ad8c374"
- integrity sha512-IpzksdQNl3wdgkzf7dnA7/v10w0Utf1dF2L+B4+gKrloBrxCut+au+kky3PYvle3RMdSxZP+UiCZtLbcYRxSNQ==
+ version "1.3.693"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.693.tgz#5089c506a925c31f93fcb173a003a22e341115dd"
+ integrity sha512-vUdsE8yyeu30RecppQtI+XTz2++LWLVEIYmzeCaCRLSdtKZ2eXqdJcrs85KwLiPOPVc6PELgWyXBsfqIvzGZag==
element-resize-detector@^1.2.1:
version "1.2.1"
@@ -8385,6 +8395,24 @@ ember-a11y-testing@^3.0.0:
ember-cli-version-checker "5.1.1"
ember-get-config "0.2.4"
+ember-a11y-testing@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/ember-a11y-testing/-/ember-a11y-testing-4.0.2.tgz#960ee9582bd315bd16d9dedbf450fd508d021d99"
+ integrity sha512-3pa+6bibbY4Vsl3uKFpD0OH4Mzd8NQUwenaWWWngiJo+R/U6AgJllxo/apORzRgvLxKtBCEpXVocUp/gkZ3rOQ==
+ dependencies:
+ axe-core "^4.1.2"
+ body-parser "^1.19.0"
+ broccoli-funnel "^3.0.3"
+ broccoli-merge-trees "^4.2.0"
+ date-and-time "^0.14.1"
+ ember-auto-import "^1.10.1"
+ ember-cli-babel "^7.21.0"
+ ember-cli-typescript "^3.0.0"
+ ember-cli-version-checker "^5.1.2"
+ ember-destroyable-polyfill "^2.0.1"
+ fs-extra "^9.0.1"
+ validate-peer-dependencies "^1.1.0"
+
ember-assign-polyfill@^2.6.0:
version "2.7.2"
resolved "https://registry.yarnpkg.com/ember-assign-polyfill/-/ember-assign-polyfill-2.7.2.tgz#58f6f60235126cb23df248c846008fa9a3245fc1"
@@ -8833,7 +8861,7 @@ ember-cli-typescript@^2.0.0-rc.2, ember-cli-typescript@^2.0.2:
stagehand "^1.0.0"
walk-sync "^1.0.0"
-ember-cli-typescript@^3.1.3, ember-cli-typescript@^3.1.4:
+ember-cli-typescript@^3.0.0, ember-cli-typescript@^3.1.3, ember-cli-typescript@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-3.1.4.tgz#21d6ccd670d1f2e34c9cce68c6e32c442f46806b"
integrity sha512-HJ73kL45OGRmIkPhBNFt31I1SGUvdZND+LCH21+qpq3pPlFpJG8GORyXpP+2ze8PbnITNLzwe5AwUrpyuRswdQ==
@@ -9120,7 +9148,7 @@ ember-decorators-polyfill@^1.1.5:
ember-cli-version-checker "^3.1.3"
ember-compatibility-helpers "^1.2.0"
-ember-destroyable-polyfill@^2.0.2, ember-destroyable-polyfill@^2.0.3:
+ember-destroyable-polyfill@^2.0.1, ember-destroyable-polyfill@^2.0.2, ember-destroyable-polyfill@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/ember-destroyable-polyfill/-/ember-destroyable-polyfill-2.0.3.tgz#1673ed66609a82268ef270a7d917ebd3647f11e1"
integrity sha512-TovtNqCumzyAiW0/OisSkkVK93xnVF4NRU6+FN0ubpfwEOpRrmM2RqDwXI6YAChCgSHON1cz0DfQStpA1Gjuuw==
@@ -12264,9 +12292,9 @@ heimdalljs@^0.3.0:
rsvp "~3.2.1"
highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@~10.5.0:
- version "10.6.0"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.6.0.tgz#0073aa71d566906965ba6e1b7be7b2682f5e18b6"
- integrity sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ==
+ version "10.7.1"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.1.tgz#a8ec4152db24ea630c90927d6cae2a45f8ecb955"
+ integrity sha512-S6G97tHGqJ/U8DsXcEdnACbirtbx58Bx9CzIVeYli8OuswCfYI/LsXH2EiGcoGio1KAC3x4mmUwulOllJ2ZyRA==
hmac-drbg@^1.0.1:
version "1.0.1"
@@ -12316,6 +12344,13 @@ hosted-git-info@^3.0.2, hosted-git-info@^3.0.6:
dependencies:
lru-cache "^6.0.0"
+hosted-git-info@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.1.tgz#710ef5452ea429a844abc33c981056e7371edab7"
+ integrity sha512-eT7NrxAsppPRQEBSwKSosReE+v8OzABwEScQYk5d4uxaEPlzxTIku7LINXtBGalthkLhJnq5lBI89PfK43zAKg==
+ dependencies:
+ lru-cache "^6.0.0"
+
html-encoding-sniffer@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
@@ -12600,9 +12635,9 @@ ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8:
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
immer@8.0.1, immer@^8.0.1:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
- integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
+ version "8.0.4"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a"
+ integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ==
import-cwd@^2.0.0:
version "2.1.0"
@@ -12644,7 +12679,7 @@ import-lazy@^4.0.0:
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
-imurmurhash@^0.1.4:
+imurmurhash@*, imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -14327,6 +14362,11 @@ lodash._baseflatten@^3.0.0:
lodash.isarguments "^3.0.0"
lodash.isarray "^3.0.0"
+lodash._baseindexof@*:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
+ integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
+
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -14335,11 +14375,16 @@ lodash._baseuniq@~4.6.0:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
-lodash._bindcallback@^3.0.0:
+lodash._bindcallback@*, lodash._bindcallback@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
+lodash._cacheindexof@*:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
+ integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
+
lodash._createassigner@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11"
@@ -14349,12 +14394,19 @@ lodash._createassigner@^3.0.0:
lodash._isiterateecall "^3.0.0"
lodash.restparam "^3.0.0"
+lodash._createcache@*:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
+ integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
+ dependencies:
+ lodash._getnative "^3.0.0"
+
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
-lodash._getnative@^3.0.0:
+lodash._getnative@*, lodash._getnative@^3.0.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
@@ -14567,7 +14619,7 @@ lodash.pick@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=
-lodash.restparam@^3.0.0:
+lodash.restparam@*, lodash.restparam@^3.0.0:
version "3.6.1"
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
@@ -15785,9 +15837,9 @@ node-modules-regexp@^1.0.0:
integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=
node-notifier@^5.0.1, node-notifier@^8.0.1:
- version "8.0.1"
- resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1"
- integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA==
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5"
+ integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==
dependencies:
growly "^1.3.0"
is-wsl "^2.2.0"
@@ -15995,12 +16047,12 @@ npm-package-arg@^7.0.0:
validate-npm-package-name "^3.0.0"
npm-package-arg@^8.0.0:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.0.tgz#b5f6319418c3246a1c38e1a8fbaa06231bc5308f"
- integrity sha512-/ep6QDxBkm9HvOhOg0heitSd7JHA1U7y1qhhlRlteYYAi9Pdb/ZV7FW5aHpkrpM8+P+4p/jjR8zCyKPBMBjSig==
+ version "8.1.2"
+ resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.2.tgz#b868016ae7de5619e729993fbd8d11dc3c52ab62"
+ integrity sha512-6Eem455JsSMJY6Kpd3EyWE+n5hC+g9bSyHr9K9U2zqZb7+02+hObQ2c0+8iDk/mNF+8r1MhY44WypKJAkySIYA==
dependencies:
- hosted-git-info "^3.0.6"
- semver "^7.0.0"
+ hosted-git-info "^4.0.1"
+ semver "^7.3.4"
validate-npm-package-name "^3.0.0"
npm-package-arg@^8.1.0:
@@ -17671,9 +17723,9 @@ query-string@^5.0.1:
strict-uri-encode "^1.0.0"
query-string@^6.8.2:
- version "6.14.0"
- resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.0.tgz#0b7b7ca326f5facf10dd2d45d26645cd287f8c92"
- integrity sha512-In3o+lUxlgejoVJgwEdYtdxrmlL0cQWJXj0+kkI7RWVo7hg5AhFtybeKlC9Dpgbr8eOC4ydpEh8017WwyfzqVQ==
+ version "6.14.1"
+ resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a"
+ integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==
dependencies:
decode-uri-component "^0.2.0"
filter-obj "^1.1.0"
@@ -19519,9 +19571,9 @@ socks-proxy-agent@^5.0.0:
socks "^2.3.3"
socks@^2.3.3:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/socks/-/socks-2.5.1.tgz#7720640b6b5ec9a07d556419203baa3f0596df5f"
- integrity sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ==
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.0.tgz#6b984928461d39871b3666754b9000ecf39dfac2"
+ integrity sha512-mNmr9owlinMplev0Wd7UHFlqI4ofnBnNzFuzrm63PPaHgbkqCFe4T5LzwKmtQ/f2tX0NTpcdVLyD/FHxFBstYw==
dependencies:
ip "^1.1.5"
smart-buffer "^4.1.0"