Icu 12592 add filtering pagination support for auth methods (#2209)

* feat: 🎸 add filtering & pagination to auth methods

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12592

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
Co-authored-by: Cameron Perera <cameron.perera@hashicorp.com>
This commit is contained in:
authored and Zhihe Li committed 2024-04-12 14:55:34 -04:00
1 parent 13da35c06b
commit ded8bdadd1
22 files changed
+623 -327

No files matched your search

@@ -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,
+2
View File
@@ -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) =>
+1 -22
View File
@@ -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') {
@@ -62,6 +62,7 @@ auth-method:
searching: Searching
finding: Finding
always: Always
primary: Primary
types:
password: Password
oidc: OIDC
@@ -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
@@ -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'),
},
];
}
@@ -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 = [];
}
}
@@ -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<boolean>}
*/
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
@@ -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 }) =>
@@ -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(
@@ -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 }),
),
@@ -18,7 +18,7 @@
<page.actions>
{{#if (can 'create model' this.scopeModel collection='auth-methods')}}
<Hds::Dropdown as |dd|>
<Hds::Dropdown data-test-new-dropdown as |dd|>
<dd.ToggleButton @text={{t 'titles.new'}} @color='secondary' />
<dd.Interactive
@text={{t 'resources.auth-method.types.password'}}
@@ -41,9 +41,8 @@
{{/if}}
</page.actions>
<page.body>
{{#if @model}}
<page.body class='search-filtering'>
{{#if @model.authMethodsExist}}
{{#unless this.hasPrimaryAuthMethod}}
<Hds::Alert
class='auth-methods-list-alert'
@@ -61,172 +60,154 @@
</A.Description>
</Hds::Alert>
{{/unless}}
{{/if}}
{{#if
(or
@model
(has-resource-filter-selections 'scopes.scope.auth-methods' 'type')
)
}}
<Rose::Toolbar>
<Rose::Form as |form|>
{{#let
(resource-filter 'scopes.scope.auth-methods' 'type')
as |filter|
<Hds::SegmentedGroup as |S|>
<S.TextInput
@value={{this.search}}
@type='search'
placeholder={{t 'actions.search'}}
aria-label={{t 'actions.search'}}
{{on 'input' this.handleSearchInput}}
/>
<S.Generic>
<Dropdown
name='type'
@toggleText={{t 'form.type.label'}}
@itemOptions={{this.authMethodTypeOptions}}
@checkedItems={{this.types}}
@applyFilter={{fn this.applyFilter 'types'}}
/>
</S.Generic>
<S.Generic>
<Dropdown
name='primary'
@toggleText={{t 'resources.auth-method.titles.primary'}}
@itemOptions={{this.primaryOptions}}
@checkedItems={{this.primary}}
@applyFilter={{fn this.applyFilter 'primary'}}
/>
</S.Generic>
</Hds::SegmentedGroup>
<FilterTags @filters={{this.filters}} />
{{#if @model.authMethods}}
<Hds::Table
class='auth-methods-list-table'
@model={{@model.authMethods}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.type.label'))
(hash label=(t 'form.id.label'))
(hash label='')
(hash label=(t 'titles.actions'))
}}
<Rose::Dropdown
@text={{t (concat 'form.' filter.name '.label')}}
@count={{filter.selectedValue.length}}
@ignoreClickInside={{true}}
as |dropdown|
>
<form.checkboxGroup
@name={{filter.name}}
@items={{filter.allowedValues}}
@selectedItems={{filter.selectedValue}}
@onChange={{route-action 'filterBy' filter.name}}
as |group|
>
<dropdown.item>
<group.checkbox
@label={{t
(concat 'resources.auth-method.types.' group.item)
}}
value={{group.item}}
/>
</dropdown.item>
</form.checkboxGroup>
</Rose::Dropdown>
{{/let}}
{{#if
(has-resource-filter-selections 'scopes.scope.auth-methods' 'type')
}}
<Rose::Button
@style='inline-link-action'
@iconLeft='flight-icons/svg/x-16'
{{on 'click' (route-action 'clearAllFilters')}}
>{{t 'actions.clear-all-filters'}}</Rose::Button>
{{/if}}
</Rose::Form>
</Rose::Toolbar>
{{/if}}
{{#if @model}}
<Hds::Table
class='auth-methods-list-table'
@model={{@model}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.type.label'))
(hash label=(t 'form.id.label'))
(hash label='')
(hash label=(t 'titles.actions'))
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{#if (can 'read auth-method' B.data)}}
<LinkTo
@route='scopes.scope.auth-methods.auth-method'
@model={{B.data.id}}
>
{{B.data.displayName}}
</LinkTo>
{{else}}
{{B.data.displayName}}
{{/if}}
<p>{{B.data.description}}</p>
</B.Td>
<B.Td>
<Hds::Badge
@text={{t (concat 'resources.auth-method.types.' B.data.type)}}
/>
</B.Td>
<B.Td>
<Hds::Copy::Snippet
@textToCopy={{B.data.id}}
@color='secondary'
/>
</B.Td>
<B.Td>
{{#if B.data.isPrimary}}
<Hds::Badge @text={{t 'states.primary'}} @color='success' />
{{/if}}
</B.Td>
<B.Td>
{{#if (can 'makePrimary auth-method' B.data)}}
<Hds::Dropdown as |dd|>
<dd.ToggleIcon
@icon='more-horizontal'
@text={{t 'actions.manage'}}
@hasChevron={{false}}
/>
{{#unless B.data.isPrimary}}
<dd.Interactive
@text={{t 'resources.auth-method.actions.make-primary'}}
{{on 'click' (route-action 'makePrimary' B.data)}}
/>
{{/unless}}
{{#if B.data.isPrimary}}
<dd.Interactive
@text={{t
'resources.auth-method.actions.remove-as-primary'
}}
{{on 'click' (route-action 'removeAsPrimary' B.data)}}
/>
{{/if}}
</Hds::Dropdown>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{else if
(has-resource-filter-selections 'scopes.scope.auth-methods' 'type')
}}
<Rose::Layout::Centered>
<Rose::Message @title={{t 'titles.no-results'}} as |message|>
<message.description>
{{t 'descriptions.no-results'}}
</message.description>
</Rose::Message>
</Rose::Layout::Centered>
{{else}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t 'resources.auth-method.messages.welcome.title'}}
as |message|
@density='short'
@valign='middle'
>
<message.description>
{{#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|>
<B.Tr data-test-auth-methods-table-row={{B.data.id}}>
<B.Td>
{{#if (can 'read auth-method' B.data)}}
<LinkTo
@route='scopes.scope.auth-methods.auth-method'
@model={{B.data.id}}
>
{{B.data.displayName}}
</LinkTo>
{{else}}
{{B.data.displayName}}
{{/if}}
<p>{{B.data.description}}</p>
</B.Td>
<B.Td>
<Hds::Badge
@text={{t
(concat 'resources.auth-method.types.' B.data.type)
}}
/>
</B.Td>
<B.Td>
<Hds::Copy::Snippet
@textToCopy={{B.data.id}}
@color='secondary'
/>
</B.Td>
<B.Td>
{{#if B.data.isPrimary}}
<Hds::Badge @text={{t 'states.primary'}} @color='success' />
{{/if}}
</B.Td>
<B.Td>
{{#if (can 'makePrimary auth-method' B.data)}}
<Hds::Dropdown as |dd|>
<dd.ToggleIcon
@icon='more-horizontal'
@text={{t 'actions.manage'}}
@hasChevron={{false}}
/>
{{#unless B.data.isPrimary}}
<dd.Interactive
@text={{t 'resources.auth-method.actions.make-primary'}}
data-test-make-primary-action
{{on 'click' (route-action 'makePrimary' B.data)}}
/>
{{/unless}}
{{#if B.data.isPrimary}}
<dd.Interactive
@text={{t
'resources.auth-method.actions.remove-as-primary'
}}
{{on 'click' (route-action 'removeAsPrimary' B.data)}}
/>
{{/if}}
</Hds::Dropdown>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Rose::Pagination
@totalItems={{@model.totalItems}}
@currentPage={{this.page}}
@currentPageSize={{this.pageSize}}
/>
{{else}}
<Hds::ApplicationState data-test-no-auth-method-results as |A|>
<A.Header @title={{t 'titles.no-results-found'}} />
<A.Body
@text={{t
(if
this.search
'descriptions.no-search-results'
'descriptions.no-filter-results'
)
query=this.search
resource=(t 'resources.auth-method.title_plural')
}}
{{! can create (only): show create-but-not-list welcome message}}
{{t
/>
</Hds::ApplicationState>
{{/if}}
{{else}}
<Hds::ApplicationState as |A|>
<A.Header @title={{t 'resources.auth-method.messages.welcome.title'}} />
<A.Body
{{! can list (at least): show default welcome message}}
{{! can create (only): show create-but-not-list welcome message}}
{{! can neither list nor create: show neither-list-nor-create welcome message}}
@text={{t
(if
(can 'list model' this.scopeModel collection='auth-methods')
'resources.auth-method.description'
(if
(can 'create model' this.scopeModel collection='auth-methods')
'descriptions.create-but-not-list'
resource=(t 'resources.auth-method.title_plural')
}}
{{else}}
{{! can neither list nor create }}
{{t
'descriptions.neither-list-nor-create'
resource=(t 'resources.auth-method.title_plural')
}}
{{/if}}
</message.description>
</Rose::Message>
</Rose::Layout::Centered>
)
)
resource=(t 'resources.auth-method.title_plural')
}}
/>
</Hds::ApplicationState>
{{/if}}
</page.body>
</Rose::Layout::Page>
@@ -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"]';
@@ -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;
@@ -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();
});
});
@@ -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) {
@@ -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;
@@ -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 });
});
});
@@ -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;
@@ -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;
@@ -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);
});
});
@@ -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' },
]);
});