chore: 🤖 merge from main

This commit is contained in:
Randall Morey committed 2021-04-01 15:24:49 -04:00
commit c978958df2
41 files changed
+3534 -1001

No files matched your search

+3 -1
View File
@@ -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
+6
View File
@@ -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',
})
+6
View File
@@ -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';
+17 -4
View File
@@ -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',
+6 -2
View File
@@ -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'
);
});
});
+2 -2
View File
@@ -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
+14 -1
View File
@@ -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;
}
}
+1 -1
View File
@@ -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}}
&nbsp;
<Rose::Badge @style="success-outline">
{{t 'states.primary'}}
</Rose::Badge>
&nbsp;
{{/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>
+7
View File
@@ -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') {
+1
View File
@@ -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);
});
});
+5
View File
@@ -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)
+8 -4
View File
@@ -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();
});
+1 -1
View File
@@ -49,7 +49,7 @@ const generateMenuTemplate = () => {
{ role: 'reload' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomIn', accelerator: 'CommandOrControl+=' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' },
+1
View File
@@ -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({
+88 -16
View File
@@ -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);
});
});
+3 -3
View File
@@ -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);
});
});
+348
View File
@@ -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'
);
});
});
+19 -4
View File
@@ -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);
});
});
});
+1243 -929
View File
File diff suppressed because it is too large. Load diff