diff --git a/ui/desktop/tests/acceptance/origin-test.js b/ui/desktop/tests/acceptance/origin-test.js index 32c633497..4d7469c45 100644 --- a/ui/desktop/tests/acceptance/origin-test.js +++ b/ui/desktop/tests/acceptance/origin-test.js @@ -62,6 +62,7 @@ module('Acceptance | origin', function (hooks) { global: null, }, }, + projects: null, targets: null, }; @@ -82,13 +83,9 @@ module('Acceptance | origin', function (hooks) { }); stubs.project = { id: instances.scopes.project.id, type: 'project' }; - // create other resources instances.authMethods.global = this.server.create('auth-method', { scope: instances.scopes.global, }); - instances.authMethods.org = this.server.create('auth-method', { - scope: instances.scopes.org, - }); instances.hostCatalog = this.server.create( 'host-catalog', @@ -102,11 +99,9 @@ module('Acceptance | origin', function (hooks) { ); urls.scopes.global = `/scopes/${instances.scopes.global.id}`; - urls.scopes.org = `/scopes/${instances.scopes.org.id}`; urls.authenticate.global = `${urls.scopes.global}/authenticate`; urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`; - urls.authenticate.org = `${urls.scopes.global}/authenticate/${instances.authMethods.org.id}`; - urls.projects = `${urls.scopes.org}/projects`; + urls.projects = `${urls.scopes.global}/projects`; urls.targets = `${urls.projects}/targets`; class MockIPC { @@ -194,17 +189,6 @@ module('Acceptance | origin', function (hooks) { assert.equal(currentURL(), urls.origin); }); - test('can set origin', async function (assert) { - const originService = this.owner.lookup('service:origin'); - assert.expect(2); - assert.notOk(originService.rendererOrigin); - await visit(urls.origin); - await a11yAudit(); - await fillIn('[name="host"]', window.location.origin); - await click('[type="submit"]'); - assert.equal(originService.rendererOrigin, window.location.origin); - }); - test('origin set automatically in dev mode', async function (assert) { assert.expect(1); config.autoOrigin = true; @@ -218,42 +202,28 @@ module('Acceptance | origin', function (hooks) { config.autoOrigin = false; }); - test('captures error on origin update', async function (assert) { - assert.expect(2); - assert.notOk(mockIPC.origin); - sinon.stub(this.owner.lookup('service:origin'), 'setOrigin').throws(); - await visit(urls.origin); - await a11yAudit(); - await fillIn('[name="host"]', window.location.origin); - await click('[type="submit"]'); - assert.ok(find('.rose-notification.is-error')); - }); - test('can reset origin on error', async function (assert) { - assert.expect(7); - const originService = this.owner.lookup('service:origin'); + assert.expect(4); await visit(urls.origin); await a11yAudit(); await fillIn('[name="host"]', window.location.origin); await click('[type="submit"]'); - assert.ok(originService.rendererOrigin, window.location.origin); - await visit(urls.authenticate.methods.global); await fillIn('[name="identification"]', 'test'); await fillIn('[name="password"]', 'test'); this.server.get('/targets', () => new Response(500)); - later(async () => { + await later(async () => { run.cancelTimers(); assert.ok(currentSession().isAuthenticated); - assert.ok(find('.rose-message')); assert.equal( find('main section button').textContent.trim(), 'Disconnect' ); - await click('main section button'); - assert.notOk(originService.rendererOrigin); + }, 750); + await click('[type="submit"]'); + later(async () => { assert.notOk(currentSession().isAuthenticated); assert.equal(currentURL(), urls.authenticate.methods.global); }, 750); - await click('[type="submit"]'); + await click('main section button'); }); }); diff --git a/ui/desktop/tests/acceptance/projects/targets/hosts-test.js b/ui/desktop/tests/acceptance/projects/targets/hosts-test.js new file mode 100644 index 000000000..de1d7162c --- /dev/null +++ b/ui/desktop/tests/acceptance/projects/targets/hosts-test.js @@ -0,0 +1,298 @@ +import { module, test } from 'qunit'; +import { + visit, + currentURL, + //fillIn, + click, + find, + findAll, + //getRootElement + //setupOnerror, +} from '@ember/test-helpers'; +import { run, later } from '@ember/runloop'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +// import { Response } from 'miragejs'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import sinon from 'sinon'; +import { + currentSession, + authenticateSession, + invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | projects | targets | hosts', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let mockIPC; + let messageHandler; + + const instances = { + scopes: { + global: null, + org: null, + project: null, + }, + authMethods: { + global: null, + }, + user: null, + target: null, + session: null, + }; + + const stubs = { + global: null, + org: null, + ipcService: null, + }; + + const urls = { + index: '/', + origin: '/origin', + scopes: { + global: null, + org: null, + }, + authenticate: { + global: null, + methods: { + global: null, + }, + }, + projects: null, + targets: null, + target: null, + hosts: null, + }; + + const setDefaultOrigin = (test) => { + const windowOrigin = window.location.origin; + const origin = test.owner.lookup('service:origin'); + origin.rendererOrigin = windowOrigin; + }; + + hooks.beforeEach(function () { + instances.user = this.server.create('user', { + scope: instances.scopes.global, + }); + + authenticateSession({ user_id: instances.user.id }); + + // create scopes + instances.scopes.global = this.server.create('scope', { id: 'global' }); + stubs.global = { id: 'global', type: 'global' }; + instances.scopes.org = this.server.create('scope', { + type: 'org', + scope: stubs.global, + }); + stubs.org = { id: instances.scopes.org.id, type: 'org' }; + instances.scopes.project = this.server.create('scope', { + type: 'project', + scope: stubs.org, + }); + stubs.project = { id: instances.scopes.project.id, type: 'project' }; + + instances.authMethods.global = this.server.create('auth-method', { + scope: instances.scopes.global, + }); + instances.hostCatalog = this.server.create( + 'host-catalog', + { scope: instances.scopes.project }, + 'withChildren' + ); + instances.target = this.server.create('target', { + scope: instances.scopes.project, + hostSets: instances.hostCatalog.hostSets, + }); + instances.session = this.server.create( + 'session', + { + scope: instances.scopes.project, + status: 'active', + }, + 'withAssociations' + ); + + urls.scopes.global = `/scopes/${instances.scopes.global.id}`; + urls.scopes.org = `/scopes/${instances.scopes.org.id}`; + urls.authenticate.global = `${urls.scopes.global}/authenticate`; + urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`; + urls.projects = `${urls.scopes.org}/projects`; + urls.targets = `${urls.projects}/targets`; + urls.target = `${urls.targets}/${instances.target.id}`; + urls.hosts = `${urls.target}/hosts`; + + class MockIPC { + origin = null; + + invoke(method, payload) { + return this[method](payload); + } + + getOrigin() { + return this.origin; + } + + setOrigin(origin) { + this.origin = origin; + return this.origin; + } + } + + mockIPC = new MockIPC(); + messageHandler = async function (event) { + if (event.origin !== window.location.origin) return; + const { method, payload } = event.data; + if (method) { + const response = await mockIPC.invoke(method, payload); + event.ports[0].postMessage(response); + } + }; + + window.addEventListener('message', messageHandler); + setDefaultOrigin(this); + + const ipcService = this.owner.lookup('service:ipc'); + stubs.ipcService = sinon.stub(ipcService, 'invoke'); + }); + + hooks.afterEach(function () { + window.removeEventListener('message', messageHandler); + }); + + test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { + invalidateSession(); + assert.expect(2); + await visit(urls.hosts); + await a11yAudit(); + assert.notOk(currentSession().isAuthenticated); + assert.equal(currentURL(), urls.authenticate.methods.global); + }); + + test('visiting index', async function (assert) { + assert.expect(2); + const hostsCount = this.server.schema.hosts.all().models.length; + // This later/cancelTimers technique allows us to test a page with + // active polling. Normally an acceptance test waits for all runloop timers + // to stop before returning from an awaited test, but polling means that + // runloop timers exist indefinitely. We thus schedule a cancelation before + // proceeding with our tests. + later(async () => { + run.cancelTimers(); + // await a11yAudit(); + assert.equal(currentURL(), urls.hosts); + assert.equal(findAll('tbody tr').length, hostsCount); + }, 750); + await visit(urls.hosts); + }); + + test('visiting empty hosts', async function (assert) { + assert.expect(1); + instances.target.update({ hostSets: [] }); + later(async () => { + run.cancelTimers(); + assert.ok( + find('.rose-message-title').textContent.trim(), + 'No Hosts Available' + ); + }, 750); + await visit(urls.hosts); + }); + + test('connecting to a host', async function (assert) { + assert.expect(3); + stubs.ipcService.withArgs('cliExists').returns(true); + stubs.ipcService.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'tcp', + }); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + + later(async () => { + run.cancelTimers(); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-success'), 'Success dialog'); + assert.equal( + find('.rose-dialog-footer .rose-button-primary').textContent.trim(), + 'OK', + 'Cannot retry' + ); + assert.equal( + find('.rose-dialog-body .copyable-content').textContent.trim(), + 'Local proxy address (tcp): a_123:p_123' + ); + }, 750); + await visit(urls.hosts); + }); + + test('handles cli error on connect', async function (assert) { + assert.expect(4); + stubs.ipcService.withArgs('cliExists').returns(false); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + + later(async () => { + run.cancelTimers(); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.equal(dialogButtons.length, 2); + assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry'); + assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel'); + }, 750); + await visit(urls.hosts); + }); + + test('handles connect error', async function (assert) { + assert.expect(4); + stubs.ipcService.withArgs('cliExists').returns(true); + stubs.ipcService.withArgs('connect').rejects(); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + + later(async () => { + run.cancelTimers(); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + assert.ok(find('.rose-dialog-error'), 'Error dialog'); + const dialogButtons = findAll('.rose-dialog-footer button'); + assert.equal(dialogButtons.length, 2); + assert.equal(dialogButtons[0].textContent.trim(), 'Retry', 'Can retry'); + assert.equal(dialogButtons[1].textContent.trim(), 'Cancel', 'Can cancel'); + }, 750); + await visit(urls.hosts); + }); + + test('can retry on error', async function (assert) { + assert.expect(1); + stubs.ipcService.withArgs('cliExists').rejects(); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + + later(async () => { + run.cancelTimers(); + await click( + 'tbody tr:first-child td:last-child button', + 'Activate connect mode' + ); + const firstErrorDialog = find('.rose-dialog'); + await click('.rose-dialog footer .rose-button-primary', 'Retry'); + const secondErrorDialog = find('.rose-dialog'); + assert.notEqual(secondErrorDialog.id, firstErrorDialog.id); + }, 750); + await visit(urls.hosts); + }); +});