From aa40a3c9178ebb0cb47d03dbc98cf499560bdf25 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 13 Nov 2020 15:20:26 -0500 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20origin=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/desktop/app/controllers/.gitkeep | 0 ui/desktop/app/controllers/application.js | 13 +++ ui/desktop/app/router.js | 1 + ui/desktop/app/routes/application.js | 26 +++++ ui/desktop/app/routes/index.js | 15 ++- ui/desktop/app/routes/origin.js | 49 ++++++++++ ui/desktop/app/templates/application.hbs | 20 +++- ui/desktop/app/templates/origin.hbs | 19 ++++ ui/desktop/config/ember-intl.js | 94 +++++++++++++++++++ ui/desktop/config/environment.js | 7 ++ ui/desktop/electron-app/src/handlers.js | 27 ++++-- .../unit/controllers/application-test.js | 12 +++ ui/desktop/tests/unit/routes/origin-test.js | 11 +++ ui/desktop/translations/form/en-us.yaml | 3 + 14 files changed, 285 insertions(+), 12 deletions(-) delete mode 100644 ui/desktop/app/controllers/.gitkeep create mode 100644 ui/desktop/app/controllers/application.js create mode 100644 ui/desktop/app/routes/origin.js create mode 100644 ui/desktop/app/templates/origin.hbs create mode 100644 ui/desktop/config/ember-intl.js create mode 100644 ui/desktop/tests/unit/controllers/application-test.js create mode 100644 ui/desktop/tests/unit/routes/origin-test.js diff --git a/ui/desktop/app/controllers/.gitkeep b/ui/desktop/app/controllers/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/ui/desktop/app/controllers/application.js b/ui/desktop/app/controllers/application.js new file mode 100644 index 000000000..0eb5226e0 --- /dev/null +++ b/ui/desktop/app/controllers/application.js @@ -0,0 +1,13 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import config from '../config/environment'; + +export default class ApplicationController extends Controller { + // =services + + @service session; + + // =attributes + + notifyTimeout = config.notifyTimeout; +} diff --git a/ui/desktop/app/router.js b/ui/desktop/app/router.js index 67ec24b1d..98626ddce 100644 --- a/ui/desktop/app/router.js +++ b/ui/desktop/app/router.js @@ -15,4 +15,5 @@ Router.map(function () { }); }); }); + this.route('origin'); }); diff --git a/ui/desktop/app/routes/application.js b/ui/desktop/app/routes/application.js index 3c5a507fb..1c1211c89 100644 --- a/ui/desktop/app/routes/application.js +++ b/ui/desktop/app/routes/application.js @@ -16,11 +16,37 @@ export default class ApplicationRoute extends Route.extend( // =services @service session; + @service ipc; // =attributes + /** + * @type {string} + */ routeIfUnauthenticated = 'index'; + /** + * @type {ApplicationAdapter} + */ + get adapter() { + return this.store.adapterFor('application'); + } + + /** + * Check that the origin specified in the renderer matches the origin + * reported by the main process. If they differ, update the main process + * origin so that the renderer's CSP can be rewritten to allow requests. + */ + async beforeModel() { + const rendererOrigin = this.session.data.origin; + const mainOrigin = await this.ipc.invoke('getOrigin'); + const isMatch = rendererOrigin === mainOrigin; + this.adapter.host = rendererOrigin; + if (rendererOrigin && !isMatch) { + await this.ipc.invoke('setOrigin', rendererOrigin); + } + } + /** * After becoming authenticated, does nothing. This overrides the default * behavior of the ApplicationRouteMixin, which is to redirect after auth. diff --git a/ui/desktop/app/routes/index.js b/ui/desktop/app/routes/index.js index 3ce2a42e1..1bc0a0f02 100644 --- a/ui/desktop/app/routes/index.js +++ b/ui/desktop/app/routes/index.js @@ -1,12 +1,23 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class IndexRoute extends Route { + + // =services + + @service session; + // =methods /** - * Redirects to scopes route for further processing. + * If no origin is specified yet, redirects to origin, otherwise scopes. */ redirect() { - this.replaceWith('scopes'); + const rendererOrigin = this.session.data.origin; + if (!rendererOrigin) { + this.replaceWith('origin'); + } else { + this.replaceWith('scopes'); + } } } diff --git a/ui/desktop/app/routes/origin.js b/ui/desktop/app/routes/origin.js new file mode 100644 index 000000000..cb5ba961e --- /dev/null +++ b/ui/desktop/app/routes/origin.js @@ -0,0 +1,49 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class OriginRoute extends Route { + + // =services + + @service session; + @service ipc; + @service intl; + @service notify; + + // =attributes + + /** + * @type {ApplicationAdapter} + */ + get adapter() { + return this.store.adapterFor('application'); + } + + // =methods + + /** + * Points the API to the specified origin and persists it to the session, + * as well as the main process. When the main process receives the origin, + * it is expected that the renderer will be restarted. + * @param {string} origin + */ + @action + async setOrigin(origin) { + this.adapter.host = origin; + // Check if we have a Boundary endpoint... + try { + // If scopes load, this could be a Boundary API + await this.store.query('scope', {}); + this.session.set('data.origin', origin); + await this.ipc.invoke('setOrigin', origin); + this.replaceWith('index'); + } catch (e) { + // If scopes don't load, we assume this is not a Boundary API + const errorMessage = + this.intl.t('errors.origin-verification-failed.description'); + this.notify.error(errorMessage, { closeAfter: null }); + } + } + +} diff --git a/ui/desktop/app/templates/application.hbs b/ui/desktop/app/templates/application.hbs index 8f1034fb1..eb09a8cef 100644 --- a/ui/desktop/app/templates/application.hbs +++ b/ui/desktop/app/templates/application.hbs @@ -6,9 +6,12 @@ - - {{outlet "header-nav"}} - + {{#if this.session.isAuthenticated}} + + {{outlet "header-nav"}} + + {{/if}} + @@ -17,3 +20,14 @@ + + + + {{message.text}} + + diff --git a/ui/desktop/app/templates/origin.hbs b/ui/desktop/app/templates/origin.hbs new file mode 100644 index 000000000..5616eccb4 --- /dev/null +++ b/ui/desktop/app/templates/origin.hbs @@ -0,0 +1,19 @@ + + + + + + + diff --git a/ui/desktop/config/ember-intl.js b/ui/desktop/config/ember-intl.js new file mode 100644 index 000000000..783bb4032 --- /dev/null +++ b/ui/desktop/config/ember-intl.js @@ -0,0 +1,94 @@ +/*jshint node:true*/ + +module.exports = function (/* environment */) { + return { + /** + * Merges the fallback locale's translations into all other locales as a + * build-time fallback strategy. + * + * This will **not** prevent missing translation warnings or errors from occurring. + * It's meant as safety net when warning are enabled. + * When enabled along with `errorOnMissingTranslations` any fallback attempts will result in an error. + * + * @property fallbackLocale + * @type {String?} + * @default "null" + */ + fallbackLocale: null, + + /** + * Path where translations are kept. This is relative to the project root. + * For example, if your translations are an npm dependency, set this to: + *`'./node_modules/path/to/translations'` + * + * @property inputPath + * @type {String} + * @default "'translations'" + */ + inputPath: 'translations', + + /** + * Prevents the translations from being bundled with the application code. + * This enables asynchronously loading the translations for the active locale + * by fetching them from the asset folder of the build. + * + * See: https://ember-intl.github.io/ember-intl/docs/guide/asynchronously-loading-translations + * + * @property publicOnly + * @type {Boolean} + * @default "false" + */ + publicOnly: false, + + /** + * Add the subdirectories of the translations as a namespace for all keys. + * + * @property wrapTranslationsWithNamespace + * @type {Boolean} + * @default "false" + */ + wrapTranslationsWithNamespace: true, + + /** + * Cause a build error if ICU argument mismatches are detected. + * + * @property errorOnNamedArgumentMismatch + * @type {Boolean} + * @default "false" + */ + errorOnNamedArgumentMismatch: false, + + /** + * Cause a build error if missing translations are detected. + * + * See https://ember-intl.github.io/ember-intl/docs/guide/missing-translations#throwing-a-build-error-on-missing-required-translation + * + * @property errorOnMissingTranslations + * @type {Boolean} + * @default "false" + */ + errorOnMissingTranslations: false, + + /** + * removes empty translations from the build output. + * + * @property stripEmptyTranslations + * @type {Boolean} + * @default "false" + */ + stripEmptyTranslations: false, + + /** + * Filter missing translations to ignore expected missing translations. + * + * See https://ember-intl.github.io/ember-intl/docs/guide/missing-translations#requiring-translations + * + * @property requiresTranslation + * @type {Function} + * @default "function(key,locale) { return true }" + */ + requiresTranslation(/* key, locale */) { + return true; + }, + }; +}; diff --git a/ui/desktop/config/environment.js b/ui/desktop/config/environment.js index e358c0cac..57a61e665 100644 --- a/ui/desktop/config/environment.js +++ b/ui/desktop/config/environment.js @@ -26,12 +26,16 @@ module.exports = function (environment) { // when it is created }, + isElectron: Boolean(process.env.EMBER_CLI_ELECTRON), + api: { host: API_HOST, namespace: 'v1', }, appName: APP_NAME, + + notifyTimeout: 4000, }; if (environment === 'development') { @@ -52,6 +56,9 @@ module.exports = function (environment) { ENV.APP.rootElement = '#ember-testing'; ENV.APP.autoboot = false; + + // Notification timeout should be 0 for fast tests + ENV.notifyTimeout = 0; } if (environment === 'production') { diff --git a/ui/desktop/electron-app/src/handlers.js b/ui/desktop/electron-app/src/handlers.js index f5bcf175b..e6a35ba9e 100644 --- a/ui/desktop/electron-app/src/handlers.js +++ b/ui/desktop/electron-app/src/handlers.js @@ -1,17 +1,30 @@ const { ipcMain } = require('electron'); const isDev = require('electron-is-dev'); - const log = (method, request) => { if (isDev) console.log(`[ipc-main](${method}): `, request); } +let origin = 'serve://boundary'; /** - * A demonstration method `getFoobars` which returns an object with a message. + * Returns the current runtime origin, which is used by the main thread to + * rewrite the CSP to allow requests. */ -ipcMain.handle('getFoobars', async (event, request) => { - log('getFoobars', request); - return { - message: 'this is a response from an ipcMain handler' - }; +ipcMain.handle('getOrigin', async (event, request) => { + log('getOrigin', request); + return origin; +}); + +/** + * TODO: sets the main process origin to the specified value. This value will + * be used to rewrite the CSP to allow requests to that origin. If the origin + * was not previously set, the main process will set it and restart + * the renderer. + * + * For now, we update the internal origin but do not yet rewrite the CSP. + */ +ipcMain.handle('setOrigin', async (event, request) => { + log('setOrigin', request); + origin = request.data; + return origin; }); diff --git a/ui/desktop/tests/unit/controllers/application-test.js b/ui/desktop/tests/unit/controllers/application-test.js new file mode 100644 index 000000000..e4b814b28 --- /dev/null +++ b/ui/desktop/tests/unit/controllers/application-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | application', function(hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:application'); + assert.ok(controller); + }); +}); diff --git a/ui/desktop/tests/unit/routes/origin-test.js b/ui/desktop/tests/unit/routes/origin-test.js new file mode 100644 index 000000000..93e187a4c --- /dev/null +++ b/ui/desktop/tests/unit/routes/origin-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | origin', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:origin'); + assert.ok(route); + }); +}); diff --git a/ui/desktop/translations/form/en-us.yaml b/ui/desktop/translations/form/en-us.yaml index b126880d9..a4c393943 100644 --- a/ui/desktop/translations/form/en-us.yaml +++ b/ui/desktop/translations/form/en-us.yaml @@ -11,3 +11,6 @@ type: help: The type of the resource, to help differentiate schemas project: label: Project +origin: + label: Origin + description: The Boundary server origin (e.g. protocol://host) From 4495a846921cbb95fe695e8b756fe7bc814eb44e Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 13 Nov 2020 15:51:05 -0500 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20ipc=20mocks?= =?UTF-8?q?=20when=20not=20in=20electron=20environment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/desktop/mirage/config.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/ui/desktop/mirage/config.js b/ui/desktop/mirage/config.js index 475ff24eb..df5618c17 100644 --- a/ui/desktop/mirage/config.js +++ b/ui/desktop/mirage/config.js @@ -6,6 +6,38 @@ import { Response } from 'miragejs'; export default function() { + if (!config.isElectron) { + class MockIPC { + invoke(method, payload) { + console.log('MockIPC:', method, payload); + return this[method](payload); + } + + origin = null; + + getOrigin() { + return this.origin; + } + + setOrigin(origin) { + this.origin = origin; + return this.origin; + } + } + + const mockIPC = new MockIPC(); + + window.addEventListener('message', 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); + } + }); + } + // make this `http://localhost:8080`, for example, if your API is on a different server // this.urlPrefix = ''; // make this `/api`, for example, if your API is namespaced From d84d427ff550e44f6ea113fbee317f3b785a9be2 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 13 Nov 2020 17:10:56 -0500 Subject: [PATCH 3/3] =?UTF-8?q?test:=20=F0=9F=92=8D=20add=20missing=20auth?= =?UTF-8?q?=20navigation=20and=20setup=20initial=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/scopes/scope/authenticate.js | 17 ++ ui/desktop/app/templates/application.hbs | 13 ++ .../templates/scopes/scope/authenticate.hbs | 58 ++++++- .../scopes/scope/authenticate/index.hbs | 13 +- ui/desktop/mirage/config.js | 2 +- .../tests/acceptance/authentication-test.js | 152 ++++++++++++++++++ ui/desktop/tests/acceptance/origin-test.js | 133 +++++++++++++++ .../scopes/scope/authenticate-test.js | 12 ++ 8 files changed, 397 insertions(+), 3 deletions(-) create mode 100644 ui/desktop/app/controllers/scopes/scope/authenticate.js create mode 100644 ui/desktop/tests/acceptance/authentication-test.js create mode 100644 ui/desktop/tests/acceptance/origin-test.js create mode 100644 ui/desktop/tests/unit/controllers/scopes/scope/authenticate-test.js 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); + }); +});