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:
1 parent
547d44f1bd
commit
863c080196
8 files changed
+335
-389
No files matched your search
@@ -26,12 +26,18 @@ export default factory.extend({
|
||||
withAssociations: trait({
|
||||
afterCreate(record, server) {
|
||||
const scopeId = record.scopeId;
|
||||
if (record.scope.type === 'project') {
|
||||
const orgScopeId = record.scope?.scope.id;
|
||||
const user = server.schema.users.where({ scopeId: orgScopeId })
|
||||
.models[0];
|
||||
|
||||
// Assign a user to this session if not already assigned.
|
||||
if (record.scope && !record.user) {
|
||||
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 });
|
||||
}
|
||||
|
||||
// Assign target and host set to this session, if not already assigned.
|
||||
const target = server.schema.targets.where({ scopeId }).models[0];
|
||||
if (target) {
|
||||
record.update({ target });
|
||||
|
||||
@@ -102,6 +102,9 @@ export default function (server) {
|
||||
server.create('target', { scope, address: '0.0.0.0' });
|
||||
// Sessions have target data. Create it after targets.
|
||||
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
|
||||
server.createList('group', 3, { scope });
|
||||
server.createList('role', 3, { scope });
|
||||
|
||||
@@ -6,12 +6,8 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import runEvery from 'ember-pollster/decorators/route/run-every';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import { resourceFilter } from 'core/decorators/resource-filter';
|
||||
import config from '../../../../config/environment';
|
||||
|
||||
const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds;
|
||||
|
||||
export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
// =services
|
||||
@@ -47,32 +43,42 @@ export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all sessions under the current scope and encapsulates them into
|
||||
* an array of objects filtering to current user
|
||||
* @return {Promise{SessionModel[]}}
|
||||
* Loads all sessions under current scope for the current user.
|
||||
*
|
||||
* 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() {
|
||||
const { status } = this;
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
const { user_id } = this.session.data.authenticated;
|
||||
const projects = this.project || [];
|
||||
const filters = {
|
||||
user_id,
|
||||
status,
|
||||
scope_id: projects.map(({ id }) => id),
|
||||
};
|
||||
const options = {
|
||||
recursive: true,
|
||||
const queryOptions = {
|
||||
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)
|
||||
poller() {
|
||||
return this.refresh();
|
||||
// Recursively query sessions within the current scope for the current user
|
||||
let sessions = await this.resourceFilterStore.queryBy(
|
||||
'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
|
||||
|
||||
@@ -7,21 +7,18 @@ import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
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 {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service ipc;
|
||||
@service session;
|
||||
@service can;
|
||||
@service clusterUrl;
|
||||
@service confirm;
|
||||
@service ipc;
|
||||
@service resourceFilterStore;
|
||||
@service router;
|
||||
@service session;
|
||||
@service store;
|
||||
|
||||
// =attributes
|
||||
|
||||
@@ -43,36 +40,30 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
|
||||
|
||||
/**
|
||||
* 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]}}
|
||||
*/
|
||||
async model() {
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
const { user_id } = this.session.data.authenticated;
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
const queryOptions = { scope_id, recursive: true };
|
||||
|
||||
@runEvery(POLL_TIMEOUT_SECONDS * 1000)
|
||||
poller() {
|
||||
return this.refresh();
|
||||
// Recursively query all targets within the current scope
|
||||
let targets = await this.store.query('target', queryOptions);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, find, findAll } from '@ember/test-helpers';
|
||||
import { later, _cancelTimers } from '@ember/runloop';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
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) {
|
||||
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 () => {
|
||||
_cancelTimers();
|
||||
// await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.hosts);
|
||||
assert.strictEqual(findAll('tbody tr').length, hostsCount);
|
||||
}, 750);
|
||||
await visit(urls.hosts);
|
||||
assert.strictEqual(currentURL(), urls.hosts);
|
||||
assert.strictEqual(findAll('tbody tr').length, hostsCount);
|
||||
});
|
||||
|
||||
test('visiting empty hosts', async function (assert) {
|
||||
assert.expect(1);
|
||||
instances.target.update({ hostSets: [] });
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
assert.ok(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'No Hosts Available'
|
||||
);
|
||||
}, 750);
|
||||
await visit(urls.hosts);
|
||||
assert.ok(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'No Hosts Available'
|
||||
);
|
||||
});
|
||||
|
||||
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');
|
||||
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 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) {
|
||||
@@ -234,27 +218,24 @@ module('Acceptance | projects | targets | hosts', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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) {
|
||||
@@ -264,27 +245,24 @@ module('Acceptance | projects | targets | hosts', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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) {
|
||||
@@ -293,17 +271,14 @@ module('Acceptance | projects | targets | hosts', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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 { visit, currentURL, click, find, findAll } from '@ember/test-helpers';
|
||||
import { later, _cancelTimers } from '@ember/runloop';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
@@ -173,107 +172,140 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
|
||||
});
|
||||
|
||||
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 () => {
|
||||
_cancelTimers();
|
||||
// await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.sessions);
|
||||
assert.strictEqual(findAll('tbody tr').length, sessionsCount);
|
||||
}, 750);
|
||||
assert.expect(1);
|
||||
await visit(urls.sessions);
|
||||
assert.strictEqual(currentURL(), urls.sessions);
|
||||
});
|
||||
|
||||
test('visiting index redirects to sessions', async function (assert) {
|
||||
assert.expect(1);
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
// await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.sessions);
|
||||
}, 750);
|
||||
await visit(urls.target);
|
||||
assert.strictEqual(currentURL(), urls.sessions);
|
||||
});
|
||||
|
||||
test('visiting empty sessions', async function (assert) {
|
||||
assert.expect(1);
|
||||
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);
|
||||
assert.ok(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'No Sessions Available'
|
||||
);
|
||||
});
|
||||
|
||||
test('can identify target with active sessions', async function (assert) {
|
||||
assert.expect(1);
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
assert.ok(find('.rose-layout-page-header .hds-badge--color-success'));
|
||||
}, 750);
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
assert.expect(1);
|
||||
|
||||
instances.session.update({ status: 'pending' });
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
assert.ok(find('.rose-layout-page-header .hds-badge--color-success'));
|
||||
}, 750);
|
||||
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;
|
||||
|
||||
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) {
|
||||
assert.expect(1);
|
||||
instances.session.update({ status: 'terminated' });
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
assert.notOk(find('.rose-layout-page-header .hds-badge--color-success'));
|
||||
}, 750);
|
||||
|
||||
await visit(urls.sessions);
|
||||
assert.notOk(find('.rose-layout-page-header .hds-badge--color-success'));
|
||||
});
|
||||
|
||||
test('cancelling a session', async function (assert) {
|
||||
assert.expect(2);
|
||||
const sessionsCount = this.server.schema.sessions.all().models.length;
|
||||
stubs.ipcService.withArgs('cancel');
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
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);
|
||||
}, 750);
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
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));
|
||||
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 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) {
|
||||
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();
|
||||
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 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) {
|
||||
@@ -288,21 +320,18 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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) {
|
||||
@@ -317,20 +346,17 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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);
|
||||
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) {
|
||||
@@ -339,24 +365,21 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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) {
|
||||
@@ -366,24 +389,21 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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) {
|
||||
@@ -392,14 +412,11 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click, find, findAll } from '@ember/test-helpers';
|
||||
import { later, _cancelTimers } from '@ember/runloop';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
@@ -173,28 +172,17 @@ module('Acceptance | scopes', function (hooks) {
|
||||
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 () => {
|
||||
_cancelTimers();
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.targets);
|
||||
assert.strictEqual(findAll('tbody tr').length, targetsCount);
|
||||
}, 750);
|
||||
|
||||
await visit(urls.targets);
|
||||
assert.strictEqual(currentURL(), urls.targets);
|
||||
assert.strictEqual(findAll('tbody tr').length, targetsCount);
|
||||
});
|
||||
|
||||
test('visiting global scope', async function (assert) {
|
||||
assert.expect(1);
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.globalTargets);
|
||||
}, 750);
|
||||
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
|
||||
@@ -208,43 +196,28 @@ module('Acceptance | scopes', function (hooks) {
|
||||
const response = id === 'global' ? new Response(404) : scope;
|
||||
return response;
|
||||
});
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.globalTargets);
|
||||
}, 750);
|
||||
await visit(urls.scopes.global);
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.globalTargets);
|
||||
});
|
||||
|
||||
test('visiting org scope', async function (assert) {
|
||||
assert.expect(1);
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.targets);
|
||||
}, 750);
|
||||
await visit(urls.scopes.org);
|
||||
await a11yAudit();
|
||||
assert.strictEqual(currentURL(), urls.targets);
|
||||
});
|
||||
|
||||
test('can navigate among org scopes via header navigation', async function (assert) {
|
||||
assert.expect(3);
|
||||
await later(async () => _cancelTimers(), 750);
|
||||
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 later(
|
||||
async () => assert.strictEqual(currentURL(), urls.org2Targets),
|
||||
750
|
||||
);
|
||||
assert.strictEqual(currentURL(), urls.targets);
|
||||
await click('.rose-header-nav .rose-dropdown a:nth-of-type(3)');
|
||||
later(
|
||||
async () => assert.strictEqual(currentURL(), urls.globalTargets),
|
||||
750
|
||||
);
|
||||
assert.strictEqual(currentURL(), urls.org2Targets);
|
||||
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) {
|
||||
@@ -258,25 +231,19 @@ module('Acceptance | scopes', function (hooks) {
|
||||
|
||||
test('visiting a target', async function (assert) {
|
||||
assert.expect(1);
|
||||
later(async () => {
|
||||
_cancelTimers();
|
||||
await click('tbody tr th a');
|
||||
assert.strictEqual(currentURL(), urls.targetSessions);
|
||||
}, 750);
|
||||
await visit(urls.targets);
|
||||
await click('tbody tr th a');
|
||||
assert.strictEqual(currentURL(), urls.targetSessions);
|
||||
});
|
||||
|
||||
test('visiting empty targets', async function (assert) {
|
||||
assert.expect(1);
|
||||
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);
|
||||
assert.ok(
|
||||
find('.rose-message-title').textContent.trim(),
|
||||
'No Targets Available'
|
||||
);
|
||||
});
|
||||
|
||||
test('connecting to a target', async function (assert) {
|
||||
@@ -291,21 +258,18 @@ module('Acceptance | scopes', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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) {
|
||||
@@ -314,27 +278,24 @@ module('Acceptance | scopes', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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) {
|
||||
@@ -344,26 +305,23 @@ module('Acceptance | scopes', function (hooks) {
|
||||
const confirmService = this.owner.lookup('service:confirm');
|
||||
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 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL } from '@ember/test-helpers';
|
||||
import { later, _cancelTimers } from '@ember/runloop';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import a11yAudit from 'ember-a11y-testing/test-support/audit';
|
||||
@@ -113,16 +112,7 @@ module('Acceptance | targets', function (hooks) {
|
||||
|
||||
test('visiting index', async function (assert) {
|
||||
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);
|
||||
assert.strictEqual(currentURL(), urls.targets);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user