diff --git a/addons/api/addon/serializers/managed-group.js b/addons/api/addon/serializers/managed-group.js
index cb03236af..f42fc78d2 100644
--- a/addons/api/addon/serializers/managed-group.js
+++ b/addons/api/addon/serializers/managed-group.js
@@ -1,3 +1,10 @@
import ApplicationSerializer from './application';
-export default class ManagedGroupSerializer extends ApplicationSerializer {}
+export default class ManagedGroupSerializer extends ApplicationSerializer {
+ // =properties
+
+ /**
+ * @type {boolean}
+ */
+ serializeScopeID = false;
+}
diff --git a/addons/api/addon/services/resource-filter-store.js b/addons/api/addon/services/resource-filter-store.js
index 94d934359..8bd4130ae 100644
--- a/addons/api/addon/services/resource-filter-store.js
+++ b/addons/api/addon/services/resource-filter-store.js
@@ -10,7 +10,7 @@ import { inject as service } from '@ember/service';
* type: 'oidc',
* status: ['active', 'pending']
* });
- * console.log(filter.queryExpression);
+ * log(filter.queryExpression);
* // ("/item/type" == "oidc") and ("/item/status" == "active" or "/item/status" == "pending")
*/
export class ResourceFilter {
diff --git a/addons/api/app/serializers/managed-group.js b/addons/api/app/serializers/managed-group.js
new file mode 100644
index 000000000..799590738
--- /dev/null
+++ b/addons/api/app/serializers/managed-group.js
@@ -0,0 +1 @@
+export { default } from 'api/serializers/managed-group';
diff --git a/addons/api/mirage/factories/auth-method.js b/addons/api/mirage/factories/auth-method.js
index 755154b09..865c10b3d 100644
--- a/addons/api/mirage/factories/auth-method.js
+++ b/addons/api/mirage/factories/auth-method.js
@@ -15,6 +15,7 @@ export default factory.extend({
authorized_collection_actions: () => {
return {
accounts: ['create', 'list'],
+ 'managed-groups': ['create', 'list'],
};
},
id: (i) => `auth-method-id-${i}`,
diff --git a/addons/core/addon/decorators/resource-filter-param.js b/addons/core/addon/decorators/resource-filter-param.js
new file mode 100644
index 000000000..61978ee06
--- /dev/null
+++ b/addons/core/addon/decorators/resource-filter-param.js
@@ -0,0 +1,86 @@
+/**
+ * A resource filter param is a concise way to express a route query parameter.
+ * Resource filter params abstract away a route's query parameter
+ * interface, treating a query param as a simple attribute on a route instance.
+ * Getting the attribute value is equivalent to looking up the associated query
+ * parameter. Setting the attribute is equivalent to transitioning to the route
+ * and passing the associated query parameter value.
+ *
+ * Additionally, resource filter params may declare `allowedValues`, an array
+ * containing the set of values the parameter may take. While unenforced,
+ * `allowedValues` are useful metadata about a resource filter and may be fetch
+ * via `ResourceFilterParamHelper`. An optional default value may be specified
+ * via `defaultValue` passed to the decorator.
+ *
+ * @see ResourceFilterParamHelper
+ *
+ * @example
+ *
+ * export default class MyRoute extends Route {
+ *
+ * @resourceFilterParam(['active', 'pending'], ['active'])
+ * status;
+ *
+ * @action
+ * changeStatus(status) {
+ * // Sets query param `filter-status` on the current route, which occurs
+ * // via a route transition.
+ * this.status = status;
+ * // Log the value of query param `filter-status`.
+ * log(this.status);
+ * }
+ * }
+ *
+ */
+export function resourceFilterParam(allowedValues, defaultValue) {
+ /**
+ * @param {object} target
+ * @param {string} name
+ * @param {object} descriptor
+ * @return {object{get, set}}
+ */
+ return function (target, name /*, descriptor*/) {
+ const filterKey = `filter-${name}`;
+ const filterAllowedValuesKey = `filter-allowed-values-${name}`;
+
+ // If the route has no query params specified yet, declare them.
+ if (!Object.keys(target.queryParams).length) target.queryParams = {};
+
+ // Add the resource filter query parameter to the route. Resource filter
+ // param changes refresh the model but do not contribute to browser history.
+ // See https://guides.emberjs.com/release/routing/query-params/
+ target.queryParams[filterKey] = {
+ refreshModel: true,
+ replace: true,
+ };
+
+ // Store the allowed values for future lookup
+ // (via `ResourceFilterParamHelper`).
+ target[filterAllowedValuesKey] = allowedValues;
+
+ // Override the decorated attribute with a getter and setter.
+ return {
+ /**
+ * Returns the JSON-parsed query parameter value OR defaultValue.
+ */
+ get() {
+ const value =
+ this.paramsFor(this.routeName)[filterKey] ||
+ JSON.stringify(defaultValue);
+ return value ? JSON.parse(value) : null;
+ },
+
+ /**
+ * Sets the resource filter param to the stringified representation
+ * of `value`. NOTE: this is equivalent to transitioning into the
+ * current route, changing the associated query param.
+ * @param value
+ */
+ set(value) {
+ const queryParams = {};
+ queryParams[filterKey] = JSON.stringify(value);
+ this.transitionTo({ queryParams });
+ },
+ };
+ };
+}
diff --git a/addons/core/addon/helpers/resource-filter-param.js b/addons/core/addon/helpers/resource-filter-param.js
new file mode 100644
index 000000000..e78ac20a7
--- /dev/null
+++ b/addons/core/addon/helpers/resource-filter-param.js
@@ -0,0 +1,73 @@
+import Helper from '@ember/component/helper';
+import { inject as service } from '@ember/service';
+import { getOwner } from '@ember/application';
+import { action } from '@ember/object';
+
+/**
+ * Use this helper to fetch the current value of a resource filter param,
+ * as well as its allowedValues.
+ *
+ * @see resourceFilterParam (decorator)
+ *
+ * @example
+ *
+ * {{#with resource-filter-param 'status' as |param|}}
+ * {{param.name}} / {{param.value}} / {{param.selectedValue}}
+ * {{/with}}
+ */
+export default class ResourceFilterParamHelper extends Helper {
+ // =services
+
+ @service router;
+
+ // =methods
+
+ // =lifecycle management methods
+
+ /**
+ * Recompute this helper when the route changes.
+ */
+ constructor() {
+ super(...arguments);
+ this.router.on('routeDidChange', this.routeDidChange);
+ }
+
+ /**
+ * Stop listening to route events when this helper is destroyed.
+ */
+ willDestroy() {
+ this.router.off('routeDidChange', this.routeDidChange);
+ super.willDestroy();
+ }
+
+ // =compute method
+
+ /**
+ * Returns an object containing `name`, `value`, and `selectedValue`.
+ * `name` - The name of the resource filter param.
+ * `value` - The value of the resource filter param
+ * @return {object}
+ */
+ compute([routeName, name]) {
+ const owner = getOwner(this);
+ // Filter options
+ const route = owner.lookup(`route:${routeName}`);
+ const filterAllowedValuesKey = `filter-allowed-values-${name}`;
+ const allowedValues = route[filterAllowedValuesKey];
+ // Selected filters
+ const rawValue = route[name];
+ const selectedValue = rawValue || null;
+ return { name, allowedValues, selectedValue };
+ }
+
+ // =actions
+
+ /**
+ * Triggers a recompute (see above) when the route changes, especially when
+ * route query params change.
+ */
+ @action
+ routeDidChange() {
+ this.recompute();
+ }
+}
diff --git a/addons/core/app/decorators/resource-filter-param.js b/addons/core/app/decorators/resource-filter-param.js
new file mode 100644
index 000000000..4e5cebc3c
--- /dev/null
+++ b/addons/core/app/decorators/resource-filter-param.js
@@ -0,0 +1 @@
+export { default } from 'core/decorators/resource-filter-param';
diff --git a/addons/core/app/helpers/resource-filter-param.js b/addons/core/app/helpers/resource-filter-param.js
new file mode 100644
index 000000000..8977c55f0
--- /dev/null
+++ b/addons/core/app/helpers/resource-filter-param.js
@@ -0,0 +1 @@
+export { default } from 'core/helpers/resource-filter-param';
diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 7b9d1e4fe..4d7f4842f 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -358,3 +358,7 @@ managed-group:
actions:
delete: Delete Managed Group
create: Create Managed Group
+ messages:
+ welcome:
+ title: Welcome to Managed Groups
+ description: Create and manage managed groups for an auth method.
diff --git a/ui/admin/app/components/form/managed-group/index.hbs b/ui/admin/app/components/form/managed-group/index.hbs
index 7918dd146..523bb52bb 100644
--- a/ui/admin/app/components/form/managed-group/index.hbs
+++ b/ui/admin/app/components/form/managed-group/index.hbs
@@ -69,11 +69,12 @@
{{/if}}
-
-
+ {{#if (can 'save model' @model)}}
+
+ {{/if}}
\ No newline at end of file
diff --git a/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups.js b/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups.js
new file mode 100644
index 000000000..8d72eb80d
--- /dev/null
+++ b/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups.js
@@ -0,0 +1,18 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsController extends Controller {
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * Translated managed groups breadcrumb
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('resources.managed-group.title_plural');
+ }
+}
diff --git a/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups/managed-group.js b/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups/managed-group.js
new file mode 100644
index 000000000..4b770231b
--- /dev/null
+++ b/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups/managed-group.js
@@ -0,0 +1,13 @@
+import Controller from '@ember/controller';
+
+export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsManagedGroupController extends Controller {
+ // =attributes
+
+ /**
+ * A managed group breadcrumb
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.model.displayName;
+ }
+}
diff --git a/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups/new.js b/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups/new.js
new file mode 100644
index 000000000..cf16a4e94
--- /dev/null
+++ b/ui/admin/app/controllers/scopes/scope/auth-methods/auth-method/managed-groups/new.js
@@ -0,0 +1,17 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsNewController extends Controller {
+ // =services
+
+ @service intl;
+
+ // =attributes
+ /**
+ * Translated new managed group breadcrumb
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('titles.new');
+ }
+}
diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups.js b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups.js
index cef4017d2..770d1357e 100644
--- a/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups.js
+++ b/ui/admin/app/routes/scopes/scope/auth-methods/auth-method/managed-groups.js
@@ -21,7 +21,13 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsRoute extends
model() {
const authMethod = this.modelFor('scopes.scope.auth-methods.auth-method');
const { id: auth_method_id } = authMethod;
- return this.store.query('managed-group', { auth_method_id });
+ const canListManagedGroups = this.can.can('list collection', authMethod, {
+ collection: 'managed-groups',
+ });
+
+ if (canListManagedGroups) {
+ return this.store.query('managed-group', { auth_method_id });
+ }
}
// =actions
@@ -50,9 +56,16 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsRoute extends
)
async save(managedGroup) {
await managedGroup.save();
- await this.transitionTo(
- 'scopes.scope.auth-methods.auth-method.managed-groups'
- );
+ if (this.can.can('read model', managedGroup)) {
+ await this.transitionTo(
+ 'scopes.scope.auth-methods.auth-method.managed-groups.managed-group',
+ managedGroup
+ );
+ } else {
+ await this.transitionTo(
+ 'scopes.scope.auth-methods.auth-method.managed-groups'
+ );
+ }
this.refresh();
}
@@ -65,7 +78,7 @@ export default class ScopesScopeAuthMethodsAuthMethodManagedGroupsRoute extends
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
- async deleteManagedGroup(managedGroup) {
+ async delete(managedGroup) {
await managedGroup.destroyRecord();
await this.replaceWith(
'scopes.scope.auth-methods.auth-method.managed-groups'
diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-actions.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-actions.hbs
index 865bdfeec..5626026a6 100644
--- a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-actions.hbs
+++ b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-actions.hbs
@@ -68,14 +68,15 @@
{{/if}}
{{#if (feature-flag 'managed-groups')}}
- {{! TODO: Add capabilities }}
- {{#if @model.isOIDC}}
-
- {{t 'resources.managed-group.actions.create'}}
-
+ {{#if (can 'create collection' @model collection='managed-groups')}}
+ {{#if @model.isOIDC}}
+
+ {{t 'resources.managed-group.actions.create'}}
+
+ {{/if}}
{{/if}}
{{/if}}
diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-navigation.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-navigation.hbs
index 6c5d6bd70..00c2b814d 100644
--- a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-navigation.hbs
+++ b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/-navigation.hbs
@@ -10,10 +10,12 @@
{{/if}}
{{#if (feature-flag 'managed-groups')}}
- {{#if @model.isOIDC}}
-
- {{t 'resources.managed-group.title_plural'}}
-
+ {{#if (can 'navigate collection' @model collection='managed-groups')}}
+ {{#if @model.isOIDC}}
+
+ {{t 'resources.managed-group.title_plural'}}
+
+ {{/if}}
{{/if}}
{{/if}}
\ No newline at end of file
diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/index.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/index.hbs
index 8165f109d..c482fd414 100644
--- a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/index.hbs
+++ b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/index.hbs
@@ -41,4 +41,46 @@
{{/each}}
+{{else}}
+
+
+
+
+ {{#if
+ (can 'list collection' this.authMethod collection='managed-groups')
+ }}
+ {{! Can list (at least): show default welcome message }}
+ {{t 'resources.managed-group.description'}}
+ {{else if (can
+ 'create collection' this.authMethod collection='managed-groups'
+ )}}
+ {{! Can create only: show create but not list welcome message }}
+ {{t
+ 'descriptions.create-but-not-list'
+ resource=(t 'resources.managed-group.title_plural')
+ }}
+ {{else}}
+ {{! Can neither list nor create }}
+ {{t
+ 'descriptions.neither-list-nor-create'
+ resource=(t 'resources.managed-group.title_plural')
+ }}
+ {{/if}}
+
+
+ {{#if
+ (can 'create collection' this.authMethod collection='managed-groups')
+ }}
+
+
+ {{t 'resources.managed-group.actions.create'}}
+
+ {{/if}}
+
+
{{/if}}
\ No newline at end of file
diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/managed-group/-actions.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/managed-group/-actions.hbs
index 98a51d476..3340d97f2 100644
--- a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/managed-group/-actions.hbs
+++ b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/managed-group/-actions.hbs
@@ -7,7 +7,7 @@
{{t 'resources.managed-group.actions.delete'}}
diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/managed-group/index.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/managed-group/index.hbs
index 19ab29fcb..bcaa893ac 100644
--- a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/managed-group/index.hbs
+++ b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/managed-groups/managed-group/index.hbs
@@ -1 +1,5 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ui/admin/tests/acceptance/managed-groups/create-test.js b/ui/admin/tests/acceptance/managed-groups/create-test.js
new file mode 100644
index 000000000..8d07893e4
--- /dev/null
+++ b/ui/admin/tests/acceptance/managed-groups/create-test.js
@@ -0,0 +1,160 @@
+import { module, test } from 'qunit';
+import { visit, currentURL, click, find, 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';
+
+module('Acceptance | managed-groups | create', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ },
+ authMethod: null,
+ managedGroup: null,
+ };
+ const urls = {
+ orgScope: null,
+ authMethods: null,
+ managedGroups: null,
+ newManagedGroup: null,
+ managedGroup: null,
+ };
+
+ hooks.beforeEach(function () {
+ authenticateSession({});
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.authMethod = this.server.create('auth-method', {
+ scope: instances.scopes.org,
+ type: 'oidc',
+ });
+ 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.managedGroups = `${urls.authMethod}/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');
+ }
+ });
+
+ test('visiting managed groups', async function (assert) {
+ assert.expect(1);
+ await visit(urls.managedGroups);
+ await a11yAudit();
+ assert.equal(currentURL(), urls.managedGroups);
+ });
+
+ 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.equal(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'] = [];
+ 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}"]`)
+ );
+ });
+
+ 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('User cannot navigate to new managed group route 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'
+ ].includes('create')
+ );
+ assert.notOk(find(`[href="${urls.newManagedGroup}"]`));
+ });
+
+ 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.equal(this.server.db.managedGroups.length, managedGroupsCount);
+ assert.equal(currentURL(), urls.managedGroups);
+ });
+
+ 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', () => {
+ 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.newManagedGroup);
+ await fillIn('[name="name"]', 'new managed group');
+ await click('form [type="submit"]');
+ await a11yAudit();
+ assert.equal(
+ find('.rose-notification-body').textContent.trim(),
+ 'The request was invalid.',
+ 'Displays primary error message.'
+ );
+ assert.equal(
+ find('.rose-form-error-message').textContent.trim(),
+ 'Name is required.',
+ 'Displays field-level errors.'
+ );
+ });
+});
diff --git a/ui/admin/tests/acceptance/managed-groups/delete-test.js b/ui/admin/tests/acceptance/managed-groups/delete-test.js
new file mode 100644
index 000000000..7be2263ab
--- /dev/null
+++ b/ui/admin/tests/acceptance/managed-groups/delete-test.js
@@ -0,0 +1,103 @@
+import { module, test } from 'qunit';
+import { visit, click, find } 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';
+
+module('Acceptance | managed-groups | delete', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ },
+ authMethod: null,
+ managedGroup: null,
+ };
+ const urls = {
+ orgScope: null,
+ authMethods: null,
+ managedGroups: null,
+ newManagedGroup: null,
+ managedGroup: null,
+ };
+
+ hooks.beforeEach(function () {
+ authenticateSession({});
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.authMethod = this.server.create('auth-method', {
+ scope: instances.scopes.org,
+ type: 'oidc',
+ });
+ 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.managedGroups = `${urls.authMethod}/managed-groups`;
+ urls.newManagedGroup = `${urls.managedGroups}/new`;
+ // TODO refactor in the rest of the test this url
+ urls.managedGroup = `${urls.managedGroups}/${instances.managedGroup.id}`;
+ });
+
+ hooks.afterEach(async function () {
+ const notification = find('.rose-notification');
+ if (notification) {
+ await click('.rose-notification-dismiss');
+ }
+ });
+
+ 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.equal(this.server.db.managedGroups.length, managedGroupsCount - 1);
+ });
+
+ test('User cannot delete a managed-group without proper authorization', async function (assert) {
+ assert.expect(1);
+ instances.managedGroup.authorized_actions =
+ instances.managedGroup.authorized_actions.filter(
+ (item) => item !== 'delete'
+ );
+ await visit(urls.managedGroup);
+ assert.notOk(
+ find('.rose-layout-page-actions .rose-dropdown-button-danger')
+ );
+ });
+
+ test('Errors are displayed when delete on managed group fails', async function (assert) {
+ assert.expect(1);
+ this.server.del('/managed-groups/:id', () => {
+ return new Response(
+ 490,
+ {},
+ {
+ status: 490,
+ code: 'error',
+ message: 'Oops.',
+ }
+ );
+ });
+ await visit(urls.managedGroup);
+ await click('.rose-layout-page-actions .rose-dropdown-button-danger');
+ await a11yAudit();
+ assert.equal(
+ find('.rose-notification-body').textContent.trim(),
+ 'Oops.',
+ 'Displays primary error message.'
+ );
+ });
+});
diff --git a/ui/admin/tests/acceptance/managed-groups/list-test.js b/ui/admin/tests/acceptance/managed-groups/list-test.js
new file mode 100644
index 000000000..dc2ad9e98
--- /dev/null
+++ b/ui/admin/tests/acceptance/managed-groups/list-test.js
@@ -0,0 +1,92 @@
+import { module, test } from 'qunit';
+import { visit, click, find } 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';
+
+module('Acceptance | managed-groups | list', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ },
+ authMethod: null,
+ managedGroup: null,
+ };
+ const urls = {
+ orgScope: null,
+ authMethods: null,
+ managedGroups: null,
+ newManagedGroup: null,
+ managedGroup: null,
+ };
+
+ hooks.beforeEach(function () {
+ authenticateSession({});
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.authMethod = this.server.create('auth-method', {
+ scope: instances.scopes.org,
+ type: 'oidc',
+ });
+ 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.managedGroups = `${urls.authMethod}/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');
+ }
+ });
+
+ test('User can navigate to managed groups with proper authorization', async function (assert) {
+ assert.expect(2);
+ await visit(urls.authMethod);
+ assert.ok(
+ instances.authMethod.authorized_collection_actions[
+ 'managed-groups'
+ ].includes('list')
+ );
+ assert.ok(find(`[href="${urls.managedGroups}"]`));
+ });
+
+ test('User cannot navigate to index without either list or create actions', async function (assert) {
+ assert.expect(2);
+ instances.authMethod.authorized_collection_actions['managed-groups'] = [];
+ await visit(urls.authMethod);
+ assert.notOk(
+ instances.authMethod.authorized_collection_actions[
+ 'managed-groups'
+ ].includes('list')
+ );
+ assert.notOk(find(`[href="${urls.managedGroups}"]`));
+ });
+
+ test('User can navigate to index with only create action', async function (assert) {
+ assert.expect(2);
+ 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}"]`)
+ );
+ });
+});
diff --git a/ui/admin/tests/acceptance/managed-groups/read-test.js b/ui/admin/tests/acceptance/managed-groups/read-test.js
new file mode 100644
index 000000000..ef11179c8
--- /dev/null
+++ b/ui/admin/tests/acceptance/managed-groups/read-test.js
@@ -0,0 +1,77 @@
+import { module, test } from 'qunit';
+import { visit, click, find, 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';
+
+module('Acceptance | managed-groups | read', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ },
+ authMethod: null,
+ managedGroup: null,
+ };
+ const urls = {
+ orgScope: null,
+ authMethods: null,
+ managedGroups: null,
+ newManagedGroup: null,
+ managedGroup: null,
+ };
+
+ hooks.beforeEach(function () {
+ authenticateSession({});
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.authMethod = this.server.create('auth-method', {
+ scope: instances.scopes.org,
+ type: 'oidc',
+ });
+ 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.managedGroups = `${urls.authMethod}/managed-groups`;
+ urls.newManagedGroup = `${urls.managedGroups}/new`;
+ // TODO refactor in the rest of the test this url
+ urls.managedGroup = `${urls.managedGroups}/${instances.managedGroup.id}`;
+ });
+
+ hooks.afterEach(async function () {
+ const notification = find('.rose-notification');
+ if (notification) {
+ await click('.rose-notification-dismiss');
+ }
+ });
+
+ 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 a11yAudit();
+ assert.equal(currentURL(), urls.managedGroup);
+ });
+
+ 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'));
+ });
+});
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups-test.js
new file mode 100644
index 000000000..7f976529b
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Controller | scopes/scope/auth-methods/auth-method/managed-groups',
+ 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/auth-method/managed-groups'
+ );
+ assert.ok(controller);
+ });
+ }
+);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups/managed-group-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups/managed-group-test.js
new file mode 100644
index 000000000..22284a9cf
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups/managed-group-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Controller | scopes/scope/auth-methods/auth-method/managed-groups/managed-group',
+ 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/auth-method/managed-groups/managed-group'
+ );
+ assert.ok(controller);
+ });
+ }
+);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups/new-test.js
new file mode 100644
index 000000000..699d52e58
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/auth-method/managed-groups/new-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Controller | scopes/scope/auth-methods/auth-method/managed-groups/new',
+ 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/auth-method/managed-groups/new'
+ );
+ assert.ok(controller);
+ });
+ }
+);