Merge pull request #381 from hashicorp/feature-boundary-origin
Feature boundary origin
This commit is contained in:
21 files changed
+713
-14
No files matched your search
Whitespace-only changes.
@@ -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;
|
||||
}
|
||||
@@ -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'),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,4 +15,5 @@ Router.map(function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
this.route('origin');
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,9 +6,25 @@
|
||||
<Rose::Header class="{{if (is-loading) " loading"}}" as |header|>
|
||||
<header.brand @logo="logo-app" @text={{app-name}} />
|
||||
|
||||
<header.nav>
|
||||
{{outlet "header-nav"}}
|
||||
</header.nav>
|
||||
{{#if this.session.isAuthenticated}}
|
||||
<header.nav>
|
||||
{{outlet "header-nav"}}
|
||||
</header.nav>
|
||||
|
||||
<header.utilities as |utilities|>
|
||||
<utilities.dropdown
|
||||
@text={{this.session.data.authenticated.username}}
|
||||
as |dropdown|
|
||||
>
|
||||
<dropdown.button
|
||||
{{on "click" (route-action "invalidateSession")}}
|
||||
>
|
||||
{{t 'actions.deauthenticate'}}
|
||||
</dropdown.button>
|
||||
</utilities.dropdown>
|
||||
</header.utilities>
|
||||
{{/if}}
|
||||
|
||||
</Rose::Header>
|
||||
</layout.header>
|
||||
|
||||
@@ -17,3 +33,14 @@
|
||||
</layout.body>
|
||||
|
||||
</Rose::Layout::Global>
|
||||
|
||||
<EmberNotify @closeAfter={{this.notifyTimeout}} as |message close|>
|
||||
<Rose::Notification
|
||||
@style={{message.type}}
|
||||
@heading={{t (concat 'states.' message.type)}}
|
||||
@dismiss={{close}}
|
||||
@dismissText={{t 'actions.dismiss'}}
|
||||
>
|
||||
{{message.text}}
|
||||
</Rose::Notification>
|
||||
</EmberNotify>
|
||||
@@ -0,0 +1,19 @@
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
@onSubmit={{route-action "setOrigin" this.origin}}
|
||||
@cancel={{false}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="host"
|
||||
@type="text"
|
||||
@value={{this.origin}}
|
||||
@label={{t "form.origin.label"}}
|
||||
@helperText={{t "form.origin.description"}} />
|
||||
|
||||
<form.actions
|
||||
@submitText="Set Origin"
|
||||
@showCancel={{false}} />
|
||||
|
||||
</Rose::Form>
|
||||
@@ -1 +1,57 @@
|
||||
{{outlet}}
|
||||
{{page-title (t "actions.authenticate")}}
|
||||
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.header>
|
||||
<h2>{{t "actions.authenticate"}}</h2>
|
||||
<p>{{t "auth.descriptions.authenticating-with-org" name=@model.scope.displayName}}</p>
|
||||
</page.header>
|
||||
|
||||
<page.navigation>
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t "actions.choose-different-org"}}
|
||||
@icon="user-organization"
|
||||
as |dropdown|>
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model="global">
|
||||
{{t "titles.global"}}
|
||||
</dropdown.link>
|
||||
{{#each this.sortedScopes as |scope|}}
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model={{scope.id}}>
|
||||
{{scope.displayName}}
|
||||
</dropdown.link>
|
||||
{{/each}}
|
||||
</Rose::Dropdown>
|
||||
|
||||
<Rose::Separator />
|
||||
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
{{#each @model.authMethods as |authMethod|}}
|
||||
<nav.link @route="scopes.scope.authenticate.method" @model={{authMethod.id}}>
|
||||
{{t (concat "resources.auth-method.types." authMethod.type)}}
|
||||
</nav.link>
|
||||
{{/each}}
|
||||
</Rose::Nav::Tabs>
|
||||
</page.navigation>
|
||||
|
||||
<page.body>
|
||||
{{#if @model.authMethods}}
|
||||
{{outlet}}
|
||||
{{/if}}
|
||||
{{#unless @model.authMethods}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t "resources.auth-method.messages.none.title"}}
|
||||
@icon="alert-circle-outline"
|
||||
as |message|>
|
||||
<message.description>
|
||||
{{t "resources.auth-method.messages.none.description"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
</Rose::Layout::Centered>
|
||||
@@ -1 +1,12 @@
|
||||
{{outlet}}
|
||||
{{#unless @model}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t "resources.auth-method.messages.none.title"}}
|
||||
@icon="alert-circle-outline"
|
||||
as |message|>
|
||||
<message.description>
|
||||
{{t "resources.auth-method.messages.none.description"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -6,6 +6,38 @@ import { Response } from 'miragejs';
|
||||
|
||||
export default function() {
|
||||
|
||||
if (!config.isElectron && !Ember.testing) {
|
||||
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
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user