diff --git a/ui/desktop/app/controllers/scopes/scope/authenticate.js b/ui/desktop/app/controllers/scopes/scope/authenticate.js new file mode 100644 index 000000000..5c36ec58e --- /dev/null +++ b/ui/desktop/app/controllers/scopes/scope/authenticate.js @@ -0,0 +1,17 @@ +import Controller from '@ember/controller'; + +export default class ScopesScopeAuthenticateController extends Controller { + // =attributes + + /** + * Moves the global scope to index 0. + * @type {Array} + */ + get sortedScopes() { + return [ + ...this.model.scopes.filter((scope) => scope.id === 'global'), + ...this.model.scopes.filter((scope) => scope.id !== 'global'), + ] + } + +} diff --git a/ui/desktop/app/templates/application.hbs b/ui/desktop/app/templates/application.hbs index eb09a8cef..d1fa73961 100644 --- a/ui/desktop/app/templates/application.hbs +++ b/ui/desktop/app/templates/application.hbs @@ -10,6 +10,19 @@ {{outlet "header-nav"}} + + + + + {{t 'actions.deauthenticate'}} + + + {{/if}} diff --git a/ui/desktop/app/templates/scopes/scope/authenticate.hbs b/ui/desktop/app/templates/scopes/scope/authenticate.hbs index e2147cab0..68d98634c 100644 --- a/ui/desktop/app/templates/scopes/scope/authenticate.hbs +++ b/ui/desktop/app/templates/scopes/scope/authenticate.hbs @@ -1 +1,57 @@ -{{outlet}} \ No newline at end of file +{{page-title (t "actions.authenticate")}} + + + + + +

{{t "actions.authenticate"}}

+

{{t "auth.descriptions.authenticating-with-org" name=@model.scope.displayName}}

+
+ + + + + + {{t "titles.global"}} + + {{#each this.sortedScopes as |scope|}} + + {{scope.displayName}} + + {{/each}} + + + + + + {{#each @model.authMethods as |authMethod|}} + + {{t (concat "resources.auth-method.types." authMethod.type)}} + + {{/each}} + + + + + {{#if @model.authMethods}} + {{outlet}} + {{/if}} + {{#unless @model.authMethods}} + + + + {{t "resources.auth-method.messages.none.description"}} + + + + {{/unless}} + + +
+
diff --git a/ui/desktop/app/templates/scopes/scope/authenticate/index.hbs b/ui/desktop/app/templates/scopes/scope/authenticate/index.hbs index e2147cab0..effd53cbd 100644 --- a/ui/desktop/app/templates/scopes/scope/authenticate/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/authenticate/index.hbs @@ -1 +1,12 @@ -{{outlet}} \ No newline at end of file +{{#unless @model}} + + + + {{t "resources.auth-method.messages.none.description"}} + + + +{{/unless}} diff --git a/ui/desktop/mirage/config.js b/ui/desktop/mirage/config.js index df5618c17..100a18dfa 100644 --- a/ui/desktop/mirage/config.js +++ b/ui/desktop/mirage/config.js @@ -6,7 +6,7 @@ import { Response } from 'miragejs'; export default function() { - if (!config.isElectron) { + if (!config.isElectron && !Ember.testing) { class MockIPC { invoke(method, payload) { console.log('MockIPC:', method, payload); diff --git a/ui/desktop/tests/acceptance/authentication-test.js b/ui/desktop/tests/acceptance/authentication-test.js new file mode 100644 index 000000000..c23184a59 --- /dev/null +++ b/ui/desktop/tests/acceptance/authentication-test.js @@ -0,0 +1,152 @@ +import { module, test } from 'qunit'; +import { + visit, + currentURL, + fillIn, + click, + find, + findAll, + getRootElement + //setupOnerror, +} from '@ember/test-helpers'; +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 | authentication', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let mockIPC; + let messageHandler; + + const instances = { + scopes: { + global: null, + org: null, + project: null + }, + authMethods: { + global: null, + org: null + }, + hostCatalog: null, + target: null + }; + + const stubs = { + global: null, + org: null + }; + + const urls = { + index: '/', + origin: '/origin', + scopes: { + global: null, + org: null, + }, + authenticate: { + global: null, + methods: { + global: null, + } + }, + targets: null + }; + + const setDefaultOrigin = (test) => { + const origin = window.location.origin; + const session = test.owner.lookup('service:session'); + session.set('data.origin', origin); + }; + + hooks.beforeEach(function () { + invalidateSession(); + + // 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' }; + + // 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', { 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}`; + + 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) { + assert.expect(2); + await visit(urls.index); + //await a11yAudit(); + assert.notOk(currentSession().isAuthenticated); + assert.equal(currentURL(), urls.authenticate.methods.global); + }); + + test('visiting authenticate route when there no methods shows a message', async function (assert) { + assert.expect(2); + instances.authMethods.global.destroy(); + await visit(urls.authenticate.global); + //await a11yAudit(); + assert.equal(currentURL(), urls.authenticate.global); + assert.ok(find('.rose-message')); + }); + +}); diff --git a/ui/desktop/tests/acceptance/origin-test.js b/ui/desktop/tests/acceptance/origin-test.js new file mode 100644 index 000000000..163975f71 --- /dev/null +++ b/ui/desktop/tests/acceptance/origin-test.js @@ -0,0 +1,133 @@ +import { module, test } from 'qunit'; +import { + visit, + currentURL, + fillIn, + click, + find, + findAll, + getRootElement + //setupOnerror, +} from '@ember/test-helpers'; +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 | authentication', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let mockIPC; + let messageHandler; + + const instances = { + scopes: { + global: null, + org: null, + project: null + }, + authMethods: { + global: null, + org: null + }, + hostCatalog: null, + target: null + }; + + const stubs = { + global: null, + org: null + }; + + const urls = { + index: '/', + origin: '/origin', + scopes: { + global: null, + org: null, + }, + authenticate: { + global: null, + org: null + }, + targets: null + }; + + hooks.beforeEach(function () { + invalidateSession(); + + // 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' }; + + // 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', { 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/${instances.authMethods.global.id}`; + urls.authenticate.org = `${urls.scopes.global}/authenticate/${instances.authMethods.org.id}`; + + 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); + }); + + hooks.afterEach(function () { + window.removeEventListener('message', messageHandler); + }); + + test('visiting index without an origin specified redirects to origin route', async function (assert) { + assert.expect(2); + await visit(urls.index); + //await a11yAudit(); + assert.notOk(mockIPC.origin); + assert.equal(currentURL(), urls.origin); + }); +}); diff --git a/ui/desktop/tests/unit/controllers/scopes/scope/authenticate-test.js b/ui/desktop/tests/unit/controllers/scopes/scope/authenticate-test.js new file mode 100644 index 000000000..70b276418 --- /dev/null +++ b/ui/desktop/tests/unit/controllers/scopes/scope/authenticate-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope/authenticate', function(hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:scopes/scope/authenticate'); + assert.ok(controller); + }); +});