diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index ad976e8e7..2fd0d4ea1 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -413,9 +413,12 @@ export default function () { ); this.get('/sessions/:id', function ({ sessions }, { params: { id } }) { const session = sessions.find(id); - return session.update({ - status: pickRandomStatusString(), - }); + if (!Ember.testing) { + session.update({ + status: pickRandomStatusString(), + }); + } + return session; }); this.post( '/sessions/:idMethod', diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 7efe8ade6..22a0205cb 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -50,6 +50,7 @@ "clipboard": "^2.0.6", "core": "*", "doctoc": "^1.4.0", + "ember-a11y-testing": "^4.0.2", "ember-auto-import": "^1.10.1", "ember-autofocus-modifier": "^1.1.0", "ember-cli": "~3.24.0", diff --git a/ui/desktop/tests/acceptance/authentication-test.js b/ui/desktop/tests/acceptance/authentication-test.js index a436b4b54..fa34de896 100644 --- a/ui/desktop/tests/acceptance/authentication-test.js +++ b/ui/desktop/tests/acceptance/authentication-test.js @@ -2,7 +2,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, - //fillIn, + fillIn, click, find, //findAll, @@ -12,8 +12,8 @@ import { 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 { Response } from 'miragejs'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { currentSession, authenticateSession, @@ -37,8 +37,6 @@ module('Acceptance | authentication', function (hooks) { global: null, org: null, }, - hostCatalog: null, - target: null, }; const stubs = { @@ -60,6 +58,7 @@ module('Acceptance | authentication', function (hooks) { }, }, targets: null, + sessions: null, }; const setDefaultOrigin = (test) => { @@ -93,21 +92,13 @@ module('Acceptance | authentication', function (hooks) { scope: instances.scopes.org, }); - instances.hostCatalog = this.server.create( - 'host-catalog', - { scope: instances.scopes.project }, - 'withChildren' - ); - instances.target = this.server.create( - 'target', - { scope: instances.scopes.project }, - 'withRandomHostSets' - ); - 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.global}/projects`; + urls.targets = `${urls.projects}/targets`; + urls.sessions = `${urls.projects}/sessions`; class MockIPC { origin = null; @@ -147,7 +138,7 @@ module('Acceptance | authentication', function (hooks) { test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { assert.expect(2); await visit(urls.index); - //await a11yAudit(); + await a11yAudit(); assert.notOk(currentSession().isAuthenticated); assert.equal(currentURL(), urls.authenticate.methods.global); }); @@ -156,11 +147,89 @@ module('Acceptance | authentication', function (hooks) { assert.expect(2); instances.authMethods.global.destroy(); await visit(urls.authenticate.global); - //await a11yAudit(); + await a11yAudit(); assert.equal(currentURL(), urls.authenticate.global); assert.ok(find('.rose-message')); }); + test('visiting authenticate route without origin redirects to origin index', async function (assert) { + assert.expect(1); + this.owner.lookup('service:origin').rendererOrigin = null; + await visit(urls.authenticate.global); + await a11yAudit(); + assert.equal(currentURL(), urls.origin); + }); + + test('visiting authenticate route when the scope cannot be loaded is allowed', async function (assert) { + assert.expect(1); + this.server.get('/scopes', () => { + return new Response(404); + }); + await visit(urls.authenticate.global); + await a11yAudit(); + assert.equal(currentURL(), urls.authenticate.methods.global); + }); + + test('failed authentication shows a notification message', async function (assert) { + assert.expect(3); + await visit(urls.authenticate.methods.global); + assert.notOk(currentSession().isAuthenticated); + await fillIn('[name="identification"]', 'error'); + await click('[type="submit"]'); + assert.ok(find('.rose-notification.is-error')); + assert.notOk(currentSession().isAuthenticated); + }); + + test('can reset origin before authentication', async function (assert) { + assert.expect(1); + await visit(urls.authenticate.methods.global); + await click('.change-origin a'); + assert.equal(currentURL(), urls.origin); + }); + + test('deauthentication redirects to first global authenticate method', async function (assert) { + assert.expect(3); + await visit(urls.authenticate.methods.global); + await fillIn('[name="identification"]', 'test'); + await fillIn('[name="password"]', 'test'); + later(async () => { + run.cancelTimers(); + assert.ok(currentSession().isAuthenticated); + await click('.rose-header-utilities .rose-dropdown summary'); + assert.equal( + find( + '.rose-header-utilities .rose-dropdown-content button' + ).textContent.trim(), + 'Deauthenticate' + ); + await click('.rose-header-utilities .rose-dropdown-content button'); + assert.notOk(currentSession().isAuthenticated); + }, 750); + await click('[type="submit"]'); + }); + + test('401 responses result in deauthentication', async function (assert) { + assert.expect(3); + await visit(urls.authenticate.methods.global); + await fillIn('[name="identification"]', 'test'); + await fillIn('[name="password"]', 'test'); + later(async () => { + run.cancelTimers(); + assert.ok( + currentSession().isAuthenticated, + 'Session begins authenticated, before encountering 401' + ); + assert.ok(currentURL(), urls.targets); + this.server.get('/sessions', () => new Response(401)); + await visit(urls.sessions); + assert.notOk( + currentSession().isAuthenticated, + 'Session is unauthenticated, after encountering 401' + ); + }, 750); + await click('[type="submit"]'); + }); + test('color theme is applied from session data', async function (assert) { assert.expect(12); authenticateSession({ diff --git a/ui/desktop/tests/acceptance/origin-test.js b/ui/desktop/tests/acceptance/origin-test.js index 8830cc483..4d7469c45 100644 --- a/ui/desktop/tests/acceptance/origin-test.js +++ b/ui/desktop/tests/acceptance/origin-test.js @@ -2,24 +2,27 @@ import { module, test } from 'qunit'; import { visit, currentURL, - //fillIn, - //click, - //find, + 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 a11yAudit from 'ember-a11y-testing/test-support/audit'; +import sinon from 'sinon'; import { - //currentSession, - //authenticateSession, + currentSession, + // authenticateSession, invalidateSession, } from 'ember-simple-auth/test-support'; +import config from '../../config/environment'; -module('Acceptance | authentication', function (hooks) { +module('Acceptance | origin', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); @@ -43,6 +46,7 @@ module('Acceptance | authentication', function (hooks) { const stubs = { global: null, org: null, + ipcService: null, }; const urls = { @@ -54,8 +58,11 @@ module('Acceptance | authentication', function (hooks) { }, authenticate: { global: null, - org: null, + methods: { + global: null, + }, }, + projects: null, targets: null, }; @@ -76,13 +83,9 @@ module('Acceptance | authentication', 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', @@ -96,9 +99,10 @@ module('Acceptance | authentication', 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/${instances.authMethods.global.id}`; - urls.authenticate.org = `${urls.scopes.global}/authenticate/${instances.authMethods.org.id}`; + urls.authenticate.global = `${urls.scopes.global}/authenticate`; + urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`; + urls.projects = `${urls.scopes.global}/projects`; + urls.targets = `${urls.projects}/targets`; class MockIPC { origin = null; @@ -115,6 +119,8 @@ module('Acceptance | authentication', function (hooks) { this.origin = origin; return this.origin; } + + resetOrigin() {} } mockIPC = new MockIPC(); @@ -132,13 +138,92 @@ module('Acceptance | authentication', function (hooks) { hooks.afterEach(function () { window.removeEventListener('message', messageHandler); + sinon.restore(); + }); + + test('visiting index', async function (assert) { + assert.expect(1); + await visit(urls.origin); + await a11yAudit(); + assert.equal(currentURL(), urls.origin); }); test('visiting index without an origin specified redirects to origin route', async function (assert) { assert.expect(2); await visit(urls.index); - //await a11yAudit(); + await a11yAudit(); assert.notOk(mockIPC.origin); assert.equal(currentURL(), urls.origin); }); + + test('can set origin', async function (assert) { + assert.expect(2); + assert.notOk(mockIPC.origin); + await visit(urls.origin); + await a11yAudit(); + await fillIn('[name="host"]', window.location.origin); + await click('[type="submit"]'); + assert.equal(mockIPC.origin, window.location.origin); + }); + + 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 before authentication', async function (assert) { + assert.expect(4); + assert.notOk(mockIPC.origin); + await visit(urls.origin); + await fillIn('[name="host"]', window.location.origin); + await click('[type="submit"]'); + assert.equal(mockIPC.origin, window.location.origin); + assert.equal(currentURL(), urls.authenticate.methods.global); + await click('.change-origin a'); + assert.equal(currentURL(), urls.origin); + }); + + test('origin set automatically in dev mode', async function (assert) { + assert.expect(1); + config.autoOrigin = true; + await visit(urls.origin); + await fillIn('[name="host"]', window.location.origin); + await click('[type="submit"]'); + assert.equal( + this.owner.lookup('controller:origin').origin, + window.location.origin + ); + config.autoOrigin = false; + }); + + test('can reset origin on error', async function (assert) { + assert.expect(4); + await visit(urls.origin); + await a11yAudit(); + await fillIn('[name="host"]', window.location.origin); + await click('[type="submit"]'); + await fillIn('[name="identification"]', 'test'); + await fillIn('[name="password"]', 'test'); + this.server.get('/targets', () => new Response(500)); + await later(async () => { + run.cancelTimers(); + assert.ok(currentSession().isAuthenticated); + assert.equal( + find('main section button').textContent.trim(), + 'Disconnect' + ); + }, 750); + await click('[type="submit"]'); + later(async () => { + assert.notOk(currentSession().isAuthenticated); + assert.equal(currentURL(), urls.authenticate.methods.global); + }, 750); + await click('main section button'); + }); }); diff --git a/ui/desktop/tests/acceptance/projects-test.js b/ui/desktop/tests/acceptance/projects-test.js index b3298cca9..d7978c12f 100644 --- a/ui/desktop/tests/acceptance/projects-test.js +++ b/ui/desktop/tests/acceptance/projects-test.js @@ -12,7 +12,7 @@ import { 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 a11yAudit from 'ember-a11y-testing/test-support/audit'; import { currentSession, authenticateSession, @@ -142,7 +142,7 @@ module('Acceptance | projects', function (hooks) { invalidateSession(); assert.expect(2); await visit(urls.projects); - //await a11yAudit(); + await a11yAudit(); assert.notOk(currentSession().isAuthenticated); assert.equal(currentURL(), urls.authenticate.methods.global); }); @@ -150,7 +150,7 @@ module('Acceptance | projects', function (hooks) { test('visiting index', async function (assert) { assert.expect(1); await visit(urls.projects); - //await a11yAudit(); + await a11yAudit(); assert.equal(currentURL(), urls.projects); }); }); diff --git a/ui/desktop/tests/acceptance/projects/sessions-test.js b/ui/desktop/tests/acceptance/projects/sessions-test.js new file mode 100644 index 000000000..eb3e0bab1 --- /dev/null +++ b/ui/desktop/tests/acceptance/projects/sessions-test.js @@ -0,0 +1,261 @@ +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 { + currentSession, + authenticateSession, + invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | projects | sessions', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let mockIPC; + let messageHandler; + + const instances = { + scopes: { + global: null, + org: null, + project: null, + }, + authMethods: { + global: null, + }, + user: null, + session: null, + }; + + const stubs = { + global: null, + org: null, + }; + + const urls = { + index: '/', + origin: '/origin', + scopes: { + global: null, + org: null, + }, + authenticate: { + global: null, + methods: { + global: null, + }, + }, + projects: null, + sessions: 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.target = this.server.create( + 'target', + { scope: instances.scopes.project }, + 'withRandomHostSets' + ); + instances.session = this.server.create( + 'session', + { + scope: instances.scopes.project, + target: instances.target, + status: 'active', + user: instances.user, + }, + '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.sessions = `${urls.projects}/sessions`; + + 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); + }); + + 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.sessions); + await a11yAudit(); + assert.notOk(currentSession().isAuthenticated); + assert.equal(currentURL(), urls.authenticate.methods.global); + }); + + 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 () => { + run.cancelTimers(); + // await a11yAudit(); + assert.equal(currentURL(), urls.sessions); + assert.equal(findAll('tbody tr').length, sessionsCount); + }, 750); + await visit(urls.sessions); + }); + + test('visiting empty sessions', async function (assert) { + assert.expect(1); + this.server.get('/sessions', () => new Response(200)); + later(async () => { + run.cancelTimers(); + await a11yAudit(); + assert.ok( + find('.rose-message-title').textContent.trim(), + 'No Sessions Available' + ); + }, 750); + await visit(urls.sessions); + }); + + test('visiting sessions without targets is OK', async function (assert) { + assert.expect(2); + instances.session.update({ targetId: undefined }); + const sessionsCount = this.server.schema.sessions.all().models.length; + later(async () => { + run.cancelTimers(); + await a11yAudit(); + assert.equal(currentURL(), urls.sessions); + assert.equal(findAll('tbody tr').length, sessionsCount); + }, 750); + await visit(urls.sessions); + }); + + test('can identify active sessions', async function (assert) { + assert.expect(1); + later(async () => { + run.cancelTimers(); + assert.ok( + find('tbody tr:first-child th:first-child .session-status-active') + ); + }, 750); + await visit(urls.sessions); + }); + + test('cannot identify pending sessions', async function (assert) { + assert.expect(1); + instances.session.update({ status: 'pending' }); + later(async () => { + run.cancelTimers(); + assert.notOk( + find('tbody tr:first-child th:first-child .session-status-active') + ); + }, 750); + await visit(urls.sessions); + }); + + test('cannot identify terminated sessions', async function (assert) { + assert.expect(1); + instances.session.update({ status: 'terminated' }); + later(async () => { + run.cancelTimers(); + assert.notOk( + find('tbody tr:first-child th:first-child .session-status-active') + ); + }, 750); + await visit(urls.sessions); + }); + + test('cancelling a session', async function (assert) { + assert.expect(1); + later(async () => { + run.cancelTimers(); + await click('tbody tr:first-child td:last-child button'); + assert.ok(find('[role="alert"].is-success')); + }, 750); + await visit(urls.sessions); + }); + + test('cancelling a session with error shows notification', async function (assert) { + assert.expect(1); + this.server.post('/sessions/:id_method', () => new Response(400)); + later(async () => { + run.cancelTimers(); + await click('tbody tr:first-child td:last-child button'); + assert.ok(find('[role="alert"].is-error')); + }, 750); + await visit(urls.sessions); + }); +}); diff --git a/ui/desktop/tests/acceptance/projects/targets-test.js b/ui/desktop/tests/acceptance/projects/targets-test.js new file mode 100644 index 000000000..ebda82cc1 --- /dev/null +++ b/ui/desktop/tests/acceptance/projects/targets-test.js @@ -0,0 +1,307 @@ +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', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let mockIPC; + let messageHandler; + + const instances = { + scopes: { + global: null, + org: null, + project: null, + }, + authMethods: { + global: null, + }, + target: null, + session: null, + }; + + const stubs = { + global: null, + org: null, + ipsService: null, + }; + + const urls = { + index: '/', + origin: '/origin', + scopes: { + global: null, + org: null, + }, + authenticate: { + global: null, + methods: { + global: null, + }, + }, + projects: null, + targets: null, + target: null, + sessions: null, + }; + + const setDefaultOrigin = (test) => { + const windowOrigin = window.location.origin; + const origin = test.owner.lookup('service:origin'); + origin.rendererOrigin = 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 }, + 'withRandomHostSets' + ); + + 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.sessions = `${urls.target}/sessions`; + + 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.targets); + await a11yAudit(); + assert.notOk(currentSession().isAuthenticated); + assert.equal(currentURL(), urls.authenticate.methods.global); + }); + + 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 () => { + run.cancelTimers(); + await a11yAudit(); + assert.equal(currentURL(), urls.targets); + assert.equal(findAll('tbody tr').length, targetsCount); + }, 750); + await visit(urls.targets); + }); + + test('visiting a target', async function (assert) { + assert.expect(1); + later(async () => { + run.cancelTimers(); + await click('tbody tr th a'); + assert.equal(currentURL(), urls.sessions); + }, 750); + await visit(urls.targets); + }); + + test('visiting empty targets', async function (assert) { + assert.expect(1); + this.server.get('/targets', () => new Response(200)); + later(async () => { + run.cancelTimers(); + assert.equal( + find('.rose-message-title').textContent.trim(), + 'No Targets Available' + ); + }, 750); + await visit(urls.targets); + }); + + test('connecting to a target', async function (assert) { + assert.expect(4); + 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(findAll('.rose-dialog-footer button').length, 1); + assert.equal( + find('.rose-dialog-footer button').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.targets); + }); + + test('handles cli error on connect', async function (assert) { + assert.expect(4); + stubs.ipcService.withArgs('cliExists').returns(true); + 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.targets); + }); + + 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.targets); + }); + + 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.targets); + }); +}); 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); + }); +}); diff --git a/ui/desktop/tests/acceptance/projects/targets/sessions-test.js b/ui/desktop/tests/acceptance/projects/targets/sessions-test.js new file mode 100644 index 000000000..a28682950 --- /dev/null +++ b/ui/desktop/tests/acceptance/projects/targets/sessions-test.js @@ -0,0 +1,381 @@ +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 | sessions', 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, + sessions: 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 }, + 'withRandomHostSets' + ); + + instances.session = this.server.create( + 'session', + { + scope: instances.scopes.project, + target: instances.target, + status: 'active', + user: instances.user, + }, + '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.sessions = `${urls.target}/sessions`; + + 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.sessions); + await a11yAudit(); + assert.notOk(currentSession().isAuthenticated); + assert.equal(currentURL(), urls.authenticate.methods.global); + }); + + 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 () => { + run.cancelTimers(); + // await a11yAudit(); + assert.equal(currentURL(), urls.sessions); + assert.equal(findAll('tbody tr').length, sessionsCount); + }, 750); + await visit(urls.sessions); + }); + + test('visiting index redirects to sessions', async function (assert) { + assert.expect(1); + later(async () => { + run.cancelTimers(); + // await a11yAudit(); + assert.equal(currentURL(), urls.sessions); + }, 750); + await visit(urls.target); + }); + + test('visiting empty sessions', async function (assert) { + assert.expect(1); + this.server.get('/sessions', () => new Response(200)); + later(async () => { + run.cancelTimers(); + assert.ok( + find('.rose-message-title').textContent.trim(), + 'No Sessions Available' + ); + }, 750); + await visit(urls.sessions); + }); + + test('can identify target with active sessions', async function (assert) { + assert.expect(1); + later(async () => { + run.cancelTimers(); + assert.ok(find('.rose-layout-page-header .rose-badge-success')); + }, 750); + await visit(urls.sessions); + }); + + test('can identify target with pending sessions', async function (assert) { + assert.expect(1); + instances.session.update({ status: 'pending' }); + later(async () => { + run.cancelTimers(); + assert.ok(find('.rose-layout-page-header .rose-badge-success')); + }, 750); + await visit(urls.sessions); + }); + + test('cannot identify target with terminated sessions', async function (assert) { + assert.expect(1); + instances.session.update({ status: 'terminated' }); + later(async () => { + run.cancelTimers(); + assert.notOk(find('.rose-layout-page-header .rose-badge-success')); + }, 750); + await visit(urls.sessions); + }); + + test('cancelling a session', async function (assert) { + assert.expect(2); + const sessionsCount = this.server.schema.sessions.all().models.length; + later(async () => { + run.cancelTimers(); + await click('tbody tr:first-child td:last-child button'); + assert.ok(find('[role="alert"].is-success')); + assert.equal(findAll('tbody tr').length, sessionsCount - 1); + }, 750); + await visit(urls.sessions); + }); + + test('cancelling a session with error shows notification', async function (assert) { + assert.expect(1); + this.server.post('/sessions/:id_method', () => new Response(400)); + later(async () => { + run.cancelTimers(); + await click('tbody tr:first-child td:last-child button'); + assert.ok(find('[role="alert"].is-error')); + }, 750); + await visit(urls.sessions); + }); + + test('connecting to a target', 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('.rose-layout-page-actions 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.sessions); + }); + + test('connect details in session', 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', + }); + const confirmService = this.owner.lookup('service:confirm'); + confirmService.enabled = true; + + later(async () => { + run.cancelTimers(); + instances.session.update('status', 'active'); + await click('.rose-layout-page-actions button', 'Activate connect mode'); + assert.ok(find('.rose-dialog-success'), 'Success dialog'); + await click('.rose-dialog-dismiss'); + assert.equal( + find( + 'tbody tr:first-child td:nth-child(2) .copyable-content' + ).textContent.trim(), + 'a_123:p_123' + ); + }, 750); + await visit(urls.sessions); + }); + + 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('.rose-layout-page-actions 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.sessions); + }); + + 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('.rose-layout-page-actions 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.sessions); + }); + + 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('.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); + }); +}); diff --git a/ui/desktop/tests/acceptance/scopes-test.js b/ui/desktop/tests/acceptance/scopes-test.js new file mode 100644 index 000000000..cb15c0af9 --- /dev/null +++ b/ui/desktop/tests/acceptance/scopes-test.js @@ -0,0 +1,348 @@ +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 | scopes', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let mockIPC; + let messageHandler; + + const instances = { + scopes: { + global: null, + org: null, + project: null, + }, + authMethods: { + global: null, + }, + target: null, + session: null, + }; + + const stubs = { + global: null, + org: null, + ipcService: null, + }; + + const urls = { + index: '/', + origin: '/origin', + scopes: { + global: null, + org: null, + org2: null, + }, + authenticate: { + global: null, + methods: { + global: null, + }, + }, + projects: null, + org2Projects: null, + globalProjects: null, + targets: null, + org2Targets: null, + globalTargets: null, + target: null, + targetSessions: null, + }; + + const setDefaultOrigin = (test) => { + const windowOrigin = window.location.origin; + const origin = test.owner.lookup('service:origin'); + origin.rendererOrigin = 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, + }); + instances.scopes.org2 = 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 }, + 'withRandomHostSets' + ); + + 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.scopes.org2 = `/scopes/${instances.scopes.org2.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.org2Projects = `${urls.scopes.org2}/projects`; + urls.globalProjects = `${urls.scopes.global}/projects`; + urls.targets = `${urls.projects}/targets`; + urls.org2Targets = `${urls.org2Projects}/targets`; + urls.globalTargets = `${urls.globalProjects}/targets`; + urls.target = `${urls.targets}/${instances.target.id}`; + urls.targetSessions = `${urls.target}/sessions`; + + 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', 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 () => { + run.cancelTimers(); + await a11yAudit(); + assert.equal(currentURL(), urls.targets); + assert.equal(findAll('tbody tr').length, targetsCount); + }, 750); + await visit(urls.targets); + }); + + test('visiting global scope', async function (assert) { + assert.expect(1); + later(async () => { + run.cancelTimers(); + await a11yAudit(); + assert.equal(currentURL(), urls.globalTargets); + }, 750); + await visit(urls.scopes.global); + }); + + // TODO: this probably shouldn't be the case, but was setup to enable + // authentication when the global scope couldn't be loaded. + // In order to resolve this, we might hoist authentication routes up from + // under scopes. + test('visiting global scope is not successful when the global scope cannot be fetched', async function (assert) { + assert.expect(1); + this.server.get('/scopes/:id', ({ scopes }, { params: { id } }) => { + const scope = scopes.find(id); + const response = id === 'global' ? new Response(404) : scope; + return response; + }); + later(async () => { + run.cancelTimers(); + await a11yAudit(); + assert.equal(currentURL(), urls.globalTargets); + }, 750); + await visit(urls.scopes.global); + }); + + test('visiting org scope', async function (assert) { + assert.expect(1); + later(async () => { + run.cancelTimers(); + await a11yAudit(); + assert.equal(currentURL(), urls.targets); + }, 750); + await visit(urls.scopes.org); + }); + + test('can navigate among org scopes via header navigation', async function (assert) { + assert.expect(3); + await later(async () => run.cancelTimers(), 750); + await visit(urls.targets); + await later(async () => assert.equal(currentURL(), urls.targets), 750); + await click('.rose-header-nav .rose-dropdown a:nth-of-type(2)'); + await later(async () => assert.equal(currentURL(), urls.org2Targets), 750); + await click('.rose-header-nav .rose-dropdown a:nth-of-type(3)'); + later(async () => assert.equal(currentURL(), urls.globalTargets), 750); + await click('.rose-header-nav .rose-dropdown a:nth-of-type(1)'); + }); + + 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.equal(currentURL(), urls.authenticate.methods.global); + }); + + test('visiting a target', async function (assert) { + assert.expect(1); + later(async () => { + run.cancelTimers(); + await click('tbody tr th a'); + assert.equal(currentURL(), urls.targetSessions); + }, 750); + await visit(urls.targets); + }); + + test('visiting empty targets', async function (assert) { + assert.expect(1); + this.server.get('/targets', () => new Response(200)); + later(async () => { + run.cancelTimers(); + assert.ok( + find('.rose-message-title').textContent.trim(), + 'No Targets Available' + ); + }, 750); + await visit(urls.targets); + }); + + test('connecting to a target', 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(findAll('.rose-dialog-footer button').length, 1); + assert.equal( + find('.rose-dialog-footer button').textContent.trim(), + 'OK', + 'Cannot retry' + ); + }, 750); + await visit(urls.targets); + }); + + 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.scopes.global); + }); + + 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.targets); + }); +}); diff --git a/ui/desktop/tests/integration/components/branded-card-test.js b/ui/desktop/tests/integration/components/branded-card-test.js index 019103ec5..cc537772d 100644 --- a/ui/desktop/tests/integration/components/branded-card-test.js +++ b/ui/desktop/tests/integration/components/branded-card-test.js @@ -1,26 +1,27 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; +import { render, find } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; module('Integration | Component | branded-card', function (hooks) { setupRenderingTest(hooks); test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function(val) { ... }); - await render(hbs``); + assert.ok(find('.branded-card')); + assert.ok(find('.branded-card-title')); + assert.ok(find('.branded-card-description')); + }); - assert.equal(this.element.textContent.trim(), ''); - - // Template block usage: - await render(hbs` - - template block text - - `); - - assert.equal(this.element.textContent.trim(), 'template block text'); + test('it renders with content', async function (assert) { + await render(hbs``); + assert.equal(find('.branded-card-title').textContent.trim(), 'title'); + assert.equal( + find('.branded-card-description').textContent.trim(), + 'description' + ); }); }); diff --git a/ui/desktop/tests/unit/services/origin-test.js b/ui/desktop/tests/unit/services/origin-test.js index 1355719a2..4d8703cf6 100644 --- a/ui/desktop/tests/unit/services/origin-test.js +++ b/ui/desktop/tests/unit/services/origin-test.js @@ -1,12 +1,27 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import sinon from 'sinon'; module('Unit | Service | origin', function (hooks) { setupTest(hooks); - // TODO: Replace this with your real tests. - test('it exists', function (assert) { - let service = this.owner.lookup('service:origin'); - assert.ok(service); + let service, ipcService; + + hooks.beforeEach(function () { + service = this.owner.lookup('service:origin'); + ipcService = this.owner.lookup('service:ipc'); + }); + + test('resets origin on error', async function (assert) { + assert.expect(4); + const ipcServiceStubbed = sinon.stub(ipcService, 'invoke'); + await service.setOrigin(window.location.origin); + assert.equal(service.rendererOrigin, window.location.origin); + assert.equal(service.adapter.host, window.location.origin); + ipcServiceStubbed.withArgs('setOrigin').rejects(); + service.setOrigin('invalid-origin').catch(() => { + assert.notOk(service.rendererOrigin); + assert.equal(service.adapter.host, window.location.origin); + }); }); }); diff --git a/yarn.lock b/yarn.lock index 768f6d845..aeaef9f2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4174,6 +4174,11 @@ axe-core@3.5.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== +axe-core@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.3.tgz#64a4c85509e0991f5168340edc4bedd1ceea6966" + integrity sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ== + babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -6495,9 +6500,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001157, caniuse-lite@^1.0.30001181: - version "1.0.30001187" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001187.tgz#5706942631f83baa5a0218b7dfa6ced29f845438" - integrity sha512-w7/EP1JRZ9552CyrThUnay2RkZ1DXxKe/Q2swTC4+LElLh9RRYrL1Z+27LlakB8kzY0fSmHw9mc7XYDUKAKWMA== + version "1.0.30001204" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz#256c85709a348ec4d175e847a3b515c66e79f2aa" + integrity sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ== capture-exit@^2.0.0: version "2.0.0" @@ -7736,6 +7741,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +date-and-time@^0.14.1: + version "0.14.2" + resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.14.2.tgz#a4266c3dead460f6c231fe9674e585908dac354e" + integrity sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA== + date-time@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/date-time/-/date-time-2.1.0.tgz#0286d1b4c769633b3ca13e1e62558d2dbdc2eba2" @@ -7785,7 +7795,7 @@ debug@~4.1.0: dependencies: ms "^2.1.1" -debuglog@^1.0.1: +debuglog@*, debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -8350,9 +8360,9 @@ electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.649: integrity sha512-ojGDupQ3UMkvPWcTICe4JYe17+o9OLiFMPoduoR72Zp2ILt1mRVeqnxBEd6s/ptekrnsFU+0A4lStfBe/wyG/A== electron-to-chromium@^1.3.564: - version "1.3.687" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.687.tgz#c336184b7ab70427ffe2ee79eaeaedbc1ad8c374" - integrity sha512-IpzksdQNl3wdgkzf7dnA7/v10w0Utf1dF2L+B4+gKrloBrxCut+au+kky3PYvle3RMdSxZP+UiCZtLbcYRxSNQ== + version "1.3.693" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.693.tgz#5089c506a925c31f93fcb173a003a22e341115dd" + integrity sha512-vUdsE8yyeu30RecppQtI+XTz2++LWLVEIYmzeCaCRLSdtKZ2eXqdJcrs85KwLiPOPVc6PELgWyXBsfqIvzGZag== element-resize-detector@^1.2.1: version "1.2.1" @@ -8385,6 +8395,24 @@ ember-a11y-testing@^3.0.0: ember-cli-version-checker "5.1.1" ember-get-config "0.2.4" +ember-a11y-testing@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/ember-a11y-testing/-/ember-a11y-testing-4.0.2.tgz#960ee9582bd315bd16d9dedbf450fd508d021d99" + integrity sha512-3pa+6bibbY4Vsl3uKFpD0OH4Mzd8NQUwenaWWWngiJo+R/U6AgJllxo/apORzRgvLxKtBCEpXVocUp/gkZ3rOQ== + dependencies: + axe-core "^4.1.2" + body-parser "^1.19.0" + broccoli-funnel "^3.0.3" + broccoli-merge-trees "^4.2.0" + date-and-time "^0.14.1" + ember-auto-import "^1.10.1" + ember-cli-babel "^7.21.0" + ember-cli-typescript "^3.0.0" + ember-cli-version-checker "^5.1.2" + ember-destroyable-polyfill "^2.0.1" + fs-extra "^9.0.1" + validate-peer-dependencies "^1.1.0" + ember-assign-polyfill@^2.6.0: version "2.7.2" resolved "https://registry.yarnpkg.com/ember-assign-polyfill/-/ember-assign-polyfill-2.7.2.tgz#58f6f60235126cb23df248c846008fa9a3245fc1" @@ -8833,7 +8861,7 @@ ember-cli-typescript@^2.0.0-rc.2, ember-cli-typescript@^2.0.2: stagehand "^1.0.0" walk-sync "^1.0.0" -ember-cli-typescript@^3.1.3, ember-cli-typescript@^3.1.4: +ember-cli-typescript@^3.0.0, ember-cli-typescript@^3.1.3, ember-cli-typescript@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-3.1.4.tgz#21d6ccd670d1f2e34c9cce68c6e32c442f46806b" integrity sha512-HJ73kL45OGRmIkPhBNFt31I1SGUvdZND+LCH21+qpq3pPlFpJG8GORyXpP+2ze8PbnITNLzwe5AwUrpyuRswdQ== @@ -9120,7 +9148,7 @@ ember-decorators-polyfill@^1.1.5: ember-cli-version-checker "^3.1.3" ember-compatibility-helpers "^1.2.0" -ember-destroyable-polyfill@^2.0.2, ember-destroyable-polyfill@^2.0.3: +ember-destroyable-polyfill@^2.0.1, ember-destroyable-polyfill@^2.0.2, ember-destroyable-polyfill@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/ember-destroyable-polyfill/-/ember-destroyable-polyfill-2.0.3.tgz#1673ed66609a82268ef270a7d917ebd3647f11e1" integrity sha512-TovtNqCumzyAiW0/OisSkkVK93xnVF4NRU6+FN0ubpfwEOpRrmM2RqDwXI6YAChCgSHON1cz0DfQStpA1Gjuuw== @@ -12264,9 +12292,9 @@ heimdalljs@^0.3.0: rsvp "~3.2.1" highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@~10.5.0: - version "10.6.0" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.6.0.tgz#0073aa71d566906965ba6e1b7be7b2682f5e18b6" - integrity sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ== + version "10.7.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.1.tgz#a8ec4152db24ea630c90927d6cae2a45f8ecb955" + integrity sha512-S6G97tHGqJ/U8DsXcEdnACbirtbx58Bx9CzIVeYli8OuswCfYI/LsXH2EiGcoGio1KAC3x4mmUwulOllJ2ZyRA== hmac-drbg@^1.0.1: version "1.0.1" @@ -12316,6 +12344,13 @@ hosted-git-info@^3.0.2, hosted-git-info@^3.0.6: dependencies: lru-cache "^6.0.0" +hosted-git-info@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.1.tgz#710ef5452ea429a844abc33c981056e7371edab7" + integrity sha512-eT7NrxAsppPRQEBSwKSosReE+v8OzABwEScQYk5d4uxaEPlzxTIku7LINXtBGalthkLhJnq5lBI89PfK43zAKg== + dependencies: + lru-cache "^6.0.0" + html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -12600,9 +12635,9 @@ ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8: integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== immer@8.0.1, immer@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" - integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== + version "8.0.4" + resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a" + integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ== import-cwd@^2.0.0: version "2.1.0" @@ -12644,7 +12679,7 @@ import-lazy@^4.0.0: resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== -imurmurhash@^0.1.4: +imurmurhash@*, imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -14327,6 +14362,11 @@ lodash._baseflatten@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash._baseindexof@*: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" + integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= + lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -14335,11 +14375,16 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@^3.0.0: +lodash._bindcallback@*, lodash._bindcallback@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= +lodash._cacheindexof@*: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" + integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= + lodash._createassigner@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" @@ -14349,12 +14394,19 @@ lodash._createassigner@^3.0.0: lodash._isiterateecall "^3.0.0" lodash.restparam "^3.0.0" +lodash._createcache@*: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" + integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= + dependencies: + lodash._getnative "^3.0.0" + lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@^3.0.0: +lodash._getnative@*, lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -14567,7 +14619,7 @@ lodash.pick@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= -lodash.restparam@^3.0.0: +lodash.restparam@*, lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= @@ -15785,9 +15837,9 @@ node-modules-regexp@^1.0.0: integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= node-notifier@^5.0.1, node-notifier@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1" - integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA== + version "8.0.2" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5" + integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg== dependencies: growly "^1.3.0" is-wsl "^2.2.0" @@ -15995,12 +16047,12 @@ npm-package-arg@^7.0.0: validate-npm-package-name "^3.0.0" npm-package-arg@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.0.tgz#b5f6319418c3246a1c38e1a8fbaa06231bc5308f" - integrity sha512-/ep6QDxBkm9HvOhOg0heitSd7JHA1U7y1qhhlRlteYYAi9Pdb/ZV7FW5aHpkrpM8+P+4p/jjR8zCyKPBMBjSig== + version "8.1.2" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.2.tgz#b868016ae7de5619e729993fbd8d11dc3c52ab62" + integrity sha512-6Eem455JsSMJY6Kpd3EyWE+n5hC+g9bSyHr9K9U2zqZb7+02+hObQ2c0+8iDk/mNF+8r1MhY44WypKJAkySIYA== dependencies: - hosted-git-info "^3.0.6" - semver "^7.0.0" + hosted-git-info "^4.0.1" + semver "^7.3.4" validate-npm-package-name "^3.0.0" npm-package-arg@^8.1.0: @@ -17671,9 +17723,9 @@ query-string@^5.0.1: strict-uri-encode "^1.0.0" query-string@^6.8.2: - version "6.14.0" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.0.tgz#0b7b7ca326f5facf10dd2d45d26645cd287f8c92" - integrity sha512-In3o+lUxlgejoVJgwEdYtdxrmlL0cQWJXj0+kkI7RWVo7hg5AhFtybeKlC9Dpgbr8eOC4ydpEh8017WwyfzqVQ== + version "6.14.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== dependencies: decode-uri-component "^0.2.0" filter-obj "^1.1.0" @@ -19519,9 +19571,9 @@ socks-proxy-agent@^5.0.0: socks "^2.3.3" socks@^2.3.3: - version "2.5.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.5.1.tgz#7720640b6b5ec9a07d556419203baa3f0596df5f" - integrity sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ== + version "2.6.0" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.0.tgz#6b984928461d39871b3666754b9000ecf39dfac2" + integrity sha512-mNmr9owlinMplev0Wd7UHFlqI4ofnBnNzFuzrm63PPaHgbkqCFe4T5LzwKmtQ/f2tX0NTpcdVLyD/FHxFBstYw== dependencies: ip "^1.1.5" smart-buffer "^4.1.0"