feat: 🎸 CRUDL capabilites for ldap managed-group (#1805)

* feat: 🎸 CRUDL capabilites for ldap managed-group

✅ Closes: ICU-10100

* refactor: 💡 managed-group serializer & remove members tab

* refactor: 💡 show members tab

* fix: 🐛 add type getters in managed group model

* refactor: 💡 fix column name
This commit is contained in:
Lisbet Alvarez authored and GitHub committed 2023-07-28 13:52:04 -07:00
1 parent e5a026ed34
commit d49f3b72a6
19 files changed
+848 -301

No files matched your search

+15
View File
@@ -6,6 +6,7 @@
import GeneratedManagedGroupModel from '../generated/models/managed-group';
import { attr } from '@ember-data/model';
import { A } from '@ember/array';
import { TYPE_AUTH_METHOD_OIDC, TYPE_AUTH_METHOD_LDAP } from './auth-method';
export default class ManagedGroupModel extends GeneratedManagedGroupModel {
// =attributes
@@ -20,4 +21,18 @@ export default class ManagedGroupModel extends GeneratedManagedGroupModel {
emptyArrayIfMissing: true,
})
member_ids;
/**
* @type {boolean}
*/
get isOIDC() {
return this.type === TYPE_AUTH_METHOD_OIDC;
}
/**
* @type {boolean}
*/
get isLDAP() {
return this.type === TYPE_AUTH_METHOD_LDAP;
}
}
@@ -11,6 +11,10 @@ import {
export default class ManagedGroupSerializer extends ApplicationSerializer {
// =properties
/**
* @type {boolean}
*/
serializeScopeID = false;
/**
* Delegates to a type-specific serialization, or default.
@@ -794,6 +794,10 @@ managed-group:
messages:
title: Welcome to Members
description: No members in this managed group.
form:
group_names:
label: Group Names
help: The list of groups that this Managed Group belongs to.
worker:
title: Worker
title_pki: New PKI Worker
@@ -71,7 +71,7 @@
{{/if}}
{{#if (can 'create model' @model collection='managed-groups')}}
{{#if @model.isOIDC}}
{{#if (or @model.isOIDC @model.isLDAP)}}
<dropdown.link
@route='scopes.scope.auth-methods.auth-method.managed-groups.new'
@model={{@model}}
@@ -15,7 +15,7 @@
{{/if}}
{{#if (can 'navigate model' @model collection='managed-groups')}}
{{#if @model.isOIDC}}
{{#if (or @model.isOIDC @model.isLDAP)}}
<nav.link @route='scopes.scope.auth-methods.auth-method.managed-groups'>
{{t 'resources.managed-group.title_plural'}}
</nav.link>
@@ -3,81 +3,14 @@
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<form.input
@value={{@model.type}}
@label={{t 'form.type.label'}}
readonly={{true}}
@disabled={{true}}
/>
<form.input
@name='name'
@type='name'
@value={{@model.name}}
@label={{t 'form.name.label'}}
@helperText={{t 'form.name.help'}}
@error={{@model.error.name}}
readonly={{false}}
as |field|
>
{{#if @model.errors.name}}
<field.errors as |errors|>
{{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<Hds::Form::Textarea::Field
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
<form.input
@name='filter'
@type='text'
@value={{@model.filter_string}}
@label={{t 'form.filter.label'}}
@error={{@model.errors.filter_string}}
@helperText={{t 'form.filter.help'}}
as |field|
>
{{#if @model.errors.filter_string}}
<field.errors as |errors|>
{{#each @model.errors.filter_string as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
{{#if @model.type}}
{{component
(concat 'form/managed-group/' @model.type)
model=@model
edit=@edit
submit=@submit
cancel=@cancel
removeItemByIndex=@removeItemByIndex
addStringItem=@addStringItem
}}
{{/if}}
@@ -0,0 +1,134 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Form
@edit={{@edit}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<InfoField @value={{@model.type}} disabled={{form.disabled}} as |F|>
<F.Label>{{t 'form.type.label'}}</F.Label>
</InfoField>
<Hds::Form::TextInput::Field
@isOptional={{true}}
name='name'
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
<Hds::Form::Fieldset @isRequired={{true}} as |F|>
<F.Legend>{{t 'resources.managed-group.form.group_names.label'}}</F.Legend>
<F.HelperText>{{t
'resources.managed-group.form.group_names.help'
}}</F.HelperText>
{{#let (unique-id) as |valueHeaderID|}}
<F.Control>
<Hds::Table class='key-value-list' name='group_names'>
<:head as |H|>
<H.Tr>
<H.Th id={{valueHeaderID}}>{{t 'titles.value'}}</H.Th>
<H.Th>{{t 'titles.actions'}}</H.Th>
</H.Tr>
</:head>
<:body as |B|>
{{#each @model.group_names as |group i|}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{group.value}}
disabled={{form.disabled}}
aria-labelledby={{valueHeaderID}}
{{on 'input' (set-from-event group 'value')}}
/>
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.remove'}}
@color='critical'
@icon='trash'
@isIconOnly={{true}}
disabled={{form.disabled}}
{{on 'click' (fn @removeItemByIndex 'group_names' i)}}
/>
</B.Td>
</B.Tr>
{{/each}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{this.newGroup}}
@type='text'
disabled={{form.disabled}}
aria-labelledby={{valueHeaderID}}
{{on 'input' (set-from-event this 'newGroup')}}
/>
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.add'}}
@color='secondary'
disabled={{or form.disabled (not this.newGroup)}}
{{on 'click' this.addGroupName}}
/>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
</F.Control>
{{/let}}
{{#if @model.errors.group_names}}
<F.Error as |E|>
{{#each @model.errors.group_names as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Fieldset>
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,24 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class FormManagedGroupLdapComponent extends Component {
// =attributes
/**
* @type {string}
*/
@tracked newGroup = '';
@action
addGroupName() {
if (this.newGroup) {
this.args.addStringItem('group_names', this.newGroup);
}
this.newGroup = '';
}
}
@@ -0,0 +1,83 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<form.input
@value={{@model.type}}
@label={{t 'form.type.label'}}
readonly={{true}}
@disabled={{true}}
/>
<form.input
@name='name'
@type='name'
@value={{@model.name}}
@label={{t 'form.name.label'}}
@helperText={{t 'form.name.help'}}
@error={{@model.error.name}}
readonly={{false}}
as |field|
>
{{#if @model.errors.name}}
<field.errors as |errors|>
{{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<Hds::Form::Textarea::Field
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
<form.input
@name='filter'
@type='text'
@value={{@model.filter_string}}
@label={{t 'form.filter.label'}}
@error={{@model.errors.filter_string}}
@helperText={{t 'form.filter.help'}}
as |field|
>
{{#if @model.errors.filter_string}}
<field.errors as |errors|>
{{#each @model.errors.filter_string as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -4,6 +4,7 @@
*/
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupRoute extends Route {
@@ -24,4 +25,21 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupRo
reload: true,
});
}
/**
* Copies the contents of string array fields in order to force the instance
* into a dirty state. This ensures that `model.rollbackAttributes()` reverts
* to the original expected array.
*
* The deep copy implemented here is required to ensure that both the
* array itself and its members are all new.
*
* @param {managedGroupModel} managedGroup
*/
@action
edit(managedGroup) {
if (managedGroup.group_names) {
managedGroup.group_names = structuredClone(managedGroup.group_names);
}
}
}
@@ -84,7 +84,7 @@
<Hds::Table
@model={{@model.accounts}}
@columns={{array
(hash label=(t 'resources.account.form.login_name.label'))
(hash label=(t 'form.name.label'))
(hash label=(t 'resources.account.form.email.label'))
(hash label=(t 'resources.account.form.full_name.label'))
(hash label=(t 'resources.account.form.dn.label'))
@@ -23,48 +23,89 @@
<page.body>
{{#if @model.managedGroups}}
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
<row.headerCell>{{t 'form.description.label'}}</row.headerCell>
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @model.managedGroups as |managed-group|}}
<body.row as |row|>
<row.headerCell>
{{#if (can 'read model' managed-group)}}
{{#if @model.managedGroups.firstObject.isOIDC}}
<Hds::Table
@model={{@model.managedGroups}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.description.label'))
(hash label=(t 'form.type.label'))
(hash label=(t 'form.id.label'))
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{#if (can 'read model' B.data)}}
<LinkTo
@route='scopes.scope.auth-methods.auth-method.managed-groups.managed-group'
@model={{managed-group.id}}
@model={{B.data.id}}
>
{{managed-group.displayName}}
{{B.data.displayName}}
</LinkTo>
{{else}}
{{managed-group.displayName}}
{{B.data.displayName}}
{{/if}}
</row.headerCell>
<row.cell>{{managed-group.description}}</row.cell>
<row.cell>
<Hds::Badge @text={{managed-group.type}} />
</row.cell>
<row.cell>
</B.Td>
<B.Td>{{B.data.description}}</B.Td>
<B.Td><Hds::Badge @text={{B.data.type}} /></B.Td>
<B.Td>
<Copyable
@text={{managed-group.id}}
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{managed-group.id}}</code>
<code>{{B.data.id}}</code>
</Copyable>
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{else}}
<Hds::Table
@model={{@model.managedGroups}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.description.label'))
(hash label=(t 'form.type.label'))
(hash label=(t 'form.id.label'))
(hash label=(t 'resources.managed-group.form.group_names.label'))
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{#if (can 'read model' B.data)}}
<LinkTo
@route='scopes.scope.auth-methods.auth-method.managed-groups.managed-group'
@model={{B.data.id}}
>
{{B.data.displayName}}
</LinkTo>
{{else}}
{{B.data.displayName}}
{{/if}}
</B.Td>
<B.Td>{{B.data.description}}</B.Td>
<B.Td><Hds::Badge @text={{B.data.type}} /></B.Td>
<B.Td>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{B.data.id}}</code>
</Copyable>
</B.Td>
<B.Td>
{{truncate-list 'actions.more' B.data.group_names 3}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{/if}}
{{else}}
<Rose::Layout::Centered>
<Rose::Message
@@ -30,8 +30,11 @@
<page.body>
<Form::ManagedGroup
@model={{@model}}
@edit={{route-action 'edit' @model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@removeItemByIndex={{route-action 'removeItemByIndex' @model}}
@addStringItem={{route-action 'addStringItem' @model}}
/>
</page.body>
@@ -31,53 +31,73 @@
<page.body>
{{#if @model.members}}
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
<row.headerCell>{{t 'form.issuer.label'}}</row.headerCell>
<row.headerCell>{{t 'form.subject.label'}}</row.headerCell>
<row.headerCell>{{t 'form.email.label'}}</row.headerCell>
<row.headerCell>{{t 'form.full_name.label'}}</row.headerCell>
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @model.members as |member|}}
<body.row as |row|>
<row.headerCell scope='row'>
{{member.accountName}}
<p>{{member.description}}</p>
</row.headerCell>
<row.cell>
<code>{{member.issuer}}</code>
</row.cell>
<row.cell>
<code>{{member.subject}}</code>
</row.cell>
<row.cell>
<code>{{member.email}}</code>
</row.cell>
<row.cell>
{{member.full_name}}
</row.cell>
<row.cell>
<Hds::Badge @text={{member.type}} />
</row.cell>
<row.cell>
{{#if @model.members.firstObject.isOIDC}}
<Hds::Table
@model={{@model.members}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.issuer.label'))
(hash label=(t 'form.subject.label'))
(hash label=(t 'form.email.label'))
(hash label=(t 'form.full_name.label'))
(hash label=(t 'form.type.label'))
(hash label=(t 'form.id.label'))
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{B.data.accountName}}
<p>{{B.data.description}}</p>
</B.Td>
<B.Td>{{B.data.issuer}}</B.Td>
<B.Td>{{B.data.subject}}</B.Td>
<B.Td>{{B.data.email}}</B.Td>
<B.Td>{{B.data.full_name}}</B.Td>
<B.Td><Hds::Badge @text={{B.data.type}} /></B.Td>
<B.Td>
<Copyable
@text={{member.id}}
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{member.id}}</code>
<code>{{B.data.id}}</code>
</Copyable>
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{else}}
<Hds::Table
@model={{@model.members}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'resources.account.form.email.label'))
(hash label=(t 'resources.account.form.full_name.label'))
(hash label=(t 'resources.account.form.dn.label'))
(hash label=(t 'resources.account.form.member_of_groups.label'))
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{B.data.displayName}}
<p>{{B.data.description}}</p>
</B.Td>
<B.Td>{{B.data.email}}</B.Td>
<B.Td>{{B.data.full_name}}</B.Td>
<B.Td>{{B.data.dn}}</B.Td>
<B.Td>{{truncate-list
'actions.more'
B.data.member_of_groups
3
}}</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{/if}}
{{else}}
<Rose::Layout::Centered>
<Rose::Message
@@ -12,8 +12,7 @@
<page.header>
<h2>
{{t 'titles.new'}}
{{! TODO: change the icon link }}
<DocLink @doc='account.new' @iconSize='large' />
<DocLink @doc='managed-groups' @iconSize='large' />
</h2>
</page.header>
@@ -22,6 +21,8 @@
@model={{@model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@removeItemByIndex={{route-action 'removeItemByIndex' @model}}
@addStringItem={{route-action 'addStringItem' @model}}
/>
</page.body>
@@ -4,32 +4,48 @@
*/
import { module, test } from 'qunit';
import { visit, currentURL, click, find, fillIn } from '@ember/test-helpers';
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
import {
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Acceptance | managed-groups | create', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let getManagedGroupCount;
let featuresService;
const SAVE_BTN_SELECTOR = '.rose-form-actions [type="submit"]';
const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
const NAME_INPUT_SELECTOR = '[name="name"]';
const DESC_INPUT_SELECTOR = '[name="description"]';
const ERROR_MSG_SELECTOR = '.rose-notification-body';
const FIELD_ERROR_TEXT_SELECTOR = '.hds-form-error__message';
const instances = {
scopes: {
global: null,
org: null,
},
authMethod: null,
managedGroup: null,
ldapAuthMethod: null,
};
const urls = {
orgScope: null,
authMethods: null,
authMethod: null,
ldapAuthMethod: null,
managedGroups: null,
ldapManagedGroups: null,
newManagedGroup: null,
managedGroup: null,
newLdapManagedGroup: null,
};
hooks.beforeEach(function () {
@@ -43,93 +59,126 @@ module('Acceptance | managed-groups | create', function (hooks) {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_OIDC,
});
instances.managedGroup = this.server.create('managed-group', {
instances.ldapAuthMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
authMethod: instances.authMethod,
type: TYPE_AUTH_METHOD_LDAP,
});
// Generate route URLs for resources
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.authMethods = `${urls.orgScope}/auth-methods`;
urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`;
urls.ldapAuthMethod = `${urls.authMethods}/${instances.ldapAuthMethod.id}`;
urls.managedGroups = `${urls.authMethod}/managed-groups`;
urls.ldapManagedGroups = `${urls.ldapAuthMethod}/managed-groups`;
urls.newManagedGroup = `${urls.managedGroups}/new`;
urls.managedGroup = `${urls.managedGroups}/${instances.authMethod.id}`;
});
urls.newLdapManagedGroup = `${urls.ldapManagedGroups}/new`;
hooks.afterEach(async function () {
const notification = find('.rose-notification');
if (notification) {
await click('.rose-notification-dismiss');
}
});
test('visiting managed groups', async function (assert) {
assert.expect(1);
await visit(urls.managedGroups);
await a11yAudit();
assert.strictEqual(currentURL(), urls.managedGroups);
getManagedGroupCount = () =>
this.server.schema.managedGroups.all().models.length;
featuresService = this.owner.lookup('service:features');
});
test('can create a new managed group', async function (assert) {
assert.expect(1);
const managedGroupsCount = this.server.db.managedGroups.length;
await visit(urls.newManagedGroup);
await fillIn('[name="name"]', 'Managed group name');
await fillIn('[name="description"]', 'description');
await click('form [type="submit"]:not(:disabled)');
assert.strictEqual(
this.server.db.managedGroups.length,
managedGroupsCount + 1
);
});
test('User can not create a new managed group without proper authorization', async function (assert) {
assert.expect(2);
instances.authMethod.authorized_collection_actions['managed-groups'] = [];
assert.expect(3);
const managedGroupsCount = getManagedGroupCount();
const name = 'Managed group name';
await visit(urls.authMethod);
assert.notOk(
instances.authMethod.authorized_collection_actions[
'managed-groups'
].includes('create')
);
assert.notOk(
find(`.rose-layout-page-actions [href="${urls.newManagedGroup}"]`)
);
await click(`[href="${urls.newManagedGroup}"]`);
await fillIn(NAME_INPUT_SELECTOR, name);
await fillIn(DESC_INPUT_SELECTOR, 'description');
await click(SAVE_BTN_SELECTOR);
const managedGroup = this.server.schema.managedGroups.findBy({ name });
assert.strictEqual(managedGroup.name, name);
assert.strictEqual(managedGroup.description, 'description');
assert.strictEqual(getManagedGroupCount(), managedGroupsCount + 1);
});
test('User can navigate to new managed group route with proper authorization', async function (assert) {
assert.expect(2);
await visit(urls.managedGroups);
assert.ok(
instances.authMethod.authorized_collection_actions[
'managed-groups'
].includes('create')
);
assert.ok(find(`[href="${urls.newManagedGroup}"]`));
test('can create a new ldap managed group', async function (assert) {
assert.expect(4);
const managedGroupsCount = getManagedGroupCount();
const name = 'Managed group name';
await visit(urls.ldapAuthMethod);
await click(`[href="${urls.newLdapManagedGroup}"]`);
await fillIn(NAME_INPUT_SELECTOR, name);
await fillIn(DESC_INPUT_SELECTOR, 'description');
await fillIn('[name="group_names"] input', 'group name');
await click('[name="group_names"] button');
await click(SAVE_BTN_SELECTOR);
const managedGroup = this.server.schema.managedGroups.findBy({ name });
assert.strictEqual(managedGroup.name, name);
assert.strictEqual(managedGroup.description, 'description');
assert.deepEqual(managedGroup.attributes.group_names, ['group name']);
assert.strictEqual(getManagedGroupCount(), managedGroupsCount + 1);
});
test('User cannot navigate to new managed group route without proper authorization', async function (assert) {
test('User cannot create a new managed group without proper authorization', async function (assert) {
assert.expect(2);
instances.authMethod.authorized_collection_actions['managed-groups'] = [];
await visit(urls.managedGroups);
assert.notOk(
instances.authMethod.authorized_collection_actions['managed-groups'] =
instances.authMethod.authorized_collection_actions[
'managed-groups'
].filter((item) => item !== 'create');
await visit(urls.authMethods);
await click(`[href="${urls.authMethod}"]`);
assert.false(
instances.authMethod.authorized_collection_actions[
'managed-groups'
].includes('create')
);
assert.notOk(find(`[href="${urls.newManagedGroup}"]`));
assert.dom(`[href="${urls.newManagedGroup}"]`).doesNotExist();
});
test('User cannot create a new ldap managed group without proper authorization', async function (assert) {
assert.expect(2);
featuresService.enable('ldap-auth-methods');
instances.ldapAuthMethod.authorized_collection_actions['managed-groups'] =
instances.ldapAuthMethod.authorized_collection_actions[
'managed-groups'
].filter((item) => item !== 'create');
await visit(urls.authMethods);
await click(`[href="${urls.ldapAuthMethod}"]`);
assert.false(
instances.ldapAuthMethod.authorized_collection_actions[
'managed-groups'
].includes('create')
);
assert.dom(`[href="${urls.newLdapManagedGroup}"]`).doesNotExist();
});
test('User can cancel a new managed group creation', async function (assert) {
assert.expect(2);
const managedGroupsCount = this.server.db.managedGroups.length;
await visit(urls.newManagedGroup);
await fillIn('[name="name"]', 'Managed group name');
await click('form button:not([type="submit"])');
assert.strictEqual(this.server.db.managedGroups.length, managedGroupsCount);
const managedGroupsCount = getManagedGroupCount();
await visit(urls.authMethod);
await click(`[href="${urls.newManagedGroup}"]`);
await fillIn(NAME_INPUT_SELECTOR, 'Managed group name');
await click(CANCEL_BTN_SELECTOR);
assert.strictEqual(getManagedGroupCount(), managedGroupsCount);
assert.strictEqual(currentURL(), urls.managedGroups);
});
test('User can cancel a new ldap managed group creation', async function (assert) {
assert.expect(2);
const managedGroupsCount = getManagedGroupCount();
await visit(urls.ldapAuthMethod);
await click(`[href="${urls.newLdapManagedGroup}"]`);
await fillIn(NAME_INPUT_SELECTOR, 'Managed group name');
await click(CANCEL_BTN_SELECTOR);
assert.strictEqual(getManagedGroupCount(), managedGroupsCount);
assert.strictEqual(currentURL(), urls.ldapManagedGroups);
});
test('When user saving a new managed group with invalid fields displays error message', async function (assert) {
assert.expect(2);
this.server.post('/managed-groups', () => {
@@ -151,19 +200,46 @@ module('Acceptance | managed-groups | create', function (hooks) {
}
);
});
await visit(urls.newManagedGroup);
await fillIn('[name="name"]', 'new managed group');
await click('form [type="submit"]');
await visit(urls.authMethod);
await click(`[href="${urls.newManagedGroup}"]`);
await fillIn(NAME_INPUT_SELECTOR, 'new managed group');
await click(SAVE_BTN_SELECTOR);
await a11yAudit();
assert.strictEqual(
find('.rose-notification-body').textContent.trim(),
'The request was invalid.',
'Displays primary error message.'
);
assert.strictEqual(
find('.rose-form-error-message').textContent.trim(),
'Name is required.',
'Displays field-level errors.'
);
assert.dom(ERROR_MSG_SELECTOR).hasText('The request was invalid.');
assert.dom('.rose-form-error-message').hasText('Name is required.');
});
test('When user saving a new ldap managed group with invalid fields displays error message', async function (assert) {
assert.expect(2);
this.server.post('/managed-groups', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {
request_fields: [
{
name: 'name',
description: 'Name is required.',
},
],
},
}
);
});
await visit(urls.ldapAuthMethod);
await click(`[href="${urls.newLdapManagedGroup}"]`);
await fillIn(NAME_INPUT_SELECTOR, 'new managed group');
await click(SAVE_BTN_SELECTOR);
await a11yAudit();
assert.dom(ERROR_MSG_SELECTOR).hasText('The request was invalid.');
assert.dom(FIELD_ERROR_TEXT_SELECTOR).hasText('Name is required.');
});
});
@@ -4,32 +4,47 @@
*/
import { module, test } from 'qunit';
import { visit, click, find } from '@ember/test-helpers';
import { visit, click, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
import {
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Acceptance | managed-groups | delete', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let getManagedGroupCount;
const DELETE_DROPDOWN_SELECTOR =
'.rose-layout-page-actions .rose-dropdown-button-danger';
const ERROR_MSG_SELECTOR = '.rose-notification-body';
const instances = {
scopes: {
global: null,
org: null,
},
authMethod: null,
ldapAuthMethod: null,
managedGroup: null,
ldapManagedGroup: null,
};
const urls = {
orgScope: null,
authMethods: null,
authMethod: null,
ldapAuthMethod: null,
managedGroups: null,
newManagedGroup: null,
ldapManagedGroups: null,
managedGroup: null,
ldapManagedGroup: null,
};
hooks.beforeEach(function () {
@@ -43,36 +58,54 @@ module('Acceptance | managed-groups | delete', function (hooks) {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_OIDC,
});
instances.ldapAuthMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
});
instances.managedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
authMethod: instances.authMethod,
});
instances.ldapManagedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
authMethod: instances.ldapAuthMethod,
});
// Generate route URLs for resources
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.authMethods = `${urls.orgScope}/auth-methods`;
urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`;
urls.ldapAuthMethod = `${urls.authMethods}/${instances.ldapAuthMethod.id}`;
urls.managedGroups = `${urls.authMethod}/managed-groups`;
urls.newManagedGroup = `${urls.managedGroups}/new`;
// TODO refactor in the rest of the test this url
urls.ldapManagedGroups = `${urls.ldapAuthMethod}/managed-groups`;
urls.managedGroup = `${urls.managedGroups}/${instances.managedGroup.id}`;
});
urls.ldapManagedGroup = `${urls.ldapManagedGroups}/${instances.ldapManagedGroup.id}`;
hooks.afterEach(async function () {
const notification = find('.rose-notification');
if (notification) {
await click('.rose-notification-dismiss');
}
getManagedGroupCount = () =>
this.server.schema.managedGroups.all().models.length;
});
test('User can delete a managed group', async function (assert) {
assert.expect(1);
const managedGroupsCount = this.server.db.managedGroups.length;
await visit(urls.managedGroup);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.strictEqual(
this.server.db.managedGroups.length,
managedGroupsCount - 1
);
assert.expect(2);
const managedGroupsCount = getManagedGroupCount();
await visit(urls.managedGroups);
await click(`[href="${urls.managedGroup}"]`);
await click(DELETE_DROPDOWN_SELECTOR);
assert.strictEqual(getManagedGroupCount(), managedGroupsCount - 1);
assert.strictEqual(currentURL(), urls.managedGroups);
});
test('User can delete a ldap managed group', async function (assert) {
assert.expect(2);
const managedGroupsCount = getManagedGroupCount();
await visit(urls.ldapManagedGroups);
await click(`[href="${urls.ldapManagedGroup}"]`);
await click(DELETE_DROPDOWN_SELECTOR);
assert.strictEqual(getManagedGroupCount(), managedGroupsCount - 1);
assert.strictEqual(currentURL(), urls.ldapManagedGroups);
});
test('User cannot delete a managed-group without proper authorization', async function (assert) {
@@ -81,14 +114,28 @@ module('Acceptance | managed-groups | delete', function (hooks) {
instances.managedGroup.authorized_actions.filter(
(item) => item !== 'delete'
);
await visit(urls.managedGroup);
assert.notOk(
find('.rose-layout-page-actions .rose-dropdown-button-danger')
);
await visit(urls.managedGroups);
await click(`[href="${urls.managedGroup}"]`);
assert.dom(DELETE_DROPDOWN_SELECTOR).doesNotExist();
});
test('User cannot delete a ldap managed-group without proper authorization', async function (assert) {
assert.expect(1);
instances.ldapManagedGroup.authorized_actions =
instances.ldapManagedGroup.authorized_actions.filter(
(item) => item !== 'delete'
);
await visit(urls.ldapManagedGroups);
await click(`[href="${urls.ldapManagedGroup}"]`);
assert.dom(DELETE_DROPDOWN_SELECTOR).doesNotExist();
});
test('Errors are displayed when delete on managed group fails', async function (assert) {
assert.expect(1);
assert.expect(2);
this.server.del('/managed-groups/:id', () => {
return new Response(
490,
@@ -100,13 +147,36 @@ module('Acceptance | managed-groups | delete', function (hooks) {
}
);
});
await visit(urls.managedGroup);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
await visit(urls.managedGroups);
await click(`[href="${urls.managedGroup}"]`);
await click(DELETE_DROPDOWN_SELECTOR);
await a11yAudit();
assert.strictEqual(
find('.rose-notification-body').textContent.trim(),
'Oops.',
'Displays primary error message.'
);
assert.dom(ERROR_MSG_SELECTOR).hasText('Oops.');
assert.strictEqual(currentURL(), urls.managedGroup);
});
test('Errors are displayed when delete on ldap managed group fails', async function (assert) {
assert.expect(2);
this.server.del('/managed-groups/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.ldapManagedGroups);
await click(`[href="${urls.ldapManagedGroup}"]`);
await click(DELETE_DROPDOWN_SELECTOR);
await a11yAudit();
assert.dom(ERROR_MSG_SELECTOR).hasText('Oops.');
assert.strictEqual(currentURL(), urls.ldapManagedGroup);
});
});
@@ -4,30 +4,39 @@
*/
import { module, test } from 'qunit';
import { visit, click, find } from '@ember/test-helpers';
import { visit, click } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
import {
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Acceptance | managed-groups | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let featuresService;
const instances = {
scopes: {
global: null,
org: null,
},
authMethod: null,
ldapAuthMethod: null,
managedGroup: null,
};
const urls = {
orgScope: null,
authMethods: null,
authMethod: null,
ldapAuthMethod: null,
managedGroups: null,
ldapManagedGroups: null,
newManagedGroup: null,
managedGroup: null,
newLdapManagedGroup: null,
};
hooks.beforeEach(function () {
@@ -41,47 +50,106 @@ module('Acceptance | managed-groups | list', function (hooks) {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_OIDC,
});
instances.ldapAuthMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
});
instances.managedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
authMethod: instances.authMethod,
});
// Generate route URLs for resources
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.authMethods = `${urls.orgScope}/auth-methods`;
urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`;
urls.ldapAuthMethod = `${urls.authMethods}/${instances.ldapAuthMethod.id}`;
urls.managedGroups = `${urls.authMethod}/managed-groups`;
urls.ldapManagedGroups = `${urls.ldapAuthMethod}/managed-groups`;
urls.newManagedGroup = `${urls.managedGroups}/new`;
urls.managedGroup = `${urls.managedGroups}/${instances.authMethod.id}`;
});
hooks.afterEach(async function () {
const notification = find('.rose-notification');
if (notification) {
await click('.rose-notification-dismiss');
}
urls.newLdapManagedGroup = `${urls.ldapManagedGroups}/new`;
featuresService = this.owner.lookup('service:features');
});
test('User can navigate to managed groups with proper authorization', async function (assert) {
assert.expect(2);
await visit(urls.authMethod);
assert.ok(
assert.expect(3);
await visit(urls.authMethods);
await click(`[href="${urls.authMethod}"]`);
assert.true(
instances.authMethod.authorized_collection_actions[
'managed-groups'
].includes('list')
);
assert.ok(find(`[href="${urls.managedGroups}"]`));
assert.true(
instances.authMethod.authorized_collection_actions[
'managed-groups'
].includes('create')
);
assert.dom(`[href="${urls.managedGroups}"]`).exists();
});
test('User can navigate to ldap managed groups with proper authorization', async function (assert) {
assert.expect(3);
featuresService.enable('ldap-auth-methods');
await visit(urls.authMethods);
await click(`[href="${urls.ldapAuthMethod}"]`);
assert.true(
instances.ldapAuthMethod.authorized_collection_actions[
'managed-groups'
].includes('list')
);
assert.true(
instances.ldapAuthMethod.authorized_collection_actions[
'managed-groups'
].includes('create')
);
assert.dom(`[href="${urls.ldapManagedGroups}"]`).exists();
});
test('User cannot navigate to index without either list or create actions', async function (assert) {
assert.expect(2);
assert.expect(3);
instances.authMethod.authorized_collection_actions['managed-groups'] = [];
await visit(urls.authMethod);
assert.notOk(
await visit(urls.authMethods);
await click(`[href="${urls.authMethod}"]`);
assert.false(
instances.authMethod.authorized_collection_actions[
'managed-groups'
].includes('list')
);
assert.notOk(find(`[href="${urls.managedGroups}"]`));
assert.false(
instances.authMethod.authorized_collection_actions[
'managed-groups'
].includes('create')
);
assert.dom(`[href="${urls.managedGroups}"]`).doesNotExist();
});
test('User cannot navigate to ldap managed group index without either list or create actions', async function (assert) {
assert.expect(3);
featuresService.enable('ldap-auth-methods');
instances.ldapAuthMethod.authorized_collection_actions['managed-groups'] =
[];
await visit(urls.authMethods);
await click(`[href="${urls.ldapAuthMethod}"]`);
assert.false(
instances.ldapAuthMethod.authorized_collection_actions[
'managed-groups'
].includes('list')
);
assert.false(
instances.ldapAuthMethod.authorized_collection_actions[
'managed-groups'
].includes('create')
);
assert.dom(`[href="${urls.ldapManagedGroups}"]`).doesNotExist();
});
test('User can navigate to index with only create action', async function (assert) {
@@ -89,10 +157,25 @@ module('Acceptance | managed-groups | list', function (hooks) {
instances.authMethod.authorized_collection_actions['managed-groups'] = [
'create',
];
await visit(urls.authMethod);
assert.ok(find(`[href="${urls.managedGroups}"]`));
assert.ok(
find(`.rose-layout-page-actions [href="${urls.newManagedGroup}"]`)
);
await visit(urls.authMethods);
await click(`[href="${urls.authMethod}"]`);
assert.dom(`[href="${urls.managedGroups}"]`).exists();
assert.dom(`[href="${urls.newManagedGroup}"]`).exists();
});
test('User can navigate to ldap managed groups index with only create action', async function (assert) {
assert.expect(2);
featuresService.enable('ldap-auth-methods');
instances.authMethod.authorized_collection_actions['managed-groups'] = [
'create',
];
await visit(urls.authMethods);
await click(`[href="${urls.ldapAuthMethod}"]`);
assert.dom(`[href="${urls.ldapManagedGroups}"]`).exists();
assert.dom(`[href="${urls.newLdapManagedGroup}"]`).exists();
});
});
@@ -4,12 +4,15 @@
*/
import { module, test } from 'qunit';
import { visit, click, find, currentURL } from '@ember/test-helpers';
import { visit, click, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
import {
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Acceptance | managed-groups | read', function (hooks) {
setupApplicationTest(hooks);
@@ -21,14 +24,19 @@ module('Acceptance | managed-groups | read', function (hooks) {
org: null,
},
authMethod: null,
ldapAuthMethod: null,
managedGroup: null,
ldapManagedGroup: null,
};
const urls = {
orgScope: null,
authMethods: null,
authMethod: null,
ldapAuthMethod: null,
managedGroups: null,
newManagedGroup: null,
ldapManagedGroups: null,
managedGroup: null,
ldapManagedGroup: null,
};
hooks.beforeEach(function () {
@@ -42,42 +50,72 @@ module('Acceptance | managed-groups | read', function (hooks) {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_OIDC,
});
instances.ldapAuthMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
});
instances.managedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
authMethod: instances.authMethod,
});
instances.ldapManagedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
authMethod: instances.ldapAuthMethod,
});
// Generate route URLs for resources
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.authMethods = `${urls.orgScope}/auth-methods`;
urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`;
urls.ldapAuthMethod = `${urls.authMethods}/${instances.ldapAuthMethod.id}`;
urls.managedGroups = `${urls.authMethod}/managed-groups`;
urls.newManagedGroup = `${urls.managedGroups}/new`;
// TODO refactor in the rest of the test this url
urls.ldapManagedGroups = `${urls.ldapAuthMethod}/managed-groups`;
urls.managedGroup = `${urls.managedGroups}/${instances.managedGroup.id}`;
});
hooks.afterEach(async function () {
const notification = find('.rose-notification');
if (notification) {
await click('.rose-notification-dismiss');
}
urls.ldapManagedGroup = `${urls.ldapManagedGroups}/${instances.ldapManagedGroup.id}`;
});
test('User can navigate to a managed group form', async function (assert) {
assert.expect(1);
await visit(urls.managedGroups);
await click('main tbody .rose-table-header-cell:nth-child(1) a');
await click(`[href="${urls.managedGroup}"]`);
await a11yAudit();
assert.strictEqual(currentURL(), urls.managedGroup);
});
test('User can navigate to a ldap managed group form', async function (assert) {
assert.expect(1);
await visit(urls.ldapManagedGroups);
await click(`[href="${urls.ldapManagedGroup}"]`);
await a11yAudit();
assert.strictEqual(currentURL(), urls.ldapManagedGroup);
});
test('User cannot navigate to a managed group form without proper authorization', async function (assert) {
assert.expect(1);
instances.managedGroup.authorized_actions =
instances.managedGroup.authorized_actions.filter(
(item) => item !== 'read'
);
await visit(urls.managedGroups);
assert.notOk(find('main tbody .rose-table-header-cell:nth-child(1) a'));
await visit(urls.authMethod);
await click(`[href="${urls.managedGroups}"]`);
assert.dom(`[href="${urls.managedGroup}"]`).doesNotExist();
});
test('User cannot navigate to a ldap managed group form without proper authorization', async function (assert) {
assert.expect(1);
instances.ldapManagedGroup.authorized_actions =
instances.ldapManagedGroup.authorized_actions.filter(
(item) => item !== 'read'
);
await visit(urls.ldapAuthMethod);
await click(`[href="${urls.ldapManagedGroups}"]`);
assert.dom(`[href="${urls.ldapManagedGroup}"]`).doesNotExist();
});
});