From ded8bdadd1b0d96e8828b77f9464727d0f6ead05 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:31:32 -0700 Subject: [PATCH] Icu 12592 add filtering pagination support for auth methods (#2209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add filtering & pagination to auth methods ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12592 --------- Co-authored-by: Zhihe Li Co-authored-by: Cameron Perera --- .../api/addon/handlers/indexed-db-handler.js | 15 +- addons/api/addon/services/indexed-db.js | 2 + addons/api/mirage/config.js | 23 +- addons/core/translations/resources/en-us.yaml | 1 + .../scopes/scope/auth-methods/index.js | 73 ++++ .../controllers/scopes/scope/targets/index.js | 10 +- .../app/routes/scopes/scope/auth-methods.js | 47 +-- .../routes/scopes/scope/auth-methods/index.js | 99 ++++++ .../scopes/scope/roles/role/add-principals.js | 24 +- .../scopes/scope/roles/role/principals.js | 23 +- .../scopes/scope/users/user/add-accounts.js | 8 +- .../scopes/scope/auth-methods/index.hbs | 311 ++++++++---------- .../acceptance/auth-methods/create-test.js | 3 +- .../acceptance/auth-methods/delete-test.js | 2 + .../acceptance/auth-methods/list-test.js | 154 +++++++-- .../acceptance/auth-methods/read-test.js | 60 ++-- .../acceptance/auth-methods/update-test.js | 2 + .../tests/acceptance/authentication-test.js | 25 +- .../acceptance/managed-groups/create-test.js | 2 + .../acceptance/managed-groups/list-test.js | 2 + .../scopes/scope/auth-methods/index-test.js | 60 +++- .../scopes/scope/targets/index-test.js | 4 +- 22 files changed, 623 insertions(+), 327 deletions(-) diff --git a/addons/api/addon/handlers/indexed-db-handler.js b/addons/api/addon/handlers/indexed-db-handler.js index 3c7c93fbd..591c84018 100644 --- a/addons/api/addon/handlers/indexed-db-handler.js +++ b/addons/api/addon/handlers/indexed-db-handler.js @@ -20,10 +20,15 @@ export default class IndexedDbHandler { switch (context.request.op) { case 'query': { const { store, data } = context.request; + // TODO: Remove storeToken option as this is a temporary fix for auth-methods. const { type, query, - options: { pushToStore = true, peekIndexedDB = false } = {}, + options: { + pushToStore = true, + peekIndexedDB = false, + storeToken = true, + } = {}, } = data; const supportedModels = Object.keys(modelIndexes); const { db: indexedDb } = this.indexedDb ?? {}; @@ -42,7 +47,11 @@ export default class IndexedDbHandler { if (!peekIndexedDB) { const tokenKey = `${type}-${hashCode(remainingQuery)}`; - const listToken = await indexedDb.token.get(tokenKey); + + let listToken; + if (storeToken) { + listToken = await indexedDb.token.get(tokenKey); + } let payload; try { @@ -77,7 +86,7 @@ export default class IndexedDbHandler { } // Store the token we just got back from the payload if it exists - if (payload.list_token) { + if (payload.list_token && storeToken) { await indexedDb.token.put({ id: tokenKey, token: payload.list_token, diff --git a/addons/api/addon/services/indexed-db.js b/addons/api/addon/services/indexed-db.js index c0369d80a..08d4d8dea 100644 --- a/addons/api/addon/services/indexed-db.js +++ b/addons/api/addon/services/indexed-db.js @@ -22,6 +22,8 @@ export const modelIndexes = { '&id, attributes.created_time, attributes.name, attributes.description, attributes.type, attributes.scope.scope_id', 'host-catalog': '&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name', + 'auth-method': + '&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id', }; export const formatDbName = (userId, clusterUrl) => diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index 4834cf933..c777427ac 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -117,28 +117,7 @@ function routes() { ); // Auth & IAM resources - this.get( - '/auth-methods', - ({ authMethods }, { queryParams: { scope_id, recursive, filter } }) => { - let resultSet; - if (recursive && scope_id === 'global') { - resultSet = authMethods.all(); - } else if (recursive) { - resultSet = authMethods.where((authMethod) => { - const authMethodModel = authMethods.find(authMethod.id); - return ( - authMethod.scopeId === scope_id || - authMethodModel?.scope?.scope?.id === scope_id - ); - }); - } else { - resultSet = authMethods.where( - (authMethod) => authMethod.scopeId === scope_id, - ); - } - return resultSet.filter(makeBooleanFilter(filter)); - }, - ); + this.get('/auth-methods'); this.post('/auth-methods', function ({ authMethods }) { const attrs = this.normalizedRequestAttrs(); if (attrs.type === 'oidc') { diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 14747bbe5..b8676da48 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -62,6 +62,7 @@ auth-method: searching: Searching finding: Finding always: Always + primary: Primary types: password: Password oidc: OIDC diff --git a/ui/admin/app/controllers/scopes/scope/auth-methods/index.js b/ui/admin/app/controllers/scopes/scope/auth-methods/index.js index 9d03c4899..45e0cc1b3 100644 --- a/ui/admin/app/controllers/scopes/scope/auth-methods/index.js +++ b/ui/admin/app/controllers/scopes/scope/auth-methods/index.js @@ -4,11 +4,33 @@ */ import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; +import { debounce } from 'core/decorators/debounce'; +import { TYPES_AUTH_METHOD } from 'api/models/auth-method'; export default class ScopesScopeAuthMethodsIndexController extends Controller { + // =services + + @service intl; + // =attributes + queryParams = [ + 'search', + { types: { type: 'array' } }, + { primary: { type: 'array' } }, + 'page', + 'pageSize', + ]; + + @tracked search; + @tracked types = []; + @tracked primary = []; + @tracked page = 1; + @tracked pageSize = 10; + /** * True if the current scope has a primary auth method set. * @type {boolean} @@ -17,8 +39,59 @@ export default class ScopesScopeAuthMethodsIndexController extends Controller { return Boolean(this.scopeModel.primary_auth_method_id); } + get primaryOptions() { + return [ + { id: 'true', name: this.intl.t('actions.yes') }, + { id: 'false', name: this.intl.t('actions.no') }, + ]; + } + + get authMethodTypeOptions() { + return TYPES_AUTH_METHOD.map((type) => ({ + id: type, + name: this.intl.t(`resources.auth-method.types.${type}`), + })); + } + + get filters() { + return { + allFilters: { + primary: this.primaryOptions, + types: this.authMethodTypeOptions, + }, + selectedFilters: { + primary: this.primary, + types: this.types, + }, + }; + } + // =actions + /** + * Handles input on each keystroke and the search queryParam + * @param {object} event + */ + @action + @debounce(250) + handleSearchInput(event) { + const { value } = event.target; + this.search = value; + this.page = 1; + } + + /** + * Sets a query param to the value of selectedItems + * and resets the page to 1. + * @param {string} paramKey + * @param {[string]} selectedItems + */ + @action + applyFilter(paramKey, selectedItems) { + this[paramKey] = [...selectedItems]; + this.page = 1; + } + /** * Removes an item from array `property` at `index` on the * passed `authMethod`. This is used to manage entries in fragment array diff --git a/ui/admin/app/controllers/scopes/scope/targets/index.js b/ui/admin/app/controllers/scopes/scope/targets/index.js index ab25f4b6e..d4ccde5d1 100644 --- a/ui/admin/app/controllers/scopes/scope/targets/index.js +++ b/ui/admin/app/controllers/scopes/scope/targets/index.js @@ -30,8 +30,14 @@ export default class ScopesScopeTargetsIndexController extends Controller { get availableSessionOptions() { return [ - { id: 'yes', name: this.intl.t('actions.yes') }, - { id: 'no', name: this.intl.t('actions.no') }, + { + id: 'yes', + name: this.intl.t('resources.target.filter.active-sessions.yes'), + }, + { + id: 'no', + name: this.intl.t('resources.target.filter.active-sessions.no'), + }, ]; } diff --git a/ui/admin/app/routes/scopes/scope/auth-methods.js b/ui/admin/app/routes/scopes/scope/auth-methods.js index b0647d226..a3785bcf2 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods.js @@ -9,23 +9,14 @@ import { action } from '@ember/object'; import { loading } from 'ember-loading'; import { confirm } from 'core/decorators/confirm'; import { notifySuccess, notifyError } from 'core/decorators/notify'; -import { resourceFilter } from 'core/decorators/resource-filter'; -import { TYPES_AUTH_METHOD } from 'api/models/auth-method'; export default class ScopesScopeAuthMethodsRoute extends Route { // =services - @service intl; - @service session; @service can; - @service resourceFilterStore; @service router; - // =attributes - - @resourceFilter({ allowed: TYPES_AUTH_METHOD }) type; - // =methods /** @@ -35,24 +26,8 @@ export default class ScopesScopeAuthMethodsRoute extends Route { if (!this.session.isAuthenticated) this.router.transitionTo('index'); } - /** - * Load all auth-methods under current scope - * @return {Promise[AuthMethodModel]} - */ - async model() { - const scope = this.modelFor('scopes.scope'); - const { id: scope_id } = scope; - if (this.can.can('list model', scope, { collection: 'auth-methods' })) { - const { type } = this; - return this.resourceFilterStore.queryBy( - 'auth-method', - { type }, - { scope_id }, - ); - } - } - // =actions + /** * Rollback changes to an auth-method. * @param {AuthMethodModel} authMethod @@ -102,7 +77,7 @@ export default class ScopesScopeAuthMethodsRoute extends Route { // Reload the scope, since this is where the primary_auth_method_id is // stored. An auth method deletion could affect this field. await scopeModel.reload(); - await this.router.replaceWith('scopes.scope.auth-methods'); + this.router.replaceWith('scopes.scope.auth-methods'); this.refresh(); } @@ -203,22 +178,4 @@ export default class ScopesScopeAuthMethodsRoute extends Route { const array = [...existingArray, { from, to }]; authMethod.set(field, array); } - - /** - * Sets the specified resource filter field to the specified value. - * @param {string} field - * @param value - */ - @action - filterBy(field, value) { - this[field] = value; - } - - /** - * Clears and filter selections. - */ - @action - clearAllFilters() { - this.type = []; - } } diff --git a/ui/admin/app/routes/scopes/scope/auth-methods/index.js b/ui/admin/app/routes/scopes/scope/auth-methods/index.js index bc09bdb8b..2ce246d81 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods/index.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods/index.js @@ -4,10 +4,109 @@ */ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class ScopesScopeAuthMethodsIndexRoute extends Route { + // =services + + @service store; + @service can; + @service router; + + // =attributes + + queryParams = { + search: { + refreshModel: true, + replace: true, + }, + types: { + refreshModel: true, + replace: true, + }, + primary: { + refreshModel: true, + replace: true, + }, + page: { + refreshModel: true, + }, + pageSize: { + refreshModel: true, + }, + }; + // =methods + /** + * Loads queried auth-methods and the number of auth-methods under current scope. + * @returns {Promise<{totalItems: number, authMethods: [AuthMethodModel], authMethodsExist: boolean }> } + */ + async model({ search, types, primary, page, pageSize }) { + const scope = this.modelFor('scopes.scope'); + const { id: scope_id } = scope; + const filters = { + scope_id: [{ equals: scope_id }], + type: [], + is_primary: [], + }; + + types.forEach((type) => { + filters.type.push({ equals: type }); + }); + if (primary.length == 1) { + primary.forEach((val) => { + filters.is_primary.push({ equals: val }); + }); + } + + let authMethods; + let totalItems = 0; + let authMethodsExist = false; + if (this.can.can('list model', scope, { collection: 'auth-methods' })) { + // TODO: Remove storeToken option as this is a temporary fix for auth-methods. + const options = { storeToken: false }; + authMethods = await this.store.query( + 'auth-method', + { + scope_id, + query: { search, filters }, + page, + pageSize, + }, + options, + ); + totalItems = authMethods.meta?.totalItems; + authMethodsExist = await this.getAuthMethodsExist(scope_id, totalItems); + } + + return { authMethods, authMethodsExist, totalItems }; + } + + /** + * Sets authMethodsExist to true if there exists any auth methods. + * @param {string} scope_id + * @param {number} totalItems + * @returns {Promise} + */ + async getAuthMethodsExist(scope_id, totalItems) { + if (totalItems > 0) { + return true; + } + const options = { pushToStore: false, peekIndexedDB: true }; + const authMethod = await this.store.query( + 'auth-method', + { + scope_id, + query: { filters: { scope_id: [{ equals: scope_id }] } }, + page: 1, + pageSize: 1, + }, + options, + ); + return authMethod.length > 0; + } + /** * Adds the scope to the controller context. * @param {Controller} controller diff --git a/ui/admin/app/routes/scopes/scope/roles/role/add-principals.js b/ui/admin/app/routes/scopes/scope/roles/role/add-principals.js index b5e4acc76..041cde507 100644 --- a/ui/admin/app/routes/scopes/scope/roles/role/add-principals.js +++ b/ui/admin/app/routes/scopes/scope/roles/role/add-principals.js @@ -19,7 +19,6 @@ export default class ScopesScopeRolesRoleAddPrincipalsRoute extends Route { @service router; @service store; - @service resourceFilterStore; // =attributes @@ -64,20 +63,15 @@ export default class ScopesScopeRolesRoleAddPrincipalsRoute extends Route { query, }); //query authmethods from all the scopes - const authMethods = scopeIDs?.length - ? await this.resourceFilterStore.queryBy( - 'auth-method', - { - scope_id: scopeIDs, - type: [TYPE_AUTH_METHOD_OIDC, TYPE_AUTH_METHOD_LDAP], - }, - { scope_id: 'global', recursive: true }, - ) - : await this.resourceFilterStore.queryBy( - 'auth-method', - { type: [TYPE_AUTH_METHOD_OIDC, TYPE_AUTH_METHOD_LDAP] }, - { scope_id: 'global', recursive: true }, - ); + query.filters.type = [ + { equals: TYPE_AUTH_METHOD_OIDC }, + { equals: TYPE_AUTH_METHOD_LDAP }, + ]; + const authMethods = await this.store.query('auth-method', { + scope_id: 'global', + recursive: true, + query, + }); //query all the managed groups for each auth method id const managedGroups = await Promise.all( authMethods.map(({ id: auth_method_id }) => diff --git a/ui/admin/app/routes/scopes/scope/roles/role/principals.js b/ui/admin/app/routes/scopes/scope/roles/role/principals.js index 23590b04f..ec1a60e30 100644 --- a/ui/admin/app/routes/scopes/scope/roles/role/principals.js +++ b/ui/admin/app/routes/scopes/scope/roles/role/principals.js @@ -90,16 +90,21 @@ export default class ScopesScopeRolesRolePrincipalsRoute extends Route { async getManagedGroups(ids) { let managedGroups = []; if (ids?.length) { - // TODO: Refactor to directly use store service - // when types get added to indexedDB. - // Collect all oidc and ldap type auth methods. - const authMethods = await this.resourceFilterStore.queryBy( - 'auth-method', - { type: [TYPE_AUTH_METHOD_OIDC, TYPE_AUTH_METHOD_LDAP] }, - { scope_id: 'global', recursive: true }, - ); - + const authMethods = await this.store.query('auth-method', { + scope_id: 'global', + recursive: true, + query: { + filters: { + type: [ + { equals: TYPE_AUTH_METHOD_OIDC }, + { equals: TYPE_AUTH_METHOD_LDAP }, + ], + }, + }, + }); + // TODO: Refactor to directly use store service + // and add managed-group to indexedDB. const nestedManagedGroups = await Promise.all( authMethods.map(({ id: auth_method_id }) => this.resourceFilterStore.queryBy( diff --git a/ui/admin/app/routes/scopes/scope/users/user/add-accounts.js b/ui/admin/app/routes/scopes/scope/users/user/add-accounts.js index c3b26b676..dc6cd0b89 100644 --- a/ui/admin/app/routes/scopes/scope/users/user/add-accounts.js +++ b/ui/admin/app/routes/scopes/scope/users/user/add-accounts.js @@ -6,7 +6,6 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; -import { all } from 'rsvp'; import { loading } from 'ember-loading'; import { notifySuccess, notifyError } from 'core/decorators/notify'; @@ -33,8 +32,11 @@ export default class ScopesScopeUsersUserAddAccountsRoute extends Route { async model() { const user = this.modelFor('scopes.scope.users.user'); const { id: scope_id } = this.modelFor('scopes.scope'); - const authMethods = await this.store.query('auth-method', { scope_id }); - await all( + const authMethods = await this.store.query('auth-method', { + scope_id, + query: { filters: { scope_id: [{ equals: scope_id }] } }, + }); + await Promise.all( authMethods.map(({ id: auth_method_id }) => this.store.query('account', { auth_method_id }), ), diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/index.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/index.hbs index 53e9d0fbc..011003503 100644 --- a/ui/admin/app/templates/scopes/scope/auth-methods/index.hbs +++ b/ui/admin/app/templates/scopes/scope/auth-methods/index.hbs @@ -18,7 +18,7 @@ {{#if (can 'create model' this.scopeModel collection='auth-methods')}} - + - - - {{#if @model}} + + {{#if @model.authMethodsExist}} {{#unless this.hasPrimaryAuthMethod}} {{/unless}} - {{/if}} - {{#if - (or - @model - (has-resource-filter-selections 'scopes.scope.auth-methods' 'type') - ) - }} - - - {{#let - (resource-filter 'scopes.scope.auth-methods' 'type') - as |filter| + + + + + + + + + + + + + {{#if @model.authMethods}} + - - - - - - - {{/let}} - - {{#if - (has-resource-filter-selections 'scopes.scope.auth-methods' 'type') - }} - {{t 'actions.clear-all-filters'}} - {{/if}} - - - {{/if}} - - {{#if @model}} - - <:body as |B|> - - - {{#if (can 'read auth-method' B.data)}} - - {{B.data.displayName}} - - {{else}} - {{B.data.displayName}} - {{/if}} -

{{B.data.description}}

-
- - - - - - - - {{#if B.data.isPrimary}} - - {{/if}} - - - {{#if (can 'makePrimary auth-method' B.data)}} - - - {{#unless B.data.isPrimary}} - - {{/unless}} - {{#if B.data.isPrimary}} - - {{/if}} - - {{/if}} - -
- -
- - {{else if - (has-resource-filter-selections 'scopes.scope.auth-methods' 'type') - }} - - - - - {{t 'descriptions.no-results'}} - - - - - {{else}} - - - - - {{#if (can 'list model' this.scopeModel collection='auth-methods')}} - {{! can list (at least): show default welcome message}} - {{t 'resources.auth-method.messages.welcome.description'}} - {{else if - (can 'create model' this.scopeModel collection='auth-methods') + <:body as |B|> + + + {{#if (can 'read auth-method' B.data)}} + + {{B.data.displayName}} + + {{else}} + {{B.data.displayName}} + {{/if}} +

{{B.data.description}}

+
+ + + + + + + + {{#if B.data.isPrimary}} + + {{/if}} + + + {{#if (can 'makePrimary auth-method' B.data)}} + + + {{#unless B.data.isPrimary}} + + {{/unless}} + {{#if B.data.isPrimary}} + + {{/if}} + + {{/if}} + +
+ + + + {{else}} + + + + + {{/if}} + {{else}} + + + -
-
- + ) + ) + resource=(t 'resources.auth-method.title_plural') + }} + /> + {{/if}}
\ No newline at end of file diff --git a/ui/admin/tests/acceptance/auth-methods/create-test.js b/ui/admin/tests/acceptance/auth-methods/create-test.js index 981d4351d..c22a998de 100644 --- a/ui/admin/tests/acceptance/auth-methods/create-test.js +++ b/ui/admin/tests/acceptance/auth-methods/create-test.js @@ -25,7 +25,8 @@ module('Acceptance | auth-methods | create', function (hooks) { 'tbody .hds-table__tr:nth-child(1) .hds-table__td:last-child .hds-dropdown-toggle-icon'; const DROPDOWN_SELECTOR_OPTION = '.hds-dropdown__content .hds-dropdown-list-item [type=button]'; - const NEW_DROPDOWN_SELECTOR = '.hds-dropdown-toggle-button'; + const NEW_DROPDOWN_SELECTOR = + '[data-test-new-dropdown] .hds-dropdown-toggle-button'; const SAVE_BTN_SELECTOR = '.rose-form-actions [type="submit"]'; const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]'; const NAME_INPUT_SELECTOR = '[name="name"]'; diff --git a/ui/admin/tests/acceptance/auth-methods/delete-test.js b/ui/admin/tests/acceptance/auth-methods/delete-test.js index a47313230..06f2b7511 100644 --- a/ui/admin/tests/acceptance/auth-methods/delete-test.js +++ b/ui/admin/tests/acceptance/auth-methods/delete-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, click, currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { Response } from 'miragejs'; import { @@ -20,6 +21,7 @@ import { TYPE_AUTH_METHOD_LDAP } from 'api/models/auth-method'; module('Acceptance | auth-methods | delete', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; let getAuthMethodCount; diff --git a/ui/admin/tests/acceptance/auth-methods/list-test.js b/ui/admin/tests/acceptance/auth-methods/list-test.js index e5bf26d8b..6a9b7e71e 100644 --- a/ui/admin/tests/acceptance/auth-methods/list-test.js +++ b/ui/admin/tests/acceptance/auth-methods/list-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { visit, find } from '@ember/test-helpers'; +import { visit, click, waitFor, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; @@ -14,31 +14,46 @@ import { //currentSession, //invalidateSession, } from 'ember-simple-auth/test-support'; +import { + TYPE_AUTH_METHOD_PASSWORD, + TYPE_AUTH_METHOD_OIDC, +} from 'api/models/auth-method'; module('Acceptance | auth-methods | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); setupIndexedDb(hooks); + const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]'; + const NO_RESULTS_MSG_SELECTOR = '[data-test-no-auth-method-results]'; + const FILTER_DROPDOWN_SELECTOR = (name) => + `.search-filtering [name="${name}"] button`; + const FILTER_APPLY_BUTTON_SELECTOR = + '.search-filtering [data-test-dropdown-apply-button]'; + const AUTH_ACTIONS_SELECTOR = (id) => + `tbody [data-test-auth-methods-table-row="${id}"] .hds-table__td:last-child .hds-dropdown button`; + const MAKE_PRIMARY_ACTION_SELECTOR = '[data-test-make-primary-action]'; + const instances = { scopes: { global: null, org: null, }, - orgScope: null, + passwordAuthMethod: null, + oidcAuthMethod: null, }; const urls = { - scopes: { - org: null, - }, - authMethods: null, + globalScope: null, orgScope: null, + authMethods: null, + passwordAuthMethod: null, + oidcAuthMethod: null, }; hooks.beforeEach(function () { instances.scopes.global = this.server.create('scope', { id: 'global' }); - instances.orgScope = this.server.create( + instances.scopes.org = this.server.create( 'scope', { type: 'org', @@ -46,38 +61,123 @@ module('Acceptance | auth-methods | list', function (hooks) { }, 'withChildren', ); + instances.passwordAuthMethod = this.server.create('auth-method', { + scope: instances.scopes.org, + type: TYPE_AUTH_METHOD_PASSWORD, + }); + instances.oidcAuthMethod = this.server.create('auth-method', { + scope: instances.scopes.org, + type: TYPE_AUTH_METHOD_OIDC, + }); + urls.globalScope = `/scopes/global/scopes`; + urls.orgScope = `/scopes/${instances.scopes.org.id}`; + urls.authMethods = `/scopes/${instances.scopes.org.id}/auth-methods`; + urls.passwordAuthMethod = `${urls.authMethods}/${instances.passwordAuthMethod.id}`; + urls.oidcAuthMethod = `${urls.authMethods}/${instances.oidcAuthMethod.id}`; - urls.orgScope = `/scopes/${instances.orgScope.id}`; - urls.authMethods = `/scopes/${instances.orgScope.id}/auth-methods`; authenticateSession({}); }); - test('Users can navigate to auth methods with proper authorization', async function (assert) { - await visit(urls.orgScope); - assert.ok( - instances.orgScope.authorized_collection_actions['auth-methods'].includes( - 'list', - ), + test('users can navigate to auth methods with proper authorization', async function (assert) { + await visit(urls.globalScope); + + await click(`[href="${urls.orgScope}"]`); + + assert.true( + instances.scopes.org.authorized_collection_actions[ + 'auth-methods' + ].includes('list'), ); - assert.ok(find(`[href="${urls.authMethods}"]`)); + assert.dom(`[href="${urls.authMethods}"]`).exists(); }); - test('Users cannot navigate to index without either list or create actions', async function (assert) { - instances.orgScope.authorized_collection_actions['auth-methods'] = []; - await visit(urls.orgScope); - assert.notOk( - instances.orgScope.authorized_collection_actions['auth-methods'].includes( - 'list', - ), + test('users cannot navigate to index without either list or create actions', async function (assert) { + instances.scopes.org.authorized_collection_actions['auth-methods'] = []; + await visit(urls.globalScope); + + await click(`[href="${urls.orgScope}"]`); + + assert.false( + instances.scopes.org.authorized_collection_actions[ + 'auth-methods' + ].includes('list'), ); - assert.notOk(find(`[href="${urls.authMethods}"]`)); + assert.dom(`[href="${urls.authMethods}"]`).doesNotExist(); }); - test('User can navigate to index with only create action', async function (assert) { - instances.orgScope.authorized_collection_actions['auth-methods'] = [ + test('users can navigate to index with only create action', async function (assert) { + instances.scopes.org.authorized_collection_actions['auth-methods'] = [ 'create', ]; + await visit(urls.globalScope); + + await click(`[href="${urls.orgScope}"]`); + + assert.dom(`[href="${urls.authMethods}"]`).exists(); + }); + + test('user can search for a specifc auth-method by id', async function (assert) { await visit(urls.orgScope); - assert.ok(find(`[href="${urls.authMethods}"]`)); + + await click(`[href="${urls.authMethods}"]`); + + assert.dom(`[href="${urls.passwordAuthMethod}"]`).exists(); + assert.dom(`[href="${urls.oidcAuthMethod}"]`).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, instances.passwordAuthMethod.id); + await waitFor(`[href="${urls.oidcAuthMethod}"]`, { count: 0 }); + + assert.dom(`[href="${urls.passwordAuthMethod}"]`).exists(); + assert.dom(`[href="${urls.oidcAuthMethod}"]`).doesNotExist(); + }); + + test('user can search for auth-methods and get no results', async function (assert) { + await visit(urls.orgScope); + + await click(`[href="${urls.authMethods}"]`); + + assert.dom(`[href="${urls.passwordAuthMethod}"]`).exists(); + assert.dom(`[href="${urls.oidcAuthMethod}"]`).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, 'fake target that does not exist'); + await waitFor(NO_RESULTS_MSG_SELECTOR, { count: 1 }); + + assert.dom(`[href="${urls.passwordAuthMethod}"]`).doesNotExist(); + assert.dom(`[href="${urls.oidcAuthMethod}"]`).doesNotExist(); + assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found'); + }); + + test('user can filter for auth-methods by type', async function (assert) { + await visit(urls.orgScope); + + await click(`[href="${urls.authMethods}"]`); + + assert.dom(`[href="${urls.passwordAuthMethod}"]`).exists(); + assert.dom(`[href="${urls.oidcAuthMethod}"]`).exists(); + + await click(FILTER_DROPDOWN_SELECTOR('type')); + await click(`input[value="${TYPE_AUTH_METHOD_PASSWORD}"]`); + await click(FILTER_APPLY_BUTTON_SELECTOR); + + assert.dom(`[href="${urls.oidcAuthMethod}"]`).doesNotExist(); + assert.dom(`[href="${urls.passwordAuthMethod}"]`).exists(); + }); + + test('user can filter for auth-methods by primary', async function (assert) { + await visit(urls.orgScope); + + await click(`[href="${urls.authMethods}"]`); + + assert.dom(`[href="${urls.passwordAuthMethod}"]`).exists(); + assert.dom(`[href="${urls.oidcAuthMethod}"]`).exists(); + + await click(AUTH_ACTIONS_SELECTOR(instances.passwordAuthMethod.id)); + await click(MAKE_PRIMARY_ACTION_SELECTOR); + await click(FILTER_DROPDOWN_SELECTOR('primary')); + await click(`input[value="true"]`); + await click(FILTER_APPLY_BUTTON_SELECTOR); + + assert.dom(`[href="${urls.oidcAuthMethod}"]`).doesNotExist(); + assert.dom(`[href="${urls.passwordAuthMethod}"]`).exists(); }); }); diff --git a/ui/admin/tests/acceptance/auth-methods/read-test.js b/ui/admin/tests/acceptance/auth-methods/read-test.js index 36a33fd62..b3d1994cd 100644 --- a/ui/admin/tests/acceptance/auth-methods/read-test.js +++ b/ui/admin/tests/acceptance/auth-methods/read-test.js @@ -21,16 +21,12 @@ module('Acceptance | auth-methods | read', function (hooks) { setupMirage(hooks); setupIndexedDb(hooks); - const AUTH_LINK_SELECTOR = 'main tbody .hds-table__td:nth-child(1) a'; - const AUTH_TYPE_SELECTOR = - 'tbody .hds-table__tr:nth-child(1) .hds-table__td:nth-child(2)'; - const AUTH_ACTIONS_SELECTOR = - 'tbody .hds-table__tr:nth-child(1) .hds-table__td:last-child .hds-dropdown'; - const LDAP_AUTH_LINK_SELECTOR = 'tbody .hds-table__tr:nth-child(2) a'; - const LDAP_AUTH_TYPE_SELECTOR = - 'tbody .hds-table__tr:nth-child(2) .hds-table__td:nth-child(2)'; - const LDAP_AUTH_ACTIONS_SELECTOR = - 'tbody .hds-table__tr:nth-child(2) .hds-table__td:last-child .hds-dropdown'; + const AUTH_LINK_SELECTOR = (id) => + `tbody [data-test-auth-methods-table-row="${id}"] a`; + const AUTH_TYPE_SELECTOR = (id) => + `tbody [data-test-auth-methods-table-row="${id}"] .hds-table__td:nth-child(2)`; + const AUTH_ACTIONS_SELECTOR = (id) => + `tbody [data-test-auth-methods-table-row="${id}"] .hds-table__td:last-child .hds-dropdown`; const instances = { scopes: { @@ -99,10 +95,14 @@ module('Acceptance | auth-methods | read', function (hooks) { await click(`[href="${urls.orgAuthMethods}"]`); - assert.dom(AUTH_TYPE_SELECTOR).hasText('Password'); - assert.dom(AUTH_ACTIONS_SELECTOR).exists(); + assert + .dom(AUTH_TYPE_SELECTOR(instances.passwordAuthMethodOrg.id)) + .hasText('Password'); + assert + .dom(AUTH_ACTIONS_SELECTOR(instances.passwordAuthMethodOrg.id)) + .exists(); - await click(AUTH_LINK_SELECTOR); + await click(AUTH_LINK_SELECTOR(instances.passwordAuthMethodOrg.id)); await a11yAudit(); assert.strictEqual(currentURL(), urls.passwordAuthMethodOrg); @@ -113,10 +113,14 @@ module('Acceptance | auth-methods | read', function (hooks) { await click(`[href="${urls.globalAuthMethods}"]`); - assert.dom(AUTH_TYPE_SELECTOR).hasText('OIDC'); - assert.dom(AUTH_ACTIONS_SELECTOR).exists(); + assert + .dom(AUTH_TYPE_SELECTOR(instances.oidcAuthMethodGlobal.id)) + .hasText('OIDC'); + assert + .dom(AUTH_ACTIONS_SELECTOR(instances.oidcAuthMethodGlobal.id)) + .exists(); - await click(AUTH_LINK_SELECTOR); + await click(AUTH_LINK_SELECTOR(instances.oidcAuthMethodGlobal.id)); await a11yAudit(); assert.strictEqual(currentURL(), urls.oidcAuthMethodGlobal); @@ -131,7 +135,9 @@ module('Acceptance | auth-methods | read', function (hooks) { await click(`[href="${urls.orgAuthMethods}"]`); - assert.dom(AUTH_LINK_SELECTOR).doesNotExist(); + assert + .dom(AUTH_LINK_SELECTOR(instances.passwordAuthMethodOrg.id)) + .doesNotExist(); }); test('cannot navigate to an auth method form without proper authorization in global scope', async function (assert) { @@ -143,7 +149,9 @@ module('Acceptance | auth-methods | read', function (hooks) { await click(`[href="${urls.globalAuthMethods}"]`); - assert.dom(AUTH_LINK_SELECTOR).doesNotExist(); + assert + .dom(AUTH_LINK_SELECTOR(instances.oidcAuthMethodGlobal.id)) + .doesNotExist(); }); test('cannot navigate to an ldap auth method form in global scope', async function (assert) { @@ -155,9 +163,11 @@ module('Acceptance | auth-methods | read', function (hooks) { await click(`[href="${urls.globalAuthMethods}"]`); - assert.dom(LDAP_AUTH_LINK_SELECTOR).doesNotExist(); - assert.dom(LDAP_AUTH_TYPE_SELECTOR).hasText('LDAP'); - assert.dom(LDAP_AUTH_ACTIONS_SELECTOR).doesNotExist(); + assert.dom(AUTH_LINK_SELECTOR(instances.ldapAuthMethod.id)).doesNotExist(); + assert.dom(AUTH_TYPE_SELECTOR(instances.ldapAuthMethod.id)).hasText('LDAP'); + assert + .dom(AUTH_ACTIONS_SELECTOR(instances.ldapAuthMethod.id)) + .doesNotExist(); }); test('cannot navigate to an ldap auth method form in org scope', async function (assert) { @@ -169,9 +179,11 @@ module('Acceptance | auth-methods | read', function (hooks) { await click(`[href="${urls.orgAuthMethods}"]`); - assert.dom(LDAP_AUTH_LINK_SELECTOR).doesNotExist(); - assert.dom(LDAP_AUTH_TYPE_SELECTOR).hasText('LDAP'); - assert.dom(LDAP_AUTH_ACTIONS_SELECTOR).doesNotExist(); + assert.dom(AUTH_LINK_SELECTOR(instances.ldapAuthMethod.id)).doesNotExist(); + assert.dom(AUTH_TYPE_SELECTOR(instances.ldapAuthMethod.id)).hasText('LDAP'); + assert + .dom(AUTH_ACTIONS_SELECTOR(instances.ldapAuthMethod.id)) + .doesNotExist(); }); test('users can navigate to auth method and incorrect url autocorrects', async function (assert) { diff --git a/ui/admin/tests/acceptance/auth-methods/update-test.js b/ui/admin/tests/acceptance/auth-methods/update-test.js index a75a96f88..964dbbe47 100644 --- a/ui/admin/tests/acceptance/auth-methods/update-test.js +++ b/ui/admin/tests/acceptance/auth-methods/update-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, click, fillIn, select, findAll } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { Response } from 'miragejs'; import { @@ -23,6 +24,7 @@ import { module('Acceptance | auth-methods | update', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; diff --git a/ui/admin/tests/acceptance/authentication-test.js b/ui/admin/tests/acceptance/authentication-test.js index eefc4e935..522b0762f 100644 --- a/ui/admin/tests/acceptance/authentication-test.js +++ b/ui/admin/tests/acceptance/authentication-test.js @@ -37,7 +37,8 @@ module('Acceptance | authentication', function (hooks) { let indexURL; let globalScope; - let orgScope; + let orgScope1; + let orgScope2; let orgScopeID; let scopesURL; let orgScopeURL; @@ -70,7 +71,15 @@ module('Acceptance | authentication', function (hooks) { invalidateSession(); indexURL = '/'; globalScope = this.server.create('scope', { id: 'global' }); - orgScope = this.server.create( + orgScope1 = this.server.create( + 'scope', + { + type: 'org', + scope: { id: globalScope.id, type: globalScope.type }, + }, + 'withChildren', + ); + orgScope2 = this.server.create( 'scope', { type: 'org', @@ -87,20 +96,20 @@ module('Acceptance | authentication', function (hooks) { }, 'withChildren', ); - scope = { id: orgScope.id, type: orgScope.type }; + scope = { id: orgScope1.id, type: orgScope1.type }; globalAuthMethod = this.server.create('auth-method', { scope: globalScope, type: TYPE_AUTH_METHOD_PASSWORD, }); authMethod = this.server.create('auth-method', { - scope: orgScope, + scope: orgScope1, type: TYPE_AUTH_METHOD_PASSWORD, }); authMethodOIDC = this.server.create('auth-method', { - scope: orgScope, + scope: orgScope2, type: TYPE_AUTH_METHOD_OIDC, }); - orgScopeID = orgScope.id; + orgScopeID = orgScope1.id; globalAuthMethodID = globalAuthMethod.id; authMethodID = authMethod.id; authMethodOIDCID = authMethodOIDC.id; @@ -110,7 +119,7 @@ module('Acceptance | authentication', function (hooks) { authenticateURL = `/scopes/${orgScopeID}/authenticate`; authMethodGlobalAuthenticateURL = `/scopes/global/authenticate/${globalAuthMethodID}`; authMethodAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodID}`; - authMethodOIDCAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodOIDCID}`; + authMethodOIDCAuthenticateURL = `/scopes/${orgScope2.id}/authenticate/${authMethodOIDCID}`; changePasswordURL = `/account/change-password`; orgsURL = `/scopes/global/scopes`; orgEditURL = `/scopes/${orgScopeID}/edit`; @@ -383,6 +392,6 @@ module('Acceptance | authentication', function (hooks) { await click('.rose-dropdown-trigger'); - assert.dom('.rose-dropdown-content a').exists({ count: 2 }); + assert.dom('.rose-dropdown-content a').exists({ count: 3 }); }); }); diff --git a/ui/admin/tests/acceptance/managed-groups/create-test.js b/ui/admin/tests/acceptance/managed-groups/create-test.js index b306ce51d..75a49c55a 100644 --- a/ui/admin/tests/acceptance/managed-groups/create-test.js +++ b/ui/admin/tests/acceptance/managed-groups/create-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; 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 { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession } from 'ember-simple-auth/test-support'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { Response } from 'miragejs'; @@ -18,6 +19,7 @@ import { module('Acceptance | managed-groups | create', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let getManagedGroupCount; let featuresService; diff --git a/ui/admin/tests/acceptance/managed-groups/list-test.js b/ui/admin/tests/acceptance/managed-groups/list-test.js index e6d7bce17..05ab53b79 100644 --- a/ui/admin/tests/acceptance/managed-groups/list-test.js +++ b/ui/admin/tests/acceptance/managed-groups/list-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession } from 'ember-simple-auth/test-support'; import { TYPE_AUTH_METHOD_OIDC, @@ -16,6 +17,7 @@ import { module('Acceptance | managed-groups | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); let featuresService; diff --git a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/index-test.js b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/index-test.js index b506a97a7..9276cc3d9 100644 --- a/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/index-test.js +++ b/ui/admin/tests/unit/controllers/scopes/scope/auth-methods/index-test.js @@ -4,7 +4,13 @@ */ import { module, test } from 'qunit'; -import { setupTest } from 'admin/tests/helpers'; +import { setupTest } from 'ember-qunit'; +import { waitUntil } from '@ember/test-helpers'; +import { + TYPE_AUTH_METHOD_PASSWORD, + TYPE_AUTH_METHOD_OIDC, + TYPE_AUTH_METHOD_LDAP, +} from 'api/models/auth-method'; module('Unit | Controller | scopes/scope/auth-methods/index', function (hooks) { setupTest(hooks); @@ -15,4 +21,56 @@ module('Unit | Controller | scopes/scope/auth-methods/index', function (hooks) { ); assert.ok(controller); }); + + test('primaryOptions returns expected object', function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/auth-methods/index', + ); + assert.deepEqual(controller.primaryOptions, [ + { id: 'true', name: 'Yes' }, + { id: 'false', name: 'No' }, + ]); + }); + + test('authMethodTypeOptions returns expected object', function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/auth-methods/index', + ); + assert.deepEqual(controller.authMethodTypeOptions, [ + { id: TYPE_AUTH_METHOD_PASSWORD, name: 'Password' }, + { id: TYPE_AUTH_METHOD_OIDC, name: 'OIDC' }, + { id: TYPE_AUTH_METHOD_LDAP, name: 'LDAP' }, + ]); + }); + + test('filters returns expected entries', function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/auth-methods/index', + ); + assert.ok(controller.filters.allFilters); + assert.ok(controller.filters.selectedFilters); + }); + + test('handleSearchInput action sets expected values correctly', async function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/auth-methods/index', + ); + const searchValue = 'test'; + controller.handleSearchInput({ target: { value: searchValue } }); + await waitUntil(() => controller.search === searchValue); + + assert.strictEqual(controller.page, 1); + assert.strictEqual(controller.search, searchValue); + }); + + test('applyFilter action sets expected values correctly', async function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/auth-methods/index', + ); + const selectedItems = ['true']; + controller.applyFilter('primary', selectedItems); + + assert.strictEqual(controller.page, 1); + assert.deepEqual(controller.primary, selectedItems); + }); }); diff --git a/ui/admin/tests/unit/controllers/scopes/scope/targets/index-test.js b/ui/admin/tests/unit/controllers/scopes/scope/targets/index-test.js index 3ea5e49c5..aa83556be 100644 --- a/ui/admin/tests/unit/controllers/scopes/scope/targets/index-test.js +++ b/ui/admin/tests/unit/controllers/scopes/scope/targets/index-test.js @@ -14,8 +14,8 @@ module('Unit | Controller | scopes/scope/targets/index', function (hooks) { test('availableSessionOptions returns expected object', function (assert) { let controller = this.owner.lookup('controller:scopes/scope/targets/index'); assert.deepEqual(controller.availableSessionOptions, [ - { id: 'yes', name: 'Yes' }, - { id: 'no', name: 'No' }, + { id: 'yes', name: 'Has active sessions' }, + { id: 'no', name: 'No active sessions' }, ]); });