diff --git a/addons/api/mirage/factories/target.js b/addons/api/mirage/factories/target.js index 5dd731e65..3b788dd09 100644 --- a/addons/api/mirage/factories/target.js +++ b/addons/api/mirage/factories/target.js @@ -95,4 +95,44 @@ export default factory.extend({ }); }, }), + + withTwoHosts: trait({ + afterCreate(target, server) { + const { scope } = target; + const hostCatalog = server.create('host-catalog', { scope }); + const hosts = server.createList('host', 2, { + scope, + hostCatalog, + type: hostCatalog.type, + }); + const hostSets = [ + server.create('host-set', { + scope, + hostCatalog, + hosts, + type: hostCatalog.type, + }), + ]; + target.update({ hostSets }); + }, + }), + + withOneHost: trait({ + afterCreate(target, server) { + const { scope } = target; + const hostCatalog = server.create('host-catalog', { scope }); + const hosts = [ + server.create('host', { scope, hostCatalog, type: hostCatalog.type }), + ]; + const hostSets = [ + server.create('host-set', { + scope, + hostCatalog, + hosts, + type: hostCatalog.type, + }), + ]; + target.update({ hostSets }); + }, + }), }); diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs index 58412bd92..5a1eaa5b7 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -115,7 +115,7 @@ {{#if (can 'read model' B.data)}} {{#if (can 'connect target' B.data)}} this.server.schema.targets.all().models.length; - invoke(method, payload) { - return this[method](payload); - } - - getClusterUrl() { - return this.clusterUrl; - } - - setClusterUrl(clusterUrl) { - this.clusterUrl = clusterUrl; - return this.clusterUrl; - } - } - - 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); + // Mock the postMessage interface used by IPC. + this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); const ipcService = this.owner.lookup('service:ipc'); stubs.ipcService = sinon.stub(ipcService, 'invoke'); }); - hooks.afterEach(function () { - window.removeEventListener('message', messageHandler); - }); - test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { invalidateSession(); assert.expect(2); await visit(urls.targets); await a11yAudit(); + assert.notOk(currentSession().isAuthenticated); assert.strictEqual(currentURL(), urls.authenticate.methods.global); }); - test('visiting index', async function (assert) { + test('visiting targets index', async function (assert) { assert.expect(2); - const targetsCount = this.server.schema.targets.all().models.length; - await visit(urls.targets); + const targetsCount = getTargetCount(); + await visit(urls.projects); + + await click(`[href="${urls.targets}"]`); await a11yAudit(); + assert.strictEqual(currentURL(), urls.targets); - assert.strictEqual(findAll('tbody tr').length, targetsCount); + assert.strictEqual(getTargetCount(), targetsCount); }); test('visiting a target', async function (assert) { - assert.expect(1); - await visit(urls.targets); - await click('tbody tr td span a'); + assert.expect(2); + await visit(urls.projects); + + await click(`[href="${urls.targets}"]`); + + assert.dom(`[data-test-visit-target="${instances.target.id}"]`).exists(); + + await click(`[href="${urls.target}"]`); + assert.strictEqual(currentURL(), urls.target); }); - test('visiting empty targets', async function (assert) { + test('visiting targets list view with no targets', async function (assert) { assert.expect(1); this.server.get('/targets', () => new Response(200)); - await visit(urls.targets); - assert.strictEqual( - find('.rose-message-title').textContent.trim(), - 'No Targets Available' - ); + await visit(urls.projects); + + await click(`[href="${urls.targets}"]`); + + assert.dom(ROSE_APP_STATE_TITLE).hasText('No Targets Available'); }); - test('cannot navigate to a target without proper authorization', async function (assert) { + test('user cannot navigate to a target without proper authorization', async function (assert) { assert.expect(1); instances.target.authorized_actions = instances.target.authorized_actions.filter((item) => item !== 'read'); - await visit(urls.targets); - assert.notOk(find('main tbody .rose-table-header-cell:nth-child(1) a')); + await visit(urls.projects); + + await click(`[href="${urls.targets}"]`); + + assert + .dom(`[data-test-visit-target="${instances.target.id}"]`) + .doesNotExist(); + }); + + test('user can connect to a target with proper authorization', async function (assert) { + assert.expect(2); + await visit(urls.projects); + + await click(`[href="${urls.targets}"]`); + + assert.true( + instances.target.authorized_actions.includes('authorize-session') + ); + assert + .dom(`[data-test-targets-connect-button="${instances.target.id}"]`) + .exists(); + }); + + test('user cannot connect to a target without proper authorization', async function (assert) { + assert.expect(2); + instances.target.authorized_actions = + instances.target.authorized_actions.filter( + (item) => item !== 'authorize-session' + ); + await visit(urls.projects); + + await click(`[href="${urls.targets}"]`); + + assert.false( + instances.target.authorized_actions.includes('authorize-session') + ); + assert + .dom(`[data-test-targets-connect-button="${instances.target.id}"]`) + .doesNotExist(); + }); + + test('user is redirected to target details page when unable to connect from list view', async function (assert) { + assert.expect(2); + instances.target.address = ''; + instances.target.update({ address: '', hostSets: [] }); + await visit(urls.projects); + + await click(`[href="${urls.targets}"]`); + await click(`[data-test-targets-connect-button="${instances.target.id}"]`); + + assert.strictEqual(currentRouteName(), TARGET_DETAILS_ROUTE_NAME); + assert.dom(APP_STATE_TITLE).includesText('Cannot connect'); }); }); diff --git a/ui/desktop/tests/acceptance/projects/targets/target-test.js b/ui/desktop/tests/acceptance/projects/targets/target-test.js index 1b785ef72..8d3750273 100644 --- a/ui/desktop/tests/acceptance/projects/targets/target-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/target-test.js @@ -4,19 +4,26 @@ */ import { module, test } from 'qunit'; -import { visit, click, find, findAll } from '@ember/test-helpers'; +import { visit, click, find } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import sinon from 'sinon'; import { authenticateSession } from 'ember-simple-auth/test-support'; -import { TYPE_TARGET_SSH } from 'api/models/target'; +import WindowMockIPC from '../../../helpers/window-mock-ipc'; module('Acceptance | projects | targets | target', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); - let mockIPC; - let messageHandler; + const TARGET_CONNECT_BUTTON = '[data-test-target-detail-connect-button]'; + const TARGET_QUICK_CONNECT_BUTTON = '[data-test-host-quick-connect]'; + const TARGET_HOST_CONNECT_BUTTON = '[data-test-host-connect]'; + const APP_STATE_TITLE = '.hds-application-state__title'; + const ROSE_DIALOG_MODAL = '.rose-dialog-error'; + const ROSE_DIALOG_MODAL_BUTTONS = '.rose-dialog-footer button'; + const ROSE_DIALOG_RETRY_BUTTON = '.rose-dialog footer .rose-button-primary'; + const ROSE_DIALOG_CANCEL_BUTTON = + '.rose-dialog footer .rose-button-secondary'; const instances = { scopes: { @@ -27,32 +34,29 @@ module('Acceptance | projects | targets | target', function (hooks) { authMethods: { global: null, }, + session: null, target: null, + targetWithOneHost: null, + targetWithTwoHosts: null, }; const stubs = { global: null, org: null, - ipsService: null, + ipcService: null, }; const urls = { - index: '/', - clusterUrl: '/cluster-url', scopes: { global: null, org: null, }, - authenticate: { - global: null, - methods: { - global: null, - }, - }, projects: null, + sessions: null, targets: null, target: null, - sessions: null, + targetWithOneHost: null, + targetWithTwoHosts: null, }; const setDefaultClusterUrl = (test) => { @@ -64,7 +68,7 @@ module('Acceptance | projects | targets | target', function (hooks) { hooks.beforeEach(function () { authenticateSession(); - // create scopes + // Generate scopes instances.scopes.global = this.server.create('scope', { id: 'global' }); stubs.global = { id: 'global', type: 'global' }; instances.scopes.org = this.server.create('scope', { @@ -78,10 +82,10 @@ module('Acceptance | projects | targets | target', function (hooks) { }); stubs.project = { id: instances.scopes.project.id, type: 'project' }; + // Generate resources instances.authMethods.global = this.server.create('auth-method', { scope: instances.scopes.global, }); - instances.hostCatalog = this.server.create( 'host-catalog', { scope: instances.scopes.project }, @@ -95,7 +99,16 @@ module('Acceptance | projects | targets | target', function (hooks) { }, 'withAssociations' ); - + instances.targetWithOneHost = this.server.create( + 'target', + { scope: instances.scopes.project }, + 'withOneHost' + ); + instances.targetWithTwoHosts = this.server.create( + 'target', + { scope: instances.scopes.project }, + 'withTwoHosts' + ); instances.session = this.server.create( 'session', { @@ -105,54 +118,25 @@ module('Acceptance | projects | targets | target', function (hooks) { 'withAssociations' ); + // Generate route URLs for resources 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.targetWithOneHost = `${urls.targets}/${instances.targetWithOneHost.id}`; + urls.targetWithTwoHosts = `${urls.targets}/${instances.targetWithTwoHosts.id}`; - class MockIPC { - clusterUrl = null; - - invoke(method, payload) { - return this[method](payload); - } - - getClusterUrl() { - return this.clusterUrl; - } - - setClusterUrl(clusterUrl) { - this.clusterUrl = clusterUrl; - return this.clusterUrl; - } - } - - 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); + // Mock the postMessage interface used by IPC. + this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); const ipcService = this.owner.lookup('service:ipc'); stubs.ipcService = sinon.stub(ipcService, 'invoke'); }); - hooks.afterEach(function () { - window.removeEventListener('message', messageHandler); - }); - - test.skip('connecting to a target', async function (assert) { - assert.expect(4); + test('user can connect to a target with an address', async function (assert) { + assert.expect(3); stubs.ipcService.withArgs('cliExists').returns(true); stubs.ipcService.withArgs('connect').returns({ session_id: instances.session.id, @@ -160,67 +144,16 @@ module('Acceptance | projects | targets | target', function (hooks) { port: 'p_123', protocol: 'tcp', }); - const confirmService = this.owner.lookup('service:confirm'); - confirmService.enabled = true; 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' - ); - assert.strictEqual( - find('.rose-dialog-body .copyable-content').textContent.trim(), - 'a_123:p_123' - ); - }); - test.skip('displays the correct target type (TCP) in the success dialog body', 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; - await visit(urls.targets); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); + await click(`[href="${urls.target}"]`); - assert.ok(find('.rose-dialog-body h3').textContent.trim().includes('TCP')); - }); + assert.dom(TARGET_CONNECT_BUTTON).isEnabled(); + assert.dom(APP_STATE_TITLE).includesText('Connect for more info'); - test.skip('displays the correct target type (SSH) in the success dialog body', async function (assert) { - assert.expect(1); - instances.target.update({ - type: TYPE_TARGET_SSH, - }); - stubs.ipcService.withArgs('cliExists').returns(true); - stubs.ipcService.withArgs('connect').returns({ - session_id: instances.session.id, - address: 'a_123', - port: 'p_123', - protocol: 'ssh', - }); - const confirmService = this.owner.lookup('service:confirm'); - confirmService.enabled = true; - await visit(urls.targets); - await click( - 'tbody tr:first-child td:last-child button', - 'Activate connect mode' - ); + await click(TARGET_CONNECT_BUTTON); - assert.ok(find('.rose-dialog-body h3').textContent.trim().includes('SSH')); + assert.dom(APP_STATE_TITLE).hasText('Connected'); }); test('handles cli error on connect', async function (assert) { @@ -228,23 +161,15 @@ module('Acceptance | projects | targets | target', function (hooks) { stubs.ipcService.withArgs('cliExists').returns(true); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; + await visit(urls.targets); - await visit(urls.target); - await click('[data-test-target-detail-connect-button]'); + await click(`[href="${urls.target}"]`); + await click(TARGET_CONNECT_BUTTON); - 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' - ); + assert.dom(ROSE_DIALOG_MODAL).exists(); + assert.dom(ROSE_DIALOG_MODAL_BUTTONS).exists({ count: 2 }); + assert.dom(ROSE_DIALOG_RETRY_BUTTON).hasText('Retry'); + assert.dom(ROSE_DIALOG_CANCEL_BUTTON).hasText('Cancel'); }); test('handles connect error', async function (assert) { @@ -253,37 +178,86 @@ module('Acceptance | projects | targets | target', function (hooks) { stubs.ipcService.withArgs('connect').rejects(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; + await visit(urls.targets); - await visit(urls.target); - await click('[data-test-target-detail-connect-button]'); + await click(`[href="${urls.target}"]`); + await click(TARGET_CONNECT_BUTTON); - 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' - ); + assert.dom(ROSE_DIALOG_MODAL).exists(); + assert.dom(ROSE_DIALOG_MODAL_BUTTONS).exists({ count: 2 }); + assert.dom(ROSE_DIALOG_RETRY_BUTTON).hasText('Retry'); + assert.dom(ROSE_DIALOG_CANCEL_BUTTON).hasText('Cancel'); }); - test('can retry on error', async function (assert) { + test('user 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; + await visit(urls.targets); - await visit(urls.target); - await click('[data-test-target-detail-connect-button]'); - const firstErrorDialog = find('.rose-dialog'); - await click('.rose-dialog footer .rose-button-primary', 'Retry'); - const secondErrorDialog = find('.rose-dialog'); + await click(`[href="${urls.target}"]`); + await click(TARGET_CONNECT_BUTTON); + const firstErrorDialog = find(ROSE_DIALOG_MODAL); + await click(ROSE_DIALOG_RETRY_BUTTON, 'Retry'); + const secondErrorDialog = find(ROSE_DIALOG_MODAL); assert.notEqual(secondErrorDialog.id, firstErrorDialog.id); }); + + test('user can connect to a target with one host', async function (assert) { + assert.expect(2); + stubs.ipcService.withArgs('cliExists').returns(true); + stubs.ipcService.withArgs('connect').returns({ + session_id: instances.session.id, + address: 'a_123', + port: 'p_123', + protocol: 'tcp', + }); + await visit(urls.targets); + + await click(`[href="${urls.targetWithOneHost}"]`); + + assert.dom(APP_STATE_TITLE).hasText('Connect for more info'); + + await click(TARGET_CONNECT_BUTTON); + + assert.dom(APP_STATE_TITLE).hasText('Connected'); + }); + + test('user can connect to a target with two hosts using host modal', 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', + }); + await visit(urls.targets); + + await click(`[href="${urls.targetWithTwoHosts}"]`); + + assert.dom(APP_STATE_TITLE).hasText('Connect for more info'); + + await click(TARGET_CONNECT_BUTTON); + + assert.dom(TARGET_HOST_CONNECT_BUTTON).exists({ count: 2 }); + + await click(TARGET_QUICK_CONNECT_BUTTON); + + assert.dom(APP_STATE_TITLE).hasText('Connected'); + }); + + test('user cannot connect to a target without an address and no host sources', async function (assert) { + assert.expect(2); + instances.target.address = ''; + instances.target.update({ address: '', hostSets: [] }); + await visit(urls.targets); + + await click(`[href="${urls.target}"]`); + + assert.dom(TARGET_CONNECT_BUTTON).isDisabled(); + assert.dom(APP_STATE_TITLE).includesText('Cannot connect'); + }); }); diff --git a/ui/desktop/tests/acceptance/targets-test.js b/ui/desktop/tests/acceptance/targets-test.js deleted file mode 100644 index 3cca4e681..000000000 --- a/ui/desktop/tests/acceptance/targets-test.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { visit, click, currentURL } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; -import a11yAudit from 'ember-a11y-testing/test-support/audit'; -import { - currentSession, - authenticateSession, - invalidateSession, -} from 'ember-simple-auth/test-support'; -import WindowMockIPC from '../helpers/window-mock-ipc'; - -module('Acceptance | targets', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - const TARGET_CONNECT_BUTTON = '[data-test-target-detail-connect-button]'; - const APP_STATE_TITLE = '.hds-application-state__title'; - - const instances = { - scopes: { - global: null, - org: null, - project: null, - }, - authMethods: { - global: null, - }, - target: null, - emptyTarget: null, - }; - - const stubs = { - global: null, - org: null, - }; - - const urls = { - scopes: { - global: null, - org: null, - }, - authenticate: { - global: null, - methods: { - global: null, - }, - }, - projects: null, - targets: null, - target: null, - emptyTarget: null, - }; - - const setDefaultClusterUrl = (test) => { - const windowOrigin = window.location.origin; - const clusterUrl = test.owner.lookup('service:clusterUrl'); - clusterUrl.rendererClusterUrl = windowOrigin; - }; - - hooks.beforeEach(function () { - authenticateSession(); - - // create scopes - instances.scopes.global = this.server.create('scope', { id: 'global' }); - stubs.global = { id: 'global', type: 'global' }; - instances.scopes.org = this.server.create('scope', { - type: 'org', - scope: stubs.global, - }); - stubs.org = { id: instances.scopes.org.id, type: 'org' }; - instances.scopes.project = this.server.create('scope', { - type: 'project', - scope: stubs.org, - }); - stubs.project = { id: instances.scopes.project.id, type: 'project' }; - - instances.authMethods.global = this.server.create('auth-method', { - scope: instances.scopes.global, - }); - - instances.hostCatalog = this.server.create( - 'host-catalog', - { scope: instances.scopes.project }, - 'withChildren' - ); - instances.target = this.server.create( - 'target', - { scope: instances.scopes.project, address: '127.0.0.1' }, - 'withAssociations' - ); - instances.emptyTarget = this.server.create('target', { - scope: instances.scopes.project, - }); - - 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.emptyTarget = `${urls.targets}/${instances.emptyTarget.id}`; - - // Mock the postMessage interface used by IPC. - this.owner.register('service:browser/window', WindowMockIPC); - setDefaultClusterUrl(this); - }); - - test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { - invalidateSession(); - assert.expect(2); - await visit(urls.targets); - await a11yAudit(); - assert.notOk(currentSession().isAuthenticated); - assert.strictEqual(currentURL(), urls.authenticate.methods.global); - }); - - test('visiting index', async function (assert) { - assert.expect(1); - await visit(urls.targets); - assert.strictEqual(currentURL(), urls.targets); - }); - - test('cannot connect to a target without an address and no host sources', async function (assert) { - assert.expect(2); - await visit(urls.targets); - - await click(`[href="${urls.emptyTarget}"]`); - - assert.dom(TARGET_CONNECT_BUTTON).isDisabled(); - assert.dom(APP_STATE_TITLE).includesText('Cannot connect'); - }); - - test('can connect to a target with an address', async function (assert) { - assert.expect(2); - await visit(urls.targets); - - await click(`[href="${urls.target}"]`); - - assert.dom(TARGET_CONNECT_BUTTON).isEnabled(); - assert.dom(APP_STATE_TITLE).includesText('Connect for more info'); - }); -});