Merge pull request #331 from hashicorp/oss/merge-main-863c080196567e98841555d3b7c6b91a84b5825c

[main] OSS to ENT merge of (863c080)
This commit is contained in:
hc-github-team-secure-boundary authored and GitHub committed 2023-06-07 21:47:14 -04:00
commit 80d72ddb82
8 files changed
+335 -389

No files matched your search

+10 -4
View File
@@ -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 });
+3
View File
@@ -106,6 +106,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);
});
});
+64 -106
View File
@@ -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'
);
});
});
+1 -11
View File
@@ -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);
});
});