Optimise Desktop API queries (#1707)

* fix: 🐛 increase auth form width, which was too narrow

* chore: 🤖 optimize targets route queries

* chore: 🤖 optimize sessions route queries

* chore: 🤖 disable polling in sessions route

* chore: 🤖 disable polling in targets route

* refactor: 💡 update tests for new queries and removing timing
This commit is contained in:
Randall Morey authored and GitHub committed 2023-06-07 18:28:33 -07:00
1 parent 547d44f1bd
commit 863c080196
8 files changed
+335 -389

No files matched your search

+10 -4
View File
@@ -26,12 +26,18 @@ export default factory.extend({
withAssociations: trait({ withAssociations: trait({
afterCreate(record, server) { afterCreate(record, server) {
const scopeId = record.scopeId; const scopeId = record.scopeId;
if (record.scope.type === 'project') {
const orgScopeId = record.scope?.scope.id; // Assign a user to this session if not already assigned.
const user = server.schema.users.where({ scopeId: orgScopeId }) if (record.scope && !record.user) {
.models[0]; const scopeId =
record.scope.type === 'project'
? record.scope?.scope.id
: record.scope.id;
const user = server.schema.users.where({ scopeId }).models[0];
if (user) record.update({ user }); if (user) record.update({ user });
} }
// Assign target and host set to this session, if not already assigned.
const target = server.schema.targets.where({ scopeId }).models[0]; const target = server.schema.targets.where({ scopeId }).models[0];
if (target) { if (target) {
record.update({ target }); record.update({ target });
+3
View File
@@ -102,6 +102,9 @@ export default function (server) {
server.create('target', { scope, address: '0.0.0.0' }); server.create('target', { scope, address: '0.0.0.0' });
// Sessions have target data. Create it after targets. // Sessions have target data. Create it after targets.
server.createList('session', 4, { scope }, 'withAssociations'); server.createList('session', 4, { scope }, 'withAssociations');
// Also create sessions for "authenticateduser" for Desktop,
// which filters user_id by the currently authenticated user.
server.createList('session', 1, { scope, user }, 'withAssociations');
// Create IAM resources for project // Create IAM resources for project
server.createList('group', 3, { scope }); server.createList('group', 3, { scope });
server.createList('role', 3, { scope }); server.createList('role', 3, { scope });
@@ -6,12 +6,8 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import runEvery from 'ember-pollster/decorators/route/run-every';
import { notifySuccess, notifyError } from 'core/decorators/notify'; import { notifySuccess, notifyError } from 'core/decorators/notify';
import { resourceFilter } from 'core/decorators/resource-filter'; import { resourceFilter } from 'core/decorators/resource-filter';
import config from '../../../../config/environment';
const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds;
export default class ScopesScopeProjectsSessionsRoute extends Route { export default class ScopesScopeProjectsSessionsRoute extends Route {
// =services // =services
@@ -47,32 +43,42 @@ export default class ScopesScopeProjectsSessionsRoute extends Route {
} }
/** /**
* Loads all sessions under the current scope and encapsulates them into * Loads all sessions under current scope for the current user.
* an array of objects filtering to current user *
* @return {Promise{SessionModel[]}} * NOTE: previously, sessions were filtered only with API filter queries.
* In an effort to offload processing from the controller, sessions
* are now filtered on the client by projects and status,
* while user_id filtering remains server side.
*
* @return {Promise{[SessionModel]}}
*/ */
async model() { async model() {
const { status } = this;
const { id: scope_id } = this.modelFor('scopes.scope'); const { id: scope_id } = this.modelFor('scopes.scope');
const { user_id } = this.session.data.authenticated; const queryOptions = {
const projects = this.project || [];
const filters = {
user_id,
status,
scope_id: projects.map(({ id }) => id),
};
const options = {
recursive: true,
scope_id, scope_id,
include_terminated: filters.status?.includes('terminated'), recursive: true,
include_terminated: this.status?.includes('terminated'),
}; };
await this.store.query('target', { recursive: true, scope_id });
return await this.resourceFilterStore.queryBy('session', filters, options);
}
@runEvery(POLL_TIMEOUT_SECONDS * 1000) // Recursively query sessions within the current scope for the current user
poller() { let sessions = await this.resourceFilterStore.queryBy(
return this.refresh(); 'session',
{ user_id: this.session.data.authenticated.user_id },
queryOptions
);
// If project filters are selected, filter sessions by the selected projects
if (this.project?.length) {
const projectIDs = this.project.map((p) => p?.id);
sessions = sessions.filter((s) => projectIDs.includes(s?.scopeID));
}
// If status filters are selected, filter sessions by the selected statuses
if (this.status?.length) {
sessions = sessions.filter((s) => this.status.includes(s?.status));
}
return sessions;
} }
// =actions // =actions
@@ -7,21 +7,18 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { resourceFilter } from 'core/decorators/resource-filter'; import { resourceFilter } from 'core/decorators/resource-filter';
import runEvery from 'ember-pollster/decorators/route/run-every';
import config from '../../../../config/environment';
const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds;
export default class ScopesScopeProjectsTargetsRoute extends Route { export default class ScopesScopeProjectsTargetsRoute extends Route {
// =services // =services
@service store; @service can;
@service ipc;
@service session;
@service clusterUrl; @service clusterUrl;
@service confirm; @service confirm;
@service ipc;
@service resourceFilterStore; @service resourceFilterStore;
@service router; @service router;
@service session;
@service store;
// =attributes // =attributes
@@ -43,36 +40,30 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
/** /**
* Loads all targets under current scope. * Loads all targets under current scope.
*
* NOTE: previously, targets were filtered with API filter queries.
* In an effort to offload processing from the controller, targets
* are now filtered on the client by projects and authorized_actions.
*
* @return {Promise{[TargetModel]}} * @return {Promise{[TargetModel]}}
*/ */
async model() { async model() {
const { id: scope_id } = this.modelFor('scopes.scope'); const { id: scope_id } = this.modelFor('scopes.scope');
const { user_id } = this.session.data.authenticated; const queryOptions = { scope_id, recursive: true };
const projects = this.project || [];
await this.resourceFilterStore.queryBy(
'session',
{ user_id },
{
recursive: true,
scope_id,
}
);
return this.resourceFilterStore.queryBy(
'target',
{
scope_id: projects.map(({ id }) => id),
authorized_actions: [{ contains: 'authorize-session' }],
},
{
recursive: true,
scope_id,
}
);
}
@runEvery(POLL_TIMEOUT_SECONDS * 1000) // Recursively query all targets within the current scope
poller() { let targets = await this.store.query('target', queryOptions);
return this.refresh();
// Filter out targets to which users do not have the connect ability
targets = targets.filter((t) => this.can.can('connect target', t));
// If project filters are selected, filter targets by the selected projects
if (this.project?.length) {
const projectIDs = this.project.map((p) => p?.id);
targets = targets.filter((t) => projectIDs.includes(t?.scopeID));
}
return targets;
} }
// =actions // =actions
@@ -5,7 +5,6 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { visit, currentURL, click, find, findAll } from '@ember/test-helpers'; import { visit, currentURL, click, find, findAll } from '@ember/test-helpers';
import { later, _cancelTimers } from '@ember/runloop';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit'; import a11yAudit from 'ember-a11y-testing/test-support/audit';
@@ -169,31 +168,19 @@ module('Acceptance | projects | targets | hosts', function (hooks) {
test('visiting index', async function (assert) { test('visiting index', async function (assert) {
assert.expect(2); assert.expect(2);
const hostsCount = this.server.schema.hosts.all().models.length; 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 () => {
_cancelTimers();
// await a11yAudit();
assert.strictEqual(currentURL(), urls.hosts);
assert.strictEqual(findAll('tbody tr').length, hostsCount);
}, 750);
await visit(urls.hosts); await visit(urls.hosts);
assert.strictEqual(currentURL(), urls.hosts);
assert.strictEqual(findAll('tbody tr').length, hostsCount);
}); });
test('visiting empty hosts', async function (assert) { test('visiting empty hosts', async function (assert) {
assert.expect(1); assert.expect(1);
instances.target.update({ hostSets: [] }); instances.target.update({ hostSets: [] });
later(async () => {
_cancelTimers();
assert.ok(
find('.rose-message-title').textContent.trim(),
'No Hosts Available'
);
}, 750);
await visit(urls.hosts); await visit(urls.hosts);
assert.ok(
find('.rose-message-title').textContent.trim(),
'No Hosts Available'
);
}); });
test('connecting to a host', async function (assert) { test('connecting to a host', async function (assert) {
@@ -208,24 +195,21 @@ module('Acceptance | projects | targets | hosts', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_cancelTimers();
await click(
'tbody tr:first-child td:last-child button',
'Activate connect mode'
);
assert.ok(find('.dialog-detail'), 'Success dialog');
assert.strictEqual(
find('.rose-dialog-footer .rose-button-secondary').textContent.trim(),
'Close',
'Cannot retry'
);
assert.strictEqual(
find('.rose-dialog-body .copyable-content').textContent.trim(),
'a_123:p_123'
);
}, 750);
await visit(urls.hosts); await visit(urls.hosts);
await click(
'tbody tr:first-child td:last-child button',
'Activate connect mode'
);
assert.ok(find('.dialog-detail'), 'Success dialog');
assert.strictEqual(
find('.rose-dialog-footer .rose-button-secondary').textContent.trim(),
'Close',
'Cannot retry'
);
assert.strictEqual(
find('.rose-dialog-body .copyable-content').textContent.trim(),
'a_123:p_123'
);
}); });
test('handles cli error on connect', async function (assert) { test('handles cli error on connect', async function (assert) {
@@ -234,27 +218,24 @@ module('Acceptance | projects | targets | hosts', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}, 750);
await visit(urls.hosts); await visit(urls.hosts);
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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}); });
test('handles connect error', async function (assert) { test('handles connect error', async function (assert) {
@@ -264,27 +245,24 @@ module('Acceptance | projects | targets | hosts', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}, 750);
await visit(urls.hosts); await visit(urls.hosts);
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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}); });
test('can retry on error', async function (assert) { test('can retry on error', async function (assert) {
@@ -293,17 +271,14 @@ module('Acceptance | projects | targets | hosts', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_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); await visit(urls.hosts);
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);
}); });
}); });
@@ -5,7 +5,6 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { visit, currentURL, click, find, findAll } from '@ember/test-helpers'; import { visit, currentURL, click, find, findAll } from '@ember/test-helpers';
import { later, _cancelTimers } from '@ember/runloop';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { Response } from 'miragejs'; import { Response } from 'miragejs';
@@ -173,107 +172,140 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
}); });
test('visiting index', async function (assert) { test('visiting index', async function (assert) {
assert.expect(2); assert.expect(1);
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 () => {
_cancelTimers();
// await a11yAudit();
assert.strictEqual(currentURL(), urls.sessions);
assert.strictEqual(findAll('tbody tr').length, sessionsCount);
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
assert.strictEqual(currentURL(), urls.sessions);
}); });
test('visiting index redirects to sessions', async function (assert) { test('visiting index redirects to sessions', async function (assert) {
assert.expect(1); assert.expect(1);
later(async () => {
_cancelTimers();
// await a11yAudit();
assert.strictEqual(currentURL(), urls.sessions);
}, 750);
await visit(urls.target); await visit(urls.target);
assert.strictEqual(currentURL(), urls.sessions);
}); });
test('visiting empty sessions', async function (assert) { test('visiting empty sessions', async function (assert) {
assert.expect(1); assert.expect(1);
this.server.get('/sessions', () => new Response(200)); this.server.get('/sessions', () => new Response(200));
later(async () => {
_cancelTimers();
assert.ok(
find('.rose-message-title').textContent.trim(),
'No Sessions Available'
);
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
assert.ok(
find('.rose-message-title').textContent.trim(),
'No Sessions Available'
);
}); });
test('can identify target with active sessions', async function (assert) { test('can identify target with active sessions', async function (assert) {
assert.expect(1); assert.expect(1);
later(async () => {
_cancelTimers(); stubs.ipcService.withArgs('cliExists').returns(true);
assert.ok(find('.rose-layout-page-header .hds-badge--color-success')); stubs.ipcService.withArgs('connect').returns({
}, 750); session_id: instances.session.id,
address: 'a_123',
port: 'p_123',
protocol: 'tcp',
});
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.sessions); await visit(urls.sessions);
await click('.rose-layout-page-actions button', 'Activate connect mode');
await click('.rose-dialog-footer .rose-button-secondary');
assert.ok(find('.rose-layout-page-header .hds-badge--color-success'));
}); });
test('can identify target with pending sessions', async function (assert) { test('can identify target with pending sessions', async function (assert) {
assert.expect(1); assert.expect(1);
instances.session.update({ status: 'pending' }); instances.session.update({ status: 'pending' });
later(async () => { stubs.ipcService.withArgs('cliExists').returns(true);
_cancelTimers(); stubs.ipcService.withArgs('connect').returns({
assert.ok(find('.rose-layout-page-header .hds-badge--color-success')); session_id: instances.session.id,
}, 750); address: 'a_123',
port: 'p_123',
protocol: 'tcp',
});
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.sessions); await visit(urls.sessions);
await click('.rose-layout-page-actions button', 'Activate connect mode');
await click('.rose-dialog-footer .rose-button-secondary');
assert.ok(find('.rose-layout-page-header .hds-badge--color-success'));
}); });
test('cannot identify target with terminated sessions', async function (assert) { test('cannot identify target with terminated sessions', async function (assert) {
assert.expect(1); assert.expect(1);
instances.session.update({ status: 'terminated' }); instances.session.update({ status: 'terminated' });
later(async () => {
_cancelTimers();
assert.notOk(find('.rose-layout-page-header .hds-badge--color-success'));
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
assert.notOk(find('.rose-layout-page-header .hds-badge--color-success'));
}); });
test('cancelling a session', async function (assert) { test('cancelling a session', async function (assert) {
assert.expect(2); assert.expect(2);
const sessionsCount = this.server.schema.sessions.all().models.length;
stubs.ipcService.withArgs('cancel'); stubs.ipcService.withArgs('cliExists').returns(true);
later(async () => { stubs.ipcService.withArgs('connect').returns({
_cancelTimers(); session_id: instances.session.id,
await click('tbody tr:first-child td:last-child button'); address: 'a_123',
assert.ok(find('[role="alert"].is-success')); port: 'p_123',
assert.strictEqual(findAll('tbody tr').length, sessionsCount - 1); protocol: 'tcp',
}, 750); });
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
await visit(urls.sessions); await visit(urls.sessions);
await click('.rose-layout-page-actions button', 'Activate connect mode');
await click('.rose-dialog-footer .rose-button-secondary');
const sessionsCount = findAll(
'tbody tr:first-child td:last-child button'
).length;
await click('tbody tr:first-child td:last-child button');
assert.ok(find('[role="alert"].is-success'));
assert.strictEqual(findAll('tbody tr').length, sessionsCount - 1);
}); });
test('cancelling a session with error shows notification', async function (assert) { test('cancelling a session with error shows notification', async function (assert) {
assert.expect(1); assert.expect(1);
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;
this.server.post('/sessions/:id_method', () => new Response(400)); this.server.post('/sessions/:id_method', () => new Response(400));
later(async () => {
_cancelTimers();
await click('tbody tr:first-child td:last-child button');
assert.ok(find('[role="alert"].is-error'));
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
await click('.rose-layout-page-actions button', 'Activate connect mode');
await click('.rose-dialog-footer .rose-button-secondary');
await click('tbody tr:first-child td:last-child button');
assert.ok(find('[role="alert"].is-error'));
}); });
test('cancelling a session with ipc error shows notification', async function (assert) { test('cancelling a session with ipc error shows notification', async function (assert) {
assert.expect(1); assert.expect(1);
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;
stubs.ipcService.withArgs('stop').throws(); stubs.ipcService.withArgs('stop').throws();
later(async () => {
_cancelTimers();
await click('tbody tr:first-child td:last-child button');
assert.ok(find('[role="alert"].is-error'));
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
await click('.rose-layout-page-actions button', 'Activate connect mode');
await click('.rose-dialog-footer .rose-button-secondary');
await click('tbody tr:first-child td:last-child button');
assert.ok(find('[role="alert"].is-error'));
}); });
test('connecting to a target', async function (assert) { test('connecting to a target', async function (assert) {
@@ -288,21 +320,18 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_cancelTimers();
await click('.rose-layout-page-actions button', 'Activate connect mode');
assert.ok(find('.dialog-detail'), 'Success dialog');
assert.strictEqual(
find('.rose-dialog-footer .rose-button-secondary').textContent.trim(),
'Close',
'Cannot retry'
);
assert.strictEqual(
find('.rose-dialog-body .copyable-content').textContent.trim(),
'a_123:p_123'
);
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
await click('.rose-layout-page-actions button', 'Activate connect mode');
assert.ok(find('.dialog-detail'), 'Success dialog');
assert.strictEqual(
find('.rose-dialog-footer .rose-button-secondary').textContent.trim(),
'Close',
'Cannot retry'
);
assert.strictEqual(
find('.rose-dialog-body .copyable-content').textContent.trim(),
'a_123:p_123'
);
}); });
test('connect details in session', async function (assert) { test('connect details in session', async function (assert) {
@@ -317,20 +346,17 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_cancelTimers();
instances.session.update('status', 'active');
await click('.rose-layout-page-actions button', 'Activate connect mode');
assert.ok(find('.dialog-detail'), 'Success dialog');
await click('.rose-dialog-dismiss');
assert.strictEqual(
find(
'tbody tr:first-child td:nth-child(2) .copyable-content'
).textContent.trim(),
'a_123:p_123'
);
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
instances.session.update('status', 'active');
await click('.rose-layout-page-actions button', 'Activate connect mode');
assert.ok(find('.dialog-detail'), 'Success dialog');
await click('.rose-dialog-dismiss');
assert.strictEqual(
find(
'tbody tr:first-child td:nth-child(2) .copyable-content'
).textContent.trim(),
'a_123:p_123'
);
}); });
test('handles cli error on connect', async function (assert) { test('handles cli error on connect', async function (assert) {
@@ -339,24 +365,21 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}); });
test('handles connect error', async function (assert) { test('handles connect error', async function (assert) {
@@ -366,24 +389,21 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}, 750);
await visit(urls.sessions); await visit(urls.sessions);
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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}); });
test('can retry on error', async function (assert) { test('can retry on error', async function (assert) {
@@ -392,14 +412,11 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_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); await visit(urls.sessions);
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);
}); });
}); });
+64 -106
View File
@@ -5,7 +5,6 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { visit, currentURL, click, find, findAll } from '@ember/test-helpers'; import { visit, currentURL, click, find, findAll } from '@ember/test-helpers';
import { later, _cancelTimers } from '@ember/runloop';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { Response } from 'miragejs'; import { Response } from 'miragejs';
@@ -173,28 +172,17 @@ module('Acceptance | scopes', function (hooks) {
test('visiting index', async function (assert) { test('visiting index', async function (assert) {
assert.expect(2); assert.expect(2);
const targetsCount = this.server.schema.targets.all().models.length; 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 () => {
_cancelTimers();
await a11yAudit();
assert.strictEqual(currentURL(), urls.targets);
assert.strictEqual(findAll('tbody tr').length, targetsCount);
}, 750);
await visit(urls.targets); await visit(urls.targets);
assert.strictEqual(currentURL(), urls.targets);
assert.strictEqual(findAll('tbody tr').length, targetsCount);
}); });
test('visiting global scope', async function (assert) { test('visiting global scope', async function (assert) {
assert.expect(1); assert.expect(1);
later(async () => {
_cancelTimers();
await a11yAudit();
assert.strictEqual(currentURL(), urls.globalTargets);
}, 750);
await visit(urls.scopes.global); await visit(urls.scopes.global);
await a11yAudit();
assert.strictEqual(currentURL(), urls.globalTargets);
}); });
// TODO: this probably shouldn't be the case, but was setup to enable // TODO: this probably shouldn't be the case, but was setup to enable
@@ -208,43 +196,28 @@ module('Acceptance | scopes', function (hooks) {
const response = id === 'global' ? new Response(404) : scope; const response = id === 'global' ? new Response(404) : scope;
return response; return response;
}); });
later(async () => {
_cancelTimers();
await a11yAudit();
assert.strictEqual(currentURL(), urls.globalTargets);
}, 750);
await visit(urls.scopes.global); await visit(urls.scopes.global);
await a11yAudit();
assert.strictEqual(currentURL(), urls.globalTargets);
}); });
test('visiting org scope', async function (assert) { test('visiting org scope', async function (assert) {
assert.expect(1); assert.expect(1);
later(async () => {
_cancelTimers();
await a11yAudit();
assert.strictEqual(currentURL(), urls.targets);
}, 750);
await visit(urls.scopes.org); await visit(urls.scopes.org);
await a11yAudit();
assert.strictEqual(currentURL(), urls.targets);
}); });
test('can navigate among org scopes via header navigation', async function (assert) { test('can navigate among org scopes via header navigation', async function (assert) {
assert.expect(3); assert.expect(3);
await later(async () => _cancelTimers(), 750);
await visit(urls.targets); await visit(urls.targets);
await later(
async () => assert.strictEqual(currentURL(), urls.targets),
750
);
await click('.rose-header-nav .rose-dropdown a:nth-of-type(2)'); await click('.rose-header-nav .rose-dropdown a:nth-of-type(2)');
await later( assert.strictEqual(currentURL(), urls.targets);
async () => assert.strictEqual(currentURL(), urls.org2Targets),
750
);
await click('.rose-header-nav .rose-dropdown a:nth-of-type(3)'); await click('.rose-header-nav .rose-dropdown a:nth-of-type(3)');
later( assert.strictEqual(currentURL(), urls.org2Targets);
async () => assert.strictEqual(currentURL(), urls.globalTargets),
750
);
await click('.rose-header-nav .rose-dropdown a:nth-of-type(1)'); await click('.rose-header-nav .rose-dropdown a:nth-of-type(1)');
assert.strictEqual(currentURL(), urls.globalTargets);
}); });
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
@@ -258,25 +231,19 @@ module('Acceptance | scopes', function (hooks) {
test('visiting a target', async function (assert) { test('visiting a target', async function (assert) {
assert.expect(1); assert.expect(1);
later(async () => {
_cancelTimers();
await click('tbody tr th a');
assert.strictEqual(currentURL(), urls.targetSessions);
}, 750);
await visit(urls.targets); await visit(urls.targets);
await click('tbody tr th a');
assert.strictEqual(currentURL(), urls.targetSessions);
}); });
test('visiting empty targets', async function (assert) { test('visiting empty targets', async function (assert) {
assert.expect(1); assert.expect(1);
this.server.get('/targets', () => new Response(200)); this.server.get('/targets', () => new Response(200));
later(async () => {
_cancelTimers();
assert.ok(
find('.rose-message-title').textContent.trim(),
'No Targets Available'
);
}, 750);
await visit(urls.targets); await visit(urls.targets);
assert.ok(
find('.rose-message-title').textContent.trim(),
'No Targets Available'
);
}); });
test('connecting to a target', async function (assert) { test('connecting to a target', async function (assert) {
@@ -291,21 +258,18 @@ module('Acceptance | scopes', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_cancelTimers();
await click(
'tbody tr:first-child td:last-child button',
'Activate connect mode'
);
assert.ok(find('.dialog-detail'), 'Success dialog');
assert.strictEqual(findAll('.rose-dialog-footer button').length, 1);
assert.strictEqual(
find('.rose-dialog-footer button').textContent.trim(),
'Close',
'Cannot retry'
);
}, 750);
await visit(urls.targets); await visit(urls.targets);
await click(
'tbody tr:first-child td:last-child button',
'Activate connect mode'
);
assert.ok(find('.dialog-detail'), 'Success dialog');
assert.strictEqual(findAll('.rose-dialog-footer button').length, 1);
assert.strictEqual(
find('.rose-dialog-footer button').textContent.trim(),
'Close',
'Cannot retry'
);
}); });
test('handles cli error on connect', async function (assert) { test('handles cli error on connect', async function (assert) {
@@ -314,27 +278,24 @@ module('Acceptance | scopes', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}, 750);
await visit(urls.scopes.global); await visit(urls.scopes.global);
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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}); });
test('handles connect error', async function (assert) { test('handles connect error', async function (assert) {
@@ -344,26 +305,23 @@ module('Acceptance | scopes', function (hooks) {
const confirmService = this.owner.lookup('service:confirm'); const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true; confirmService.enabled = true;
later(async () => {
_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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}, 750);
await visit(urls.targets); await visit(urls.targets);
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.strictEqual(dialogButtons.length, 2);
assert.strictEqual(
dialogButtons[0].textContent.trim(),
'Retry',
'Can retry'
);
assert.strictEqual(
dialogButtons[1].textContent.trim(),
'Cancel',
'Can cancel'
);
}); });
}); });
+1 -11
View File
@@ -5,7 +5,6 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { visit, currentURL } from '@ember/test-helpers'; import { visit, currentURL } from '@ember/test-helpers';
import { later, _cancelTimers } from '@ember/runloop';
import { setupApplicationTest } from 'ember-qunit'; import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit'; import a11yAudit from 'ember-a11y-testing/test-support/audit';
@@ -113,16 +112,7 @@ module('Acceptance | targets', function (hooks) {
test('visiting index', async function (assert) { test('visiting index', async function (assert) {
assert.expect(1); assert.expect(1);
// 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 cancellation before
// proceeding with our tests.
later(async () => {
_cancelTimers();
await a11yAudit();
assert.strictEqual(currentURL(), urls.targets);
}, 750);
await visit(urls.targets); await visit(urls.targets);
assert.strictEqual(currentURL(), urls.targets);
}); });
}); });