chore: 🤖 merge from main
This commit is contained in:
41 files changed
+3534
-1001
No files matched your search
@@ -1,5 +1,5 @@
|
||||
name: Validate Monorepo
|
||||
on: [push]
|
||||
on: pull_request
|
||||
jobs:
|
||||
|
||||
compliance:
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
@@ -56,6 +57,7 @@ jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Contributing to Boundary
|
||||
|
||||
Thank you for contributing to Boundary! Here you can find common questions around reporting issues and opening
|
||||
pull requests to our project.
|
||||
|
||||
When contributing in any way to the Boundary project (new issue, PR, etc), please be aware that our team identifies with many gender pronouns and to use non-binary pronouns when addressing our team. For more reading on our code of conduct, please see the [HashiCorp community guidelines](https://www.hashicorp.com/community-guidelines).
|
||||
@@ -7,6 +7,12 @@ import { attr } from '@ember-data/model';
|
||||
export default class GeneratedAuthMethodModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('boolean', {
|
||||
description: 'Specifies if this is the primary auth method for its scope.',
|
||||
readOnly: true,
|
||||
})
|
||||
primary;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource, to help differentiate schemas',
|
||||
})
|
||||
|
||||
@@ -7,6 +7,12 @@ import { attr } from '@ember-data/model';
|
||||
export default class GeneratedScopeModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The ID of the primary auth method for this scope. A primary auth method\nis allowed to vivify users when new accounts are created and is the source for the users account info.',
|
||||
})
|
||||
primary_auth_method_id;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource',
|
||||
})
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import GeneratedAuthMethodModel from '../generated/models/auth-method';
|
||||
import { equal } from '@ember/object/computed';
|
||||
|
||||
export default class AuthMethodModel extends GeneratedAuthMethodModel {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@equal('primary', true) isPrimary;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class AuthMethodSerializer extends ApplicationSerializer {
|
||||
/**
|
||||
* Sorts the array in order of "primary" such that the primary method appears
|
||||
* first. The "natural" order of auth methods places primary first.
|
||||
*
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeArrayResponse() {
|
||||
const normalized = super.normalizeArrayResponse(...arguments);
|
||||
normalized.data = normalized.data.sort((a) =>
|
||||
a.attributes.primary ? -1 : 1
|
||||
);
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/auth-method';
|
||||
@@ -60,7 +60,17 @@ export default function () {
|
||||
this.post('/auth-methods');
|
||||
this.get('/auth-methods/:id');
|
||||
this.patch('/auth-methods/:id');
|
||||
this.del('/auth-methods/:id');
|
||||
this.del('/auth-methods/:id', ({ authMethods }, { params: { id } }) => {
|
||||
const authMethod = authMethods.find(id);
|
||||
const scope = authMethod.scope;
|
||||
authMethod.destroy();
|
||||
// If the primary auth method is deleted, the associated scope's
|
||||
// primary_auth_method_id field should be set to null.
|
||||
if (scope.primaryAuthMethodId === id) {
|
||||
scope.update({ primaryAuthMethodId: null });
|
||||
}
|
||||
return new Response(204);
|
||||
});
|
||||
|
||||
// Auth Method Accounts
|
||||
this.get(
|
||||
@@ -413,9 +423,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',
|
||||
|
||||
@@ -20,18 +20,22 @@ export default function (server) {
|
||||
server.create('user', { id: 'authenticateduser' });
|
||||
|
||||
// Auth
|
||||
server.createList(
|
||||
const globalAuthMethods = server.createList(
|
||||
'auth-method',
|
||||
1,
|
||||
{ scope: globalScope },
|
||||
'withAccountsAndUsers'
|
||||
);
|
||||
server.createList(
|
||||
const orgAuthMethods = server.createList(
|
||||
'auth-method',
|
||||
3,
|
||||
{ scope: orgScope },
|
||||
'withAccountsAndUsers'
|
||||
);
|
||||
// Assign primary auth methods per scope
|
||||
// TODO make this generic
|
||||
globalScope.update({ primaryAuthMethodId: globalAuthMethods[0].id });
|
||||
orgScope.update({ primaryAuthMethodId: orgAuthMethods[1].id });
|
||||
|
||||
// Groups and Users
|
||||
server.createList('group', 1, { scope: globalScope }, 'withMembers');
|
||||
|
||||
@@ -2,4 +2,17 @@ import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'auth-method',
|
||||
|
||||
_hashForModel(model) {
|
||||
const json = ApplicationSerializer.prototype._hashForModel.apply(
|
||||
this,
|
||||
arguments
|
||||
);
|
||||
// If the scope marks this auth method as primary, designate it
|
||||
// as such with a read-only boolean field (this is how the API behaves).
|
||||
const isPrimary =
|
||||
model.scope && model.scope.primaryAuthMethodId === model.id;
|
||||
json.primary = isPrimary;
|
||||
return json;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | auth method', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('auth-method');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('auth-method', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
|
||||
test('it sorts primary first in normalizeArrayResponse', function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('auth-method');
|
||||
const modelClass = store.createRecord('auth-method').constructor;
|
||||
const payload = {
|
||||
items: [{ id: 1 }, { id: 2, primary: true }, { id: 3 }],
|
||||
};
|
||||
const normalizedArray = serializer.normalizeArrayResponse(
|
||||
store,
|
||||
modelClass,
|
||||
payload
|
||||
);
|
||||
assert.ok(payload.items[1].primary, 'Second payload item is primary');
|
||||
assert.deepEqual(
|
||||
normalizedArray,
|
||||
{
|
||||
included: [],
|
||||
data: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'auth-method',
|
||||
attributes: { primary: true },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'auth-method',
|
||||
attributes: {},
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'auth-method',
|
||||
attributes: {},
|
||||
relationships: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
'First normalized item is primary'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { getOwner } from '@ember/application';
|
||||
* confirmation via the confirmation service. If the user accepts, the
|
||||
* original method is executed. If the user denies, it is not.
|
||||
*/
|
||||
export function confirm(confirmationTextKey) {
|
||||
export function confirm(confirmationTextKey, options) {
|
||||
return function (_target, _propertyKey, desc) {
|
||||
const method = desc.value;
|
||||
desc.value = function () {
|
||||
@@ -15,7 +15,7 @@ export function confirm(confirmationTextKey) {
|
||||
const text = intl.t(confirmationTextKey);
|
||||
return (
|
||||
confirmService
|
||||
.confirm(text)
|
||||
.confirm(text, options)
|
||||
// If user confirmed, execute the decorated method
|
||||
.then(() => method.apply(this, arguments))
|
||||
// If the user denied, gracefully handle promise rejection with no-op
|
||||
|
||||
@@ -16,7 +16,7 @@ account:
|
||||
auth-method:
|
||||
title: Auth Method
|
||||
title_plural: Auth Methods
|
||||
description: Auth methods govern how an org's users can login to Boundary. Boundary’s default auth method is username & passwords.
|
||||
description: Auth methods allow users to authenticate within a scope.
|
||||
messages:
|
||||
welcome:
|
||||
title: Welcome to Auth Methods
|
||||
@@ -24,12 +24,25 @@ auth-method:
|
||||
none:
|
||||
title: No Auth Methods
|
||||
description: No auth methods were found for this scope.
|
||||
no-primary-auth-method:
|
||||
title: "Recommendation: Select a Primary Auth Method"
|
||||
description: A primary auth method auto-creates users when they authenticate for the first time. Scopes without primary auth methods may experience authentication failures.
|
||||
types:
|
||||
password: Password
|
||||
oidc: OIDC
|
||||
actions:
|
||||
create: Create Auth Method
|
||||
delete: Delete Auth Method
|
||||
make-primary: Make Primary
|
||||
remove-as-primary: Remove as Primary
|
||||
notifications:
|
||||
make-primary-success: Auth method successfully elected to primary.
|
||||
remove-as-primary-success: Auth method successfully removed as primary.
|
||||
questions:
|
||||
make-primary: Make primary auth method?
|
||||
remove-as-primary: Remove primary auth method?
|
||||
make-primary-confirm: "A primary auth method auto-creates users on their first authentication attempt if needed. Note: if you change the primary auth method, the previous primary will no longer auto-create users."
|
||||
remove-as-primary-confirm: Removing the primary auth method may lead to authentication failures for this scope.
|
||||
group:
|
||||
title: Group
|
||||
title_plural: Groups
|
||||
|
||||
@@ -7,3 +7,4 @@ copied: Copied
|
||||
connection:
|
||||
success:
|
||||
description: "Local proxy address ({protocol}): {address}:{port}"
|
||||
primary: Primary
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
&.rose-badge-outline {
|
||||
--border-color: var(--ui-gray-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-dark {
|
||||
@@ -40,6 +41,7 @@
|
||||
&.rose-badge-informational-outline {
|
||||
--color: var(--info-starker-1);
|
||||
--border-color: var(--info-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-alert {
|
||||
@@ -50,6 +52,7 @@
|
||||
&.rose-badge-alert-outline {
|
||||
--color: var(--alert-starker-1);
|
||||
--border-color: var(--alert-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-error {
|
||||
@@ -60,6 +63,7 @@
|
||||
&.rose-badge-error-outline {
|
||||
--color: var(--failure-starker-1);
|
||||
--border-color: var(--failure-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-success {
|
||||
@@ -70,6 +74,7 @@
|
||||
&.rose-badge-success-outline {
|
||||
--color: var(--success-starker-1);
|
||||
--border-color: var(--success-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-warning {
|
||||
@@ -80,5 +85,6 @@
|
||||
&.rose-badge-warning-outline {
|
||||
--color: var(--warning-starker-1);
|
||||
--border-color: var(--warning-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Controller from '@ember/controller';
|
||||
|
||||
export default class ScopesScopeAuthMethodsIndexController extends Controller {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* True if the current scope has a primary auth method set.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get hasPrimaryAuthMethod() {
|
||||
return Boolean(this.scopeModel.primary_auth_method_id);
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,67 @@ export default class ScopesScopeAuthMethodsRoute extends Route {
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('notifications.delete-success')
|
||||
async delete(authMethod) {
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
await authMethod.destroyRecord();
|
||||
// Reload the scope, since this is where the primary_auth_method_id is
|
||||
// stored. An auth method deletion could affect this field.
|
||||
await scopeModel.reload();
|
||||
await this.replaceWith('scopes.scope.auth-methods');
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Elects the specified auth method to primary for the current scope.
|
||||
* @param {AuthMethodModel} authMethod
|
||||
* @param {string} authMethod.id
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('resources.auth-method.questions.make-primary-confirm', {
|
||||
title: 'resources.auth-method.questions.make-primary',
|
||||
})
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess('resources.auth-method.notifications.make-primary-success')
|
||||
async makePrimary({ id }) {
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
scopeModel.primary_auth_method_id = id;
|
||||
// Attempt to save the change to the scope. If this operation fails,
|
||||
// we rollback the change and rethrow the error so the user can be notified.
|
||||
try {
|
||||
await scopeModel.save();
|
||||
} catch (e) {
|
||||
scopeModel.rollbackAttributes();
|
||||
throw e;
|
||||
}
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `primary_auth_method_id` field to null for the current scope
|
||||
* and saves it if and only if the specified auth method is in fact primary
|
||||
* for the current scope.
|
||||
* @param {AuthMethodModel} authMethod
|
||||
*/
|
||||
@action
|
||||
@loading
|
||||
@confirm('resources.auth-method.questions.remove-as-primary-confirm', {
|
||||
title: 'resources.auth-method.questions.remove-as-primary',
|
||||
})
|
||||
@notifyError(({ message }) => message, { catch: true })
|
||||
@notifySuccess(
|
||||
'resources.auth-method.notifications.remove-as-primary-success'
|
||||
)
|
||||
async removeAsPrimary(authMethod) {
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
scopeModel.primary_auth_method_id = null;
|
||||
// Attempt to save the change to the scope. If this operation fails,
|
||||
// we rollback the change and rethrow the error so the user can be notified.
|
||||
try {
|
||||
await scopeModel.save();
|
||||
} catch (e) {
|
||||
scopeModel.rollbackAttributes();
|
||||
throw e;
|
||||
}
|
||||
await authMethod.reload();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,15 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeAuthMethodsIndexRoute extends Route {}
|
||||
export default class ScopesScopeAuthMethodsIndexRoute extends Route {
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Adds the scope to the controller context.
|
||||
* @param {Controller} controller
|
||||
*/
|
||||
setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
const scopeModel = this.modelFor('scopes.scope');
|
||||
controller.scopeModel = scopeModel;
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@
|
||||
</Rose::Dialog>
|
||||
{{else}}
|
||||
<Rose::Dialog
|
||||
@heading={{t "actions.confirm"}}
|
||||
@heading={{if confirmation.options.title (t confirmation.options.title) (t "actions.confirm")}}
|
||||
@dismissButtonText={{t "actions.cancel"}}
|
||||
@icon="alert-triangle"
|
||||
@style="warning"
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
<Rose::Dropdown @text="{{t 'actions.manage'}}" @dropdownRight={{true}} as |dropdown|>
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
{{#unless @model.isPrimary}}
|
||||
<dropdown.button
|
||||
@disabled={{@model.canSave}}
|
||||
{{on "click" (route-action "makePrimary" @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.make-primary'}}
|
||||
</dropdown.button>
|
||||
{{/unless}}
|
||||
{{#if @model.isPrimary}}
|
||||
<dropdown.button
|
||||
@disabled={{@model.canSave}}
|
||||
{{on "click" (route-action "removeAsPrimary" @model)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.remove-as-primary'}}
|
||||
</dropdown.button>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<dropdown.link
|
||||
@route="scopes.scope.auth-methods.auth-method.accounts.new"
|
||||
@model={{@model}}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<h2>
|
||||
{{t "resources.auth-method.title"}}
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
{{#if @model.isPrimary}}
|
||||
|
||||
<Rose::Badge @style="success-outline">
|
||||
{{t 'states.primary'}}
|
||||
</Rose::Badge>
|
||||
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<DocLink @doc="auth-method" @iconSize="large" />
|
||||
</h2>
|
||||
<p>{{t "resources.auth-method.description"}}</p>
|
||||
|
||||
@@ -30,12 +30,27 @@
|
||||
<page.body>
|
||||
{{#if @model}}
|
||||
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
{{#unless this.hasPrimaryAuthMethod}}
|
||||
<Rose::Notification
|
||||
@style="warning"
|
||||
@heading={{t "resources.auth-method.messages.no-primary-auth-method.title"}}
|
||||
>
|
||||
{{t "resources.auth-method.messages.no-primary-auth-method.description"}}
|
||||
</Rose::Notification>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
<row.cell></row.cell>
|
||||
<row.headerCell>{{t 'titles.actions'}}</row.headerCell>
|
||||
{{/if}}
|
||||
</header.row>
|
||||
</table.header>
|
||||
|
||||
@@ -69,6 +84,39 @@
|
||||
<code>{{authMethod.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
{{#if (feature-flag 'primary-auth-method')}}
|
||||
<row.cell>
|
||||
{{#if authMethod.isPrimary}}
|
||||
<Rose::Badge @style="success-outline">
|
||||
{{t 'states.primary'}}
|
||||
</Rose::Badge>
|
||||
{{/if}}
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Dropdown
|
||||
@text={{t "actions.manage"}}
|
||||
@dropdownRight={{true}}
|
||||
@iconOnly={{true}}
|
||||
@showCaret={{false}}
|
||||
@icon="more-horizontal"
|
||||
as |dropdown|>
|
||||
{{#unless authMethod.isPrimary}}
|
||||
<dropdown.button
|
||||
{{on "click" (route-action "makePrimary" authMethod)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.make-primary'}}
|
||||
</dropdown.button>
|
||||
{{/unless}}
|
||||
{{#if authMethod.isPrimary}}
|
||||
<dropdown.button
|
||||
{{on "click" (route-action "removeAsPrimary" authMethod)}}
|
||||
>
|
||||
{{t 'resources.auth-method.actions.remove-as-primary'}}
|
||||
</dropdown.button>
|
||||
{{/if}}
|
||||
</Rose::Dropdown>
|
||||
</row.cell>
|
||||
{{/if}}
|
||||
</body.row>
|
||||
{{/each}}
|
||||
</table.body>
|
||||
|
||||
@@ -84,6 +84,10 @@ module.exports = function (environment) {
|
||||
'ember-cli-mirage': {
|
||||
directory: '../../addons/api/mirage',
|
||||
},
|
||||
|
||||
featureFlags: {
|
||||
'primary-auth-method': false,
|
||||
},
|
||||
};
|
||||
|
||||
// Unsafe policy is necessary in development and test environments, but should
|
||||
@@ -116,6 +120,9 @@ module.exports = function (environment) {
|
||||
// rather than automatically include API_HOST. Changes to CSP should
|
||||
// be explicit.
|
||||
if (API_HOST) ENV.contentSecurityPolicy['connect-src'].push(API_HOST);
|
||||
|
||||
// Enable features in development
|
||||
ENV.featureFlags['primary-auth-method'] = true;
|
||||
}
|
||||
|
||||
if (environment === 'test') {
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"ember-concurrency": "^1.3.0",
|
||||
"ember-exam": "^6.0.1",
|
||||
"ember-export-application-global": "^2.0.1",
|
||||
"ember-feature-flags": "^6.0.0",
|
||||
"ember-inline-svg": "^1.0.0",
|
||||
"ember-load-initializers": "^2.1.2",
|
||||
"ember-maybe-import-regenerator": "^0.1.6",
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
// //currentSession,
|
||||
// //invalidateSession,
|
||||
} from 'ember-simple-auth/test-support';
|
||||
import { enableFeature } from 'ember-feature-flags/test-support';
|
||||
|
||||
module('Acceptance | auth methods', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -270,4 +271,118 @@ module('Acceptance | auth methods', function (hooks) {
|
||||
'Displays field-level errors.'
|
||||
);
|
||||
});
|
||||
|
||||
test('user can make primary an auth method', async function (assert) {
|
||||
assert.expect(2);
|
||||
assert.notOk(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is not yet set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethod);
|
||||
await click(
|
||||
'.rose-layout-page-actions .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
const scope = this.server.schema.scopes.find(instances.scopes.org.id);
|
||||
assert.equal(
|
||||
scope.primaryAuthMethodId,
|
||||
instances.authMethod.id,
|
||||
'Primary auth method is set.'
|
||||
);
|
||||
});
|
||||
|
||||
test('user is notified of error on make primary an auth method', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.patch('/scopes/:id', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'fail',
|
||||
message: 'Sorry!',
|
||||
}
|
||||
);
|
||||
});
|
||||
assert.notOk(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is not yet set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethod);
|
||||
await click(
|
||||
'.rose-layout-page-actions .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
assert.ok(find('.rose-notification'));
|
||||
});
|
||||
|
||||
test('user can remove as primary an auth method', async function (assert) {
|
||||
assert.expect(2);
|
||||
instances.scopes.org.update({
|
||||
primaryAuthMethodId: instances.authMethod.id,
|
||||
});
|
||||
assert.ok(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethod);
|
||||
await click(
|
||||
'.rose-layout-page-actions .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
const scope = this.server.schema.scopes.find(instances.scopes.org.id);
|
||||
assert.notOk(scope.primaryAuthMethodId, 'Primary auth method is unset.');
|
||||
});
|
||||
|
||||
test('user is notified of error on remove as primary an auth method', async function (assert) {
|
||||
assert.expect(2);
|
||||
this.server.patch('/scopes/:id', () => {
|
||||
return new Response(
|
||||
400,
|
||||
{},
|
||||
{
|
||||
status: 400,
|
||||
code: 'fail',
|
||||
message: 'Sorry!',
|
||||
}
|
||||
);
|
||||
});
|
||||
instances.scopes.org.update({
|
||||
primaryAuthMethodId: instances.authMethod.id,
|
||||
});
|
||||
assert.ok(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethod);
|
||||
await click(
|
||||
'.rose-layout-page-actions .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
assert.ok(find('.rose-notification'));
|
||||
});
|
||||
|
||||
test('user can make and remove primary auth methods from index', async function (assert) {
|
||||
assert.expect(3);
|
||||
assert.notOk(
|
||||
instances.scopes.org.primaryAuthMethodId,
|
||||
'Primary auth method is not yet set.'
|
||||
);
|
||||
enableFeature('primary-auth-method');
|
||||
await visit(urls.authMethods);
|
||||
await click(
|
||||
'.rose-table-body .rose-table-row:first-child .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
let scope = this.server.schema.scopes.find(instances.scopes.org.id);
|
||||
assert.equal(
|
||||
scope.primaryAuthMethodId,
|
||||
instances.authMethod.id,
|
||||
'Primary auth method is set.'
|
||||
);
|
||||
await click(
|
||||
'.rose-table-body .rose-table-row:first-child .rose-dropdown-content [type="button"]:first-child'
|
||||
);
|
||||
scope = this.server.schema.scopes.find(instances.scopes.org.id);
|
||||
assert.notOk(scope.primaryAuthMethodId, 'Primary auth method is unset.');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/auth-methods/index', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
'controller:scopes/scope/auth-methods/index'
|
||||
);
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
Canonical reference for changes, improvements, and bugfixes for Boundary Desktop.
|
||||
|
||||
## Next
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- ui: Trigger zoom-in menu option on keyboard shortcut. ([PR](https://github.com/hashicorp/boundary-ui/pull/505))
|
||||
|
||||
## v1.0.0-beta [CLI 0.1.8] (2021.03.10)
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ app.on('ready', async () => {
|
||||
// Register custom protocol
|
||||
// This file protocol is the exclusive protocol for this application. All
|
||||
// other protocols are disabled.
|
||||
ses.protocol.registerFileProtocol(emberAppProtocol, (request, callback) => { /* eng-disable PROTOCOL_HANDLER_JS_CHECK */
|
||||
ses.protocol.registerFileProtocol(emberAppProtocol, (request, callback) => {
|
||||
/* eng-disable PROTOCOL_HANDLER_JS_CHECK */
|
||||
const isDir = request.url.endsWith('/');
|
||||
const absolutePath = request.url.substr(emberAppURL.length);
|
||||
const normalizedPath = isDir
|
||||
@@ -71,7 +72,8 @@ app.on('ready', async () => {
|
||||
|
||||
// Disallow all permissions requests,
|
||||
// per Electronegativity PERMISSION_REQUEST_HANDLER_GLOBAL_CHECK
|
||||
ses.setPermissionRequestHandler((webContents, permission, callback) => { /* eng-disable PERMISSION_REQUEST_HANDLER_JS_CHECK */
|
||||
ses.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||
/* eng-disable PERMISSION_REQUEST_HANDLER_JS_CHECK */
|
||||
return callback(false);
|
||||
});
|
||||
|
||||
@@ -151,10 +153,12 @@ app.on('ready', async () => {
|
||||
|
||||
// Prevent navigation outside of serve://boundary per
|
||||
// Electronegativity LIMIT_NAVIGATION_GLOBAL_CHECK
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => { /* eng-disable LIMIT_NAVIGATION_JS_CHECK */
|
||||
mainWindow.webContents.on('will-navigate', (event, url) => {
|
||||
/* eng-disable LIMIT_NAVIGATION_JS_CHECK */
|
||||
if (!url.startsWith('serve://boundary')) event.preventDefault();
|
||||
});
|
||||
mainWindow.webContents.on('new-window', (event, url) => { /* eng-disable LIMIT_NAVIGATION_JS_CHECK */
|
||||
mainWindow.webContents.on('new-window', (event, url) => {
|
||||
/* eng-disable LIMIT_NAVIGATION_JS_CHECK */
|
||||
if (!url.startsWith('serve://boundary')) event.preventDefault();
|
||||
});
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ const generateMenuTemplate = () => {
|
||||
{ role: 'reload' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomIn', accelerator: 'CommandOrControl+=' },
|
||||
{ role: 'zoomOut' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -2,9 +2,9 @@ import { module, test } from 'qunit';
|
||||
import {
|
||||
visit,
|
||||
currentURL,
|
||||
//fillIn,
|
||||
//click,
|
||||
//find,
|
||||
fillIn,
|
||||
click,
|
||||
find,
|
||||
//findAll,
|
||||
//getRootElement
|
||||
//setupOnerror,
|
||||
@@ -12,14 +12,16 @@ 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 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 +45,7 @@ module('Acceptance | authentication', function (hooks) {
|
||||
const stubs = {
|
||||
global: null,
|
||||
org: null,
|
||||
ipcService: null,
|
||||
};
|
||||
|
||||
const urls = {
|
||||
@@ -54,8 +57,11 @@ module('Acceptance | authentication', function (hooks) {
|
||||
},
|
||||
authenticate: {
|
||||
global: null,
|
||||
org: null,
|
||||
methods: {
|
||||
global: null,
|
||||
},
|
||||
},
|
||||
projects: null,
|
||||
targets: null,
|
||||
};
|
||||
|
||||
@@ -76,13 +82,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 +98,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 +118,8 @@ module('Acceptance | authentication', function (hooks) {
|
||||
this.origin = origin;
|
||||
return this.origin;
|
||||
}
|
||||
|
||||
resetOrigin() {}
|
||||
}
|
||||
|
||||
mockIPC = new MockIPC();
|
||||
@@ -132,13 +137,80 @@ 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(3);
|
||||
assert.notOk(mockIPC.origin);
|
||||
await visit(urls.origin);
|
||||
await fillIn('[name="host"]', window.location.origin);
|
||||
await click('[type="submit"]');
|
||||
assert.equal(currentURL(), urls.authenticate.methods.global);
|
||||
assert.equal(mockIPC.origin, window.location.origin);
|
||||
});
|
||||
|
||||
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(currentURL(), urls.authenticate.methods.global);
|
||||
assert.equal(mockIPC.origin, window.location.origin);
|
||||
await click('.change-origin a');
|
||||
assert.equal(currentURL(), urls.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 fillIn('[name="host"]', window.location.origin);
|
||||
await click('[type="submit"]');
|
||||
assert.ok(find('.rose-notification.is-error'));
|
||||
});
|
||||
|
||||
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);
|
||||
this.server.get('/targets', () => new Response(500));
|
||||
await visit(urls.origin);
|
||||
await fillIn('[name="host"]', window.location.origin);
|
||||
await click('[type="submit"]');
|
||||
await click('[type="submit"]');
|
||||
assert.ok(currentSession().isAuthenticated);
|
||||
assert.equal(find('main section button').textContent.trim(), 'Disconnect');
|
||||
await click('main section button');
|
||||
assert.notOk(currentSession().isAuthenticated);
|
||||
assert.equal(currentURL(), urls.authenticate.methods.global);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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`<BrandedCard />`);
|
||||
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`
|
||||
<BrandedCard>
|
||||
template block text
|
||||
</BrandedCard>
|
||||
`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), 'template block text');
|
||||
test('it renders with content', async function (assert) {
|
||||
await render(hbs`<BrandedCard
|
||||
@title="title"
|
||||
@description="description"
|
||||
/>`);
|
||||
assert.equal(find('.branded-card-title').textContent.trim(), 'title');
|
||||
assert.equal(
|
||||
find('.branded-card-description').textContent.trim(),
|
||||
'description'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user