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); + }); + } +);