Grant actions card (#3246)
* chore: 🤖 Add initial page layout for code editor + sidebar (#3180) * chore: 🤖 Add initial page layout for code editor + sidebar * test: 💍 add missing a11y ignores for exisitng color issue ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18460 * Replace `Rose::CodeEditor` with `Hds::CodeEditor` (#2679) * chore: 🤖 replace Rose::CodeEditor with Hds::CodeEditor - Remove CodeEditor 5 dependency * chore: 🤖 Replace CodeMirror5 dep remove CodeMirror5 and replace existing editors with HDS::CodeEditor * chore: 🤖 deleting old codeeditor styles * feat: 🎸 changing code editor changing code editor ✅ Closes: 0 * removed unused export * pr cleanup * updating tests for new code editor * responding to PR feedback * added whitespace * fixing linting violations * chore: 🤖 self-review PR self-review PR in the course of switching to new HDS code editor * fix: 🐛 fix linting bug * fix: 🐛 unneeded dependency remove jslint dependency as this is handled in the new code editor from HDS * fix: 🐛 Fixing import bug Fix import bug caused by importing scss modules that no longer exist * Fix and refactor code editor acceptance test * Fix and refactor code editor acceptance test * chore: 🤖 remove codemirror dependency * refactor: 💡 fix merge conflict issues * test: 💍 update e2e test selectors * test: 💍 fix target spec click event * refactor: 💡 clean up test selectors for code editor * refactor: 💡 clean up storage bucket test selectors * chore: 🤖 remove code mirror modifiers --------- Co-authored-by: calcaide <ccorvo@hashicorp.com> Co-authored-by: cameronperera <cameron.perera@hashicorp.com> * feat: 🎸 Create string formats component (#3191) * feat: 🎸 Create string formats component * Add missing copyright headers * fix: 🐛 change to readonly ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18458 * feat: 🎸 add grant schema to mirage (#3196) * feat: 🎸 add grant schema to mirage * refactor: 💡 update grant schema mirage data * refactor: 💡 fix route name in mirage * feat: 🎸 add export options flyout (#3198) * feat: 🎸 add export options flyout * fix: 🐛 update id to be ids * test: 💍 add a11y ignores * test: 💍 fix tests for grant string format ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18524 * feat: 🎸 Add autocompletion to code editor (#3207) * feat: 🎸 create grant actions component * feat: 🎸 add description to actions * feat: 🎸 update default logic to not show all actions * feat: 🎸 show invalid id and type message * test: 💍 create const for test selectors * feat: 🎸 trigger action suggestion when cursor moves * refactor: 💡 add createGrantLineHelpers func * refactor: 💡 small refactors * feat: 🎸 Add CSP nonce to code editors (#3230) Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com> * refactor: 💡 small refactor * test: 💍 add more tests * refactor: 💡 fix merge conflicts issues * refactor: 💡 updates from PR feedback * refactor: 💡 updates for completions and linting * test: 💍 fix test issue with translations * test: 💍 fix test issue that was already fixed once * refactor: 💡 address PR feedback --------- Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com> Co-authored-by: Zack Moore <zack.moore@hashicorp.com> Co-authored-by: calcaide <ccorvo@hashicorp.com> Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com> Co-authored-by: Bharath Gajjala <120367134+bgajjala8@users.noreply.github.com>
This commit is contained in:
1 parent
1a8c895ea9
commit
08b8b03fed
12 files changed
+880
-29
No files matched your search
@@ -24,6 +24,7 @@ titles:
|
||||
empty-set: No Items Yet
|
||||
global: Global
|
||||
actions: Actions
|
||||
action: Action
|
||||
documentation: Documentation
|
||||
abandon-confirm: Discard unsaved changes?
|
||||
welcome-to-boundary: Welcome to Boundary
|
||||
|
||||
@@ -753,6 +753,76 @@ role:
|
||||
options:
|
||||
terraform: Terraform
|
||||
native-hcl: Native HCL
|
||||
actions:
|
||||
collection-actions: Collection Actions
|
||||
id-actions: ID Actions
|
||||
create: Create a new {resourceType}
|
||||
list: List all {resourceType} resources
|
||||
read: Read a {resourceType}
|
||||
update: Update a {resourceType}
|
||||
delete: Delete a {resourceType}
|
||||
read:self: Read the {resourceType} associated with the current user
|
||||
delete:self: Delete a {resourceType} associated with the current user
|
||||
cancel: Cancel a {resourceType}
|
||||
cancel:self: Cancel a {resourceType} associated with the current user
|
||||
generic:
|
||||
create: Creates resources
|
||||
list: Lists resources
|
||||
read: Reads resources
|
||||
update: Updates resources
|
||||
delete: Deletes resources
|
||||
read:self: Read the resource associated with the current user
|
||||
delete:self: Delete a resource associated with the current user
|
||||
add-members: Add members to a group
|
||||
set-members: Set the full list of members on a group
|
||||
remove-members: Remove members from a group
|
||||
add-hosts: Add hosts to a host set
|
||||
set-hosts: Set the full list of hosts on a host set
|
||||
remove-hosts: Remove hosts from a host set
|
||||
add-worker-tags: Add tags to a worker
|
||||
set-worker-tags: Set the full list of tags on a worker
|
||||
remove-worker-tags: Remove tags from a worker
|
||||
reinitialize-certificate-authority: Reinitialize the certificate authority that is used to authorize Boundary workers
|
||||
create:controller-led: Create a worker via controller-led workflow
|
||||
create:worker-led: Create a worker via worker-led workflow
|
||||
read-certificate-authority: Read the certificate authority that is used to authorize Boundary workers
|
||||
set-password: Set a password on an account, without requiring the current password
|
||||
change-password: Change the password on an account
|
||||
authenticate: Authenticate to an auth method
|
||||
change-state: Change the active and visibility state of an OIDC-type auth method
|
||||
add-accounts: Add accounts to a user
|
||||
set-accounts: Set the full list of accounts on a user
|
||||
remove-accounts: Remove accounts from a user
|
||||
list-resolvable-aliases: List all aliases that point to resources the user has permission to access
|
||||
add-principals: Add principals to a role
|
||||
set-principals: Set the full list of principals on a role
|
||||
remove-principals: Remove principals from a role
|
||||
set-grants: Set the full list of grants on a role
|
||||
add-grants: Add grants to a role
|
||||
remove-grants: Remove grants from a role
|
||||
add-grant-scopes: Add grant scopes to a role
|
||||
set-grant-scopes: Set the full list of grant scopes on a role
|
||||
remove-grant-scopes: Remove grant scopes from a role
|
||||
authorize-session: Authorize a session via a target
|
||||
add-host-sources: Add host sources to a target
|
||||
set-host-sources: Set the full list of host sources on a target
|
||||
remove-host-sources: Remove host sources from a target
|
||||
add-credential-sources: Add credential sources to a target
|
||||
set-credential-sources: Set the full list of credential sources on a target
|
||||
remove-credential-sources: Remove credential sources from a target
|
||||
attach-storage-policy: Attach a storage policy to all session recordings in the scope
|
||||
detach-storage-policy: Detach a storage policy from all session recordings in the scope
|
||||
list-keys: List the keys within a given scope
|
||||
rotate-keys: Replace a scope's current KEK and DEKs with a new set of keys
|
||||
list-key-version-destruction-jobs: List all pending key version destruction jobs within a scope
|
||||
destroy-key-version: Destroy a key version in the scope
|
||||
download: Download a session recording
|
||||
reapply-storage-policy: Reapply the storage policy to a session recording
|
||||
monthly-active-users: Display the number of monthly active Boundary users to help predict billing
|
||||
no-actions: No actions available for this resource type.
|
||||
invalid-id-or-type: Invalid ID and type. No actions available.
|
||||
no-type-detected: No resource type detected.
|
||||
unavailable: Grant actions information not available.
|
||||
scope:
|
||||
title: Scope
|
||||
title_plural: Scopes
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
@hasFullScreenButton={{true}}
|
||||
@ariaLabel={{t 'form.secret-editor.label'}}
|
||||
@value={{this.grantStringsText}}
|
||||
@onInput={{this.onInput}}
|
||||
@onSetup={{this.onSetup}}
|
||||
@customExtensions={{this.customExtensions}}
|
||||
@cspNonce={{(csp-nonce)}}
|
||||
@@ -68,6 +67,17 @@
|
||||
<GrantStringFormats />
|
||||
</:content>
|
||||
</A.Item>
|
||||
<A.Item @isOpen={{true}} @isStatic={{true}} data-test-grant-actions>
|
||||
<:toggle>{{t 'titles.actions'}}</:toggle>
|
||||
<:content>
|
||||
<div class='grant-actions-scroll-container'>
|
||||
<GrantActions
|
||||
@grantsSchema={{@grantsSchema}}
|
||||
@grantString={{this.currentLineText}}
|
||||
/>
|
||||
</div>
|
||||
</:content>
|
||||
</A.Item>
|
||||
</Hds::Accordion>
|
||||
</Hds::Layout::Grid>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
autocompletion,
|
||||
completionKeymap,
|
||||
keymap,
|
||||
EditorView,
|
||||
linter,
|
||||
lintGutter,
|
||||
lintKeymap,
|
||||
@@ -172,6 +173,15 @@ export default class FormRoleEditGrantsComponent extends Component {
|
||||
// Trigger autocompletion when the user completes a grant field (which we labeled as keywords)
|
||||
activateOnCompletion: (completion) => completion.type === 'keyword',
|
||||
}),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged || update.selectionSet) {
|
||||
const line = update.state.doc.lineAt(update.state.selection.main.head);
|
||||
this.currentLineText = line.text;
|
||||
if (update.docChanged) {
|
||||
this.grantStringsText = update.state.doc.toString();
|
||||
}
|
||||
}
|
||||
}),
|
||||
linter(this.linterSource),
|
||||
lintGutter(),
|
||||
keymap.of([...completionKeymap, ...lintKeymap]),
|
||||
@@ -233,14 +243,6 @@ export default class FormRoleEditGrantsComponent extends Component {
|
||||
this.#editorView = view;
|
||||
}
|
||||
|
||||
@action
|
||||
onInput(value, view) {
|
||||
this.grantStringsText = value;
|
||||
|
||||
const line = view.state.doc.lineAt(view.state.selection.main.head);
|
||||
this.currentLineText = line.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the export options flyout open and closed.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{{!
|
||||
Copyright IBM Corp. 2021, 2026
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if this.actionRows.length}}
|
||||
<Hds::Table
|
||||
@model={{this.actionRows}}
|
||||
@columns={{array
|
||||
(hash label=(t 'titles.action'))
|
||||
(hash label=(t 'form.description.label'))
|
||||
}}
|
||||
@valign='middle'
|
||||
data-test-grant-actions-table
|
||||
>
|
||||
<:body as |B|>
|
||||
<B.Tr>
|
||||
<B.Td>{{B.data.name}}</B.Td>
|
||||
<B.Td>{{B.data.description}}</B.Td>
|
||||
</B.Tr>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
{{else if this.showInvalidIdAndType}}
|
||||
<Hds::Alert @type='compact' as |A|>
|
||||
<A.Description data-test-grant-actions-invalid-id-or-type>
|
||||
{{t 'resources.role.edit-grants.actions.invalid-id-or-type'}}
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{else if this.showNoResourceTypeDetected}}
|
||||
<Hds::Alert @type='compact' as |A|>
|
||||
<A.Description data-test-grant-actions-no-type-detected>
|
||||
{{t 'resources.role.edit-grants.actions.no-type-detected'}}
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{else}}
|
||||
<Hds::Alert @type='compact' as |A|>
|
||||
<A.Description data-test-grant-actions-empty-state>
|
||||
{{t 'resources.role.edit-grants.completion-info.no-suggestions'}}
|
||||
</A.Description>
|
||||
</Hds::Alert>
|
||||
{{/if}}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2021, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { cached } from '@glimmer/tracking';
|
||||
import { service } from '@ember/service';
|
||||
|
||||
import { analyzeGrantString } from 'admin/utils/grant-completions';
|
||||
|
||||
export default class GrantActionsIndex extends Component {
|
||||
@service intl;
|
||||
|
||||
@cached
|
||||
get grantAnalysis() {
|
||||
return analyzeGrantString(
|
||||
this.args.grantsSchema ?? {},
|
||||
this.args.grantString,
|
||||
);
|
||||
}
|
||||
|
||||
get actionRows() {
|
||||
return this.grantAnalysis.actions.map((action) => ({
|
||||
name: action,
|
||||
description: this.getActionDescription(action),
|
||||
}));
|
||||
}
|
||||
|
||||
get showNoResourceTypeDetected() {
|
||||
const { actions, detectedResourceType } = this.grantAnalysis;
|
||||
return (
|
||||
!this.showInvalidIdAndType && !actions.length && !detectedResourceType
|
||||
);
|
||||
}
|
||||
|
||||
get showInvalidIdAndType() {
|
||||
const {
|
||||
actions,
|
||||
hasInvalidType,
|
||||
hasInvalidIds,
|
||||
hasInvalidPinnedIdTypeCombination,
|
||||
} = this.grantAnalysis;
|
||||
return (
|
||||
!actions.length &&
|
||||
(hasInvalidType || hasInvalidIds || hasInvalidPinnedIdTypeCombination)
|
||||
);
|
||||
}
|
||||
|
||||
getActionDescription(action) {
|
||||
const { detectedResourceType } = this.grantAnalysis;
|
||||
const specificKey = `resources.role.edit-grants.actions.${action}`;
|
||||
const genericKey = `resources.role.edit-grants.actions.generic.${action}`;
|
||||
|
||||
if (detectedResourceType && this.intl.exists(specificKey)) {
|
||||
return this.intl.t(specificKey, { resourceType: detectedResourceType });
|
||||
}
|
||||
|
||||
if (this.intl.exists(genericKey)) {
|
||||
return this.intl.t(genericKey);
|
||||
}
|
||||
|
||||
if (this.intl.exists(specificKey)) {
|
||||
return this.intl.t(specificKey);
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
@@ -1008,3 +1008,8 @@
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.grant-actions-scroll-container {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
const GRANT_FIELDS = ['ids', 'type', 'actions', 'output_fields'];
|
||||
const ID_TEMPLATES = ['{{.User.Id}}', '{{.Account.Id}}'];
|
||||
const CRUDL_ACTIONS = new Set(['create', 'read', 'update', 'delete', 'list']);
|
||||
const TEMPLATE_RESOURCE_TYPES = {
|
||||
'{{.User.Id}}': 'user',
|
||||
'{{.Account.Id}}': 'account',
|
||||
@@ -22,7 +23,7 @@ const getNoSuggestionsOption = (noSuggestionsLabel) => ({
|
||||
apply: () => {},
|
||||
});
|
||||
|
||||
const parseGrantFields = (lineText) =>
|
||||
const parseGrantFields = (lineText = '') =>
|
||||
lineText
|
||||
.split(';')
|
||||
.map((pair) => {
|
||||
@@ -38,8 +39,24 @@ const parseGrantFields = (lineText) =>
|
||||
})
|
||||
.filter(({ fieldName }) => fieldName);
|
||||
|
||||
const normalizeGrantsSchema = (grantsSchema) => {
|
||||
const resourceTypes = grantsSchema?.resource_types ?? [];
|
||||
const getGrantFieldValue = (parsedFields, targetFieldName) =>
|
||||
parsedFields.find(({ fieldName }) => fieldName === targetFieldName)
|
||||
?.fieldValue;
|
||||
|
||||
const parseGrantLine = (lineText = '') => {
|
||||
const parsedFields = parseGrantFields(lineText);
|
||||
|
||||
return {
|
||||
parsedFields,
|
||||
idsValue: getGrantFieldValue(parsedFields, 'ids'),
|
||||
typeValue: getGrantFieldValue(parsedFields, 'type'),
|
||||
actionsValue: getGrantFieldValue(parsedFields, 'actions'),
|
||||
outputFieldsValue: getGrantFieldValue(parsedFields, 'output_fields'),
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeGrantsSchema = (grantsSchema) => {
|
||||
const resourceTypes = grantsSchema.resource_types ?? [];
|
||||
|
||||
const resourcesByType = resourceTypes.reduce((resources, resourceType) => {
|
||||
const collectionActions = resourceType.collection_actions ?? [];
|
||||
@@ -107,7 +124,7 @@ const getResourceTypesForId = (schema, id) => {
|
||||
.map(({ type }) => type);
|
||||
};
|
||||
|
||||
const getCompatibleResourceTypeForIds = (schema, idsValue) => {
|
||||
export const getCompatibleResourceTypeForIds = (schema, idsValue) => {
|
||||
const resourceTypesById = parseIds(idsValue).flatMap((id) =>
|
||||
getResourceTypesForId(schema, id),
|
||||
);
|
||||
@@ -135,7 +152,7 @@ const getIdActions = (schema, idsValue) => {
|
||||
return schema.resourcesByType[matchedType]?.idActions;
|
||||
};
|
||||
|
||||
const getChildResourceActions = (schema, idsValue) => {
|
||||
export const getChildResourceActions = (schema, idsValue) => {
|
||||
const parentType = getCompatibleResourceTypeForIds(schema, idsValue);
|
||||
|
||||
if (
|
||||
@@ -241,9 +258,64 @@ const getActionOptions = (schema, typeValue, idsValue) => {
|
||||
}
|
||||
|
||||
// Should be wildcard IDs with a specific type, so show all actions for that type
|
||||
return selectedResource
|
||||
? withWildCard(selectedResource.actions)
|
||||
: actionOptions;
|
||||
return selectedResource ? withWildCard(selectedResource.actions) : [];
|
||||
};
|
||||
|
||||
export const analyzeGrantString = (grantsSchema, grantString = '') => {
|
||||
const schema = normalizeGrantsSchema(grantsSchema);
|
||||
const { idsValue, typeValue } = parseGrantLine(grantString);
|
||||
|
||||
const hasSpecificIds = Boolean(idsValue) && !idsValue.includes('*');
|
||||
const hasExplicitType = Boolean(typeValue) && typeValue !== '*';
|
||||
|
||||
const hasTemplateIds =
|
||||
hasSpecificIds &&
|
||||
parseIds(idsValue).some((id) => id.startsWith('{{') && id.endsWith('}}'));
|
||||
|
||||
const hasLiteralIds = hasSpecificIds && !hasTemplateIds;
|
||||
|
||||
const compatibleIdsResourceType = hasLiteralIds
|
||||
? getCompatibleResourceTypeForIds(schema, idsValue)
|
||||
: null;
|
||||
|
||||
const hasInvalidType = hasExplicitType && !schema.resourcesByType[typeValue];
|
||||
const hasInvalidIds = hasLiteralIds && !compatibleIdsResourceType;
|
||||
|
||||
const hasInvalidPinnedIdTypeCombination =
|
||||
hasSpecificIds &&
|
||||
hasExplicitType &&
|
||||
!hasInvalidIds &&
|
||||
!hasInvalidType &&
|
||||
!(
|
||||
schema.childResourceTypesByParentType[compatibleIdsResourceType] ?? []
|
||||
).includes(typeValue);
|
||||
|
||||
const detectedResourceType =
|
||||
typeValue && typeValue !== '*'
|
||||
? typeValue
|
||||
: idsValue
|
||||
? getCompatibleResourceTypeForIds(schema, idsValue)
|
||||
: null;
|
||||
|
||||
const isBothWildcard = idsValue === '*' && typeValue === '*';
|
||||
|
||||
const actions =
|
||||
idsValue || typeValue
|
||||
? getActionOptions(schema, typeValue, idsValue)
|
||||
.filter(
|
||||
(action) =>
|
||||
action !== '*' && (!isBothWildcard || CRUDL_ACTIONS.has(action)),
|
||||
)
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
: [];
|
||||
|
||||
return {
|
||||
actions,
|
||||
detectedResourceType,
|
||||
hasInvalidType,
|
||||
hasInvalidIds,
|
||||
hasInvalidPinnedIdTypeCombination,
|
||||
};
|
||||
};
|
||||
|
||||
const getIdLookupTypes = (schema, typeValue, enteredIds) => {
|
||||
@@ -281,16 +353,9 @@ async function grantCompletions(
|
||||
const line = context.state.doc.lineAt(context.pos);
|
||||
const beforeCursor = line.text.slice(0, context.pos - line.from);
|
||||
|
||||
const parsedFields = parseGrantFields(line.text);
|
||||
const { parsedFields, idsValue, typeValue } = parseGrantLine(line.text);
|
||||
const parsedFieldNames = parsedFields.map(({ fieldName }) => fieldName);
|
||||
|
||||
const idsValue = parsedFields.find(
|
||||
({ fieldName }) => fieldName === 'ids',
|
||||
)?.fieldValue;
|
||||
const typeValue = parsedFields.find(
|
||||
({ fieldName }) => fieldName === 'type',
|
||||
)?.fieldValue;
|
||||
|
||||
// Check if we're typing a field name (e.g. "type=") by starting at the beginning or after a semicolon.
|
||||
// This allows us to provide field name suggestions when the user is typing a new field
|
||||
// and restrict suggestions to only fields that haven't been used yet in the current line
|
||||
@@ -338,14 +403,15 @@ async function grantCompletions(
|
||||
const idsValueMatch = beforeCursor.match(/ids=([\w*.{},\s-]*)$/);
|
||||
if (idsValueMatch) {
|
||||
const enteredIds = idsValueMatch[1].split(',');
|
||||
const partial = enteredIds.pop() ?? '';
|
||||
const partial = (enteredIds.pop() ?? '').trim();
|
||||
const hasEnteredIds = enteredIds.length > 0;
|
||||
|
||||
const staticOptions = filterByPrefix(['*', ...ID_TEMPLATES], partial)
|
||||
.filter((value) => !hasEnteredIds || value !== '*')
|
||||
.filter(
|
||||
(value) =>
|
||||
(!hasEnteredIds && !typeValue) || !ID_TEMPLATES.includes(value),
|
||||
!ID_TEMPLATES.includes(value) ||
|
||||
(!typeValue && enteredIds.every((id) => ID_TEMPLATES.includes(id))),
|
||||
)
|
||||
.filter((value) => !enteredIds.includes(value))
|
||||
.map((value) => ({
|
||||
@@ -362,6 +428,8 @@ async function grantCompletions(
|
||||
typeValue,
|
||||
enteredIds,
|
||||
);
|
||||
const from = context.pos - partial.length;
|
||||
|
||||
const ids = await idLookup(partial, compatibleResourceTypes);
|
||||
const idOptions = ids
|
||||
.filter(({ id }) => !enteredIds.includes(id))
|
||||
@@ -383,7 +451,7 @@ async function grantCompletions(
|
||||
}
|
||||
|
||||
return {
|
||||
from: context.pos - partial.length,
|
||||
from,
|
||||
options: allOptions.length
|
||||
? allOptions
|
||||
: [getNoSuggestionsOption(translate('no-suggestions'))],
|
||||
|
||||
@@ -173,4 +173,14 @@ module('Acceptance | roles/edit grants', function (hooks) {
|
||||
expectedGrantStrings,
|
||||
);
|
||||
});
|
||||
|
||||
test('shows actions for the first grant on page load', async function (assert) {
|
||||
await visit(urls.editGrants);
|
||||
|
||||
assert.dom(selectors.GRANT_ACTIONS_NO_TYPE_DETECTED).doesNotExist();
|
||||
// opted to not check for specific actions here since the grants schema
|
||||
// may change and cause the test to fail even though the functionality
|
||||
// is working as expected
|
||||
assert.dom(selectors.GRANT_ACTIONS_TABLE).isVisible();
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,11 @@ export const REMOVE_ORG_ONLY_BTN = (name) =>
|
||||
export const REMOVE_ORG_PROJECTS_BTN = (name) =>
|
||||
`[data-test-manage-scopes-remove-${name}-modal] div:nth-child(3) button`;
|
||||
|
||||
// Grant actions
|
||||
export const GRANT_ACTIONS_TABLE = '[data-test-grant-actions-table]';
|
||||
export const GRANT_ACTIONS_NO_TYPE_DETECTED =
|
||||
'[data-test-grant-actions-no-type-detected]';
|
||||
|
||||
// Role grants
|
||||
export const GRANT_CODE_BLOCK = '.grant-codeblock .hds-code-block__code';
|
||||
export const EXPORT_OPTIONS_FLYOUT = '[data-test-export-options-flyout]';
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Copyright IBM Corp. 2021, 2026
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'admin/tests/helpers';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import * as commonSelectors from 'admin/tests/helpers/selectors';
|
||||
|
||||
const GRANT_ACTIONS_TABLE = '[data-test-grant-actions-table]';
|
||||
const GRANT_ACTIONS_EMPTY_STATE = '[data-test-grant-actions-empty-state]';
|
||||
const GRANT_ACTIONS_NO_TYPE_DETECTED =
|
||||
'[data-test-grant-actions-no-type-detected]';
|
||||
const GRANT_ACTIONS_INVALID_ID_OR_TYPE =
|
||||
'[data-test-grant-actions-invalid-id-or-type]';
|
||||
|
||||
module('Integration | Component | grant-actions/index', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.grantsSchema = {
|
||||
resource_types: [
|
||||
{
|
||||
type: 'host-catalog',
|
||||
collection_actions: ['create', 'list'],
|
||||
id_actions: ['read', 'update', 'delete'],
|
||||
id_prefixes: ['hcst'],
|
||||
},
|
||||
{
|
||||
type: 'host',
|
||||
parent_type: 'host-catalog',
|
||||
collection_actions: ['create', 'list'],
|
||||
id_actions: ['read', 'update', 'delete'],
|
||||
id_prefixes: ['h'],
|
||||
},
|
||||
{
|
||||
type: 'host-set',
|
||||
parent_type: 'host-catalog',
|
||||
collection_actions: ['create', 'list'],
|
||||
id_actions: [
|
||||
'delete',
|
||||
'add-hosts',
|
||||
'set-hosts',
|
||||
'remove-hosts',
|
||||
'read',
|
||||
'update',
|
||||
],
|
||||
id_prefixes: ['hs'],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
test('it renders the actions relevant to the current grant line', async function (assert) {
|
||||
this.grantString = 'ids=hcst_1234567890;type=host-set';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).isVisible();
|
||||
assert.dom(commonSelectors.TABLE_ROWS).isVisible({ count: 8 });
|
||||
|
||||
const rows = [...this.element.querySelectorAll(commonSelectors.TABLE_ROWS)];
|
||||
const rowData = rows.map((row) => {
|
||||
const cells = [...row.querySelectorAll('td')].map((cell) =>
|
||||
cell.textContent.trim(),
|
||||
);
|
||||
|
||||
return {
|
||||
name: cells[0],
|
||||
description: cells[1],
|
||||
};
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
rowData.map(({ name }) => name),
|
||||
[
|
||||
'add-hosts',
|
||||
'create',
|
||||
'delete',
|
||||
'list',
|
||||
'read',
|
||||
'remove-hosts',
|
||||
'set-hosts',
|
||||
'update',
|
||||
],
|
||||
);
|
||||
|
||||
assert.deepEqual(rowData[0], {
|
||||
name: 'add-hosts',
|
||||
description: 'Add hosts to a host set',
|
||||
});
|
||||
|
||||
assert.deepEqual(rowData[1], {
|
||||
name: 'create',
|
||||
description: 'Create a new host-set',
|
||||
});
|
||||
|
||||
assert.deepEqual(rowData.at(-1), {
|
||||
name: 'update',
|
||||
description: 'Update a host-set',
|
||||
});
|
||||
|
||||
assert.dom(GRANT_ACTIONS_EMPTY_STATE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_NO_TYPE_DETECTED).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders the no resource type detected state when the current line has no detectable type', async function (assert) {
|
||||
this.grantString = 'output_fields=*';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_INVALID_ID_OR_TYPE).doesNotExist();
|
||||
assert
|
||||
.dom(GRANT_ACTIONS_NO_TYPE_DETECTED)
|
||||
.hasText('No resource type detected.');
|
||||
assert.dom(GRANT_ACTIONS_EMPTY_STATE).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders the invalid ID and type state for an unknown type', async function (assert) {
|
||||
this.grantString = 'type=not-a-real-type;actions=';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_NO_TYPE_DETECTED).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_EMPTY_STATE).doesNotExist();
|
||||
assert
|
||||
.dom(GRANT_ACTIONS_INVALID_ID_OR_TYPE)
|
||||
.hasText('Invalid ID and type. No actions available.');
|
||||
});
|
||||
|
||||
test('it renders the invalid ID and type state for an unknown ID', async function (assert) {
|
||||
this.grantString = 'ids=bad_123;actions=';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_NO_TYPE_DETECTED).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_EMPTY_STATE).doesNotExist();
|
||||
assert
|
||||
.dom(GRANT_ACTIONS_INVALID_ID_OR_TYPE)
|
||||
.hasText('Invalid ID and type. No actions available.');
|
||||
});
|
||||
|
||||
test('it keeps the no suggestions state when a type is known but no actions apply', async function (assert) {
|
||||
this.grantString = 'type=host;actions=';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_INVALID_ID_OR_TYPE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_NO_TYPE_DETECTED).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_EMPTY_STATE).hasText('No suggestions');
|
||||
});
|
||||
|
||||
test('it renders the no-type-detected state for an empty grant string', async function (assert) {
|
||||
this.grantString = '';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_INVALID_ID_OR_TYPE).doesNotExist();
|
||||
assert
|
||||
.dom(GRANT_ACTIONS_NO_TYPE_DETECTED)
|
||||
.hasText('No resource type detected.');
|
||||
assert.dom(GRANT_ACTIONS_EMPTY_STATE).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders the no-type-detected state for template IDs', async function (assert) {
|
||||
this.grantString = 'ids={{.User.Id}}';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_INVALID_ID_OR_TYPE).doesNotExist();
|
||||
assert
|
||||
.dom(GRANT_ACTIONS_NO_TYPE_DETECTED)
|
||||
.hasText('No resource type detected.');
|
||||
assert.dom(GRANT_ACTIONS_EMPTY_STATE).doesNotExist();
|
||||
});
|
||||
|
||||
test('it renders the invalid state for a pinned id paired with its own type instead of a child type', async function (assert) {
|
||||
this.grantString = 'ids=hcst_1234567890;type=host-catalog';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_NO_TYPE_DETECTED).doesNotExist();
|
||||
assert.dom(GRANT_ACTIONS_EMPTY_STATE).doesNotExist();
|
||||
assert
|
||||
.dom(GRANT_ACTIONS_INVALID_ID_OR_TYPE)
|
||||
.hasText('Invalid ID and type. No actions available.');
|
||||
});
|
||||
|
||||
test('it renders only CRUDL actions with fallback descriptions when type is wildcard', async function (assert) {
|
||||
this.grantString = 'ids=*;type=*';
|
||||
|
||||
await render(hbs`
|
||||
<GrantActions
|
||||
@grantsSchema={{this.grantsSchema}}
|
||||
@grantString={{this.grantString}}
|
||||
/>
|
||||
`);
|
||||
|
||||
assert.dom(GRANT_ACTIONS_TABLE).isVisible();
|
||||
|
||||
const rows = [...this.element.querySelectorAll(commonSelectors.TABLE_ROWS)];
|
||||
const rowNames = rows.map((row) =>
|
||||
row.querySelector('td')?.textContent.trim(),
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
rowNames,
|
||||
['create', 'delete', 'list', 'read', 'update'],
|
||||
'only CRUDL actions are shown for wildcard grants',
|
||||
);
|
||||
|
||||
const createRow = rows.find(
|
||||
(row) => row.querySelector('td')?.textContent.trim() === 'create',
|
||||
);
|
||||
assert.strictEqual(
|
||||
createRow.querySelectorAll('td')[1].textContent.trim(),
|
||||
'Creates resources',
|
||||
'description falls back to generic when no resource type is detected',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,13 @@ import { setupTest } from 'ember-qunit';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
import { setupMirage } from 'admin/tests/helpers/mirage';
|
||||
|
||||
import { createGrantCompletionSource } from 'admin/utils/grant-completions';
|
||||
import {
|
||||
createGrantCompletionSource,
|
||||
analyzeGrantString,
|
||||
getChildResourceActions,
|
||||
getCompatibleResourceTypeForIds,
|
||||
normalizeGrantsSchema,
|
||||
} from 'admin/utils/grant-completions';
|
||||
|
||||
const createCompletionContext = (lineText) => ({
|
||||
pos: lineText.length,
|
||||
@@ -74,6 +80,35 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
test.each(
|
||||
'field name suggestions',
|
||||
{
|
||||
'suggest all fields on an empty line': {
|
||||
lineText: '',
|
||||
expectedLabels: ['ids=', 'type=', 'actions=', 'output_fields='],
|
||||
},
|
||||
'filter already-used fields from suggestions': {
|
||||
lineText: 'ids=*;',
|
||||
expectedLabels: ['type=', 'actions=', 'output_fields='],
|
||||
},
|
||||
'match a partial field name': {
|
||||
lineText: 'ty',
|
||||
expectedLabels: ['type='],
|
||||
},
|
||||
'filter multiple already-used fields': {
|
||||
lineText: 'ids=*;type=scope;',
|
||||
expectedLabels: ['actions=', 'output_fields='],
|
||||
},
|
||||
},
|
||||
async function (assert, { lineText, expectedLabels }) {
|
||||
const completionResult = await this.grantCompletionSource(
|
||||
createCompletionContext(lineText),
|
||||
);
|
||||
|
||||
assert.deepEqual(getLabels(completionResult), expectedLabels);
|
||||
},
|
||||
);
|
||||
|
||||
test.each(
|
||||
'type suggestions',
|
||||
{
|
||||
@@ -358,6 +393,35 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
},
|
||||
);
|
||||
|
||||
test.each(
|
||||
'ids suggestions',
|
||||
{
|
||||
'include supported template values': {
|
||||
lineText: 'ids={{',
|
||||
expectedLabels: ['{{.User.Id}}', '{{.Account.Id}}'],
|
||||
},
|
||||
'suggest wildcard and templates on an empty ids value': {
|
||||
lineText: 'ids=',
|
||||
expectedLabels: ['*', '{{.User.Id}}', '{{.Account.Id}}'],
|
||||
},
|
||||
'filter wildcard once an id is already entered': {
|
||||
lineText: 'ids={{.User.Id}},',
|
||||
expectedLabels: ['{{.Account.Id}}'],
|
||||
},
|
||||
'filter already-entered template ids when completing a partial': {
|
||||
lineText: 'ids={{.User.Id}},{{',
|
||||
expectedLabels: ['{{.Account.Id}}'],
|
||||
},
|
||||
},
|
||||
async function (assert, { lineText, expectedLabels }) {
|
||||
const completionResult = await this.grantCompletionSource(
|
||||
createCompletionContext(lineText),
|
||||
);
|
||||
|
||||
assert.deepEqual(getLabels(completionResult), expectedLabels);
|
||||
},
|
||||
);
|
||||
|
||||
test('completion info labels are translated values', async function (assert) {
|
||||
const typeCompletionResult = await this.grantCompletionSource(
|
||||
createCompletionContext('type='),
|
||||
@@ -394,6 +458,240 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
);
|
||||
});
|
||||
|
||||
test('analyzeGrantString detects resource type and actions for a complete grant string', function (assert) {
|
||||
const result = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
'ids=hcst_1234567890;type=host-set;actions=read,list;output_fields=*',
|
||||
);
|
||||
|
||||
assert.strictEqual(result.detectedResourceType, 'host-set');
|
||||
assert.false(result.hasInvalidType);
|
||||
assert.false(result.hasInvalidIds);
|
||||
assert.true(result.actions.length > 0);
|
||||
});
|
||||
|
||||
test('analyzeGrantString returns no actions and no detected type for output_fields only', function (assert) {
|
||||
const result = analyzeGrantString(this.grantsSchemaData, 'output_fields=*');
|
||||
|
||||
assert.strictEqual(result.detectedResourceType, null);
|
||||
assert.deepEqual(result.actions, []);
|
||||
});
|
||||
|
||||
test('analyzeGrantString returns no actions and no detected type for an empty string', function (assert) {
|
||||
const result = analyzeGrantString(this.grantsSchemaData, '');
|
||||
|
||||
assert.strictEqual(result.detectedResourceType, null);
|
||||
assert.deepEqual(result.actions, []);
|
||||
});
|
||||
|
||||
test('getCompatibleResourceTypeForIds returns the matched type for a known id prefix', function (assert) {
|
||||
const schema = normalizeGrantsSchema(this.grantsSchemaData);
|
||||
|
||||
assert.strictEqual(
|
||||
getCompatibleResourceTypeForIds(schema, 'hs_123'),
|
||||
'host-set',
|
||||
);
|
||||
});
|
||||
|
||||
test('getCompatibleResourceTypeForIds returns null for an unknown id prefix', function (assert) {
|
||||
const schema = normalizeGrantsSchema(this.grantsSchemaData);
|
||||
|
||||
assert.strictEqual(
|
||||
getCompatibleResourceTypeForIds(schema, 'bad_123'),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('getCompatibleResourceTypeForIds returns null for mixed resource type ids', function (assert) {
|
||||
const schema = normalizeGrantsSchema(this.grantsSchemaData);
|
||||
|
||||
assert.strictEqual(
|
||||
getCompatibleResourceTypeForIds(schema, 'hs_123,s_123'),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('getCompatibleResourceTypeForIds returns the type when all ids share the same resource type', function (assert) {
|
||||
const schema = normalizeGrantsSchema(this.grantsSchemaData);
|
||||
|
||||
assert.strictEqual(
|
||||
getCompatibleResourceTypeForIds(schema, 'hs_123,hs_456'),
|
||||
'host-set',
|
||||
);
|
||||
});
|
||||
|
||||
test('analyzeGrantString.actions returns the same actions as the completion source uses', function (assert) {
|
||||
const { actions } = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
'ids=hcst_1234567890;type=host-set',
|
||||
);
|
||||
|
||||
assert.deepEqual(actions, [
|
||||
'add-hosts',
|
||||
'create',
|
||||
'delete',
|
||||
'list',
|
||||
'read',
|
||||
'remove-hosts',
|
||||
'set-hosts',
|
||||
'update',
|
||||
]);
|
||||
});
|
||||
|
||||
test('analyzeGrantString.detectedResourceType returns the type inferred from a specific id', function (assert) {
|
||||
const { detectedResourceType } = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
'ids=hs_123',
|
||||
);
|
||||
|
||||
assert.strictEqual(detectedResourceType, 'host-set');
|
||||
});
|
||||
|
||||
test('analyzeGrantString.detectedResourceType returns an explicit type value', function (assert) {
|
||||
const { detectedResourceType } = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
'type=session',
|
||||
);
|
||||
|
||||
assert.strictEqual(detectedResourceType, 'session');
|
||||
});
|
||||
|
||||
test('analyzeGrantString.detectedResourceType returns null when no type can be detected', function (assert) {
|
||||
const { detectedResourceType } = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
'output_fields=*',
|
||||
);
|
||||
|
||||
assert.strictEqual(detectedResourceType, null);
|
||||
});
|
||||
|
||||
test('analyzeGrantString.detectedResourceType returns null for a wildcard type', function (assert) {
|
||||
const { detectedResourceType } = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
'type=*',
|
||||
);
|
||||
|
||||
assert.strictEqual(detectedResourceType, null);
|
||||
});
|
||||
|
||||
test('analyzeGrantString treats a type with additional equals signs as invalid', function (assert) {
|
||||
const result = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
'ids=s_123;type=session=a;actions=read',
|
||||
);
|
||||
|
||||
assert.true(result.hasInvalidType);
|
||||
});
|
||||
|
||||
test('getChildResourceActions returns the union of child resource actions for a pinned parent id', function (assert) {
|
||||
const schema = normalizeGrantsSchema(this.grantsSchemaData);
|
||||
|
||||
assert.deepEqual(getChildResourceActions(schema, 'hcst_1234567890'), [
|
||||
'create',
|
||||
'list',
|
||||
'read',
|
||||
'update',
|
||||
'delete',
|
||||
'add-hosts',
|
||||
'set-hosts',
|
||||
'remove-hosts',
|
||||
]);
|
||||
});
|
||||
|
||||
test.each(
|
||||
'analyzeGrantString.hasInvalidType',
|
||||
{
|
||||
'returns true for an unknown type': {
|
||||
grantString: 'type=not-a-real-type',
|
||||
expected: true,
|
||||
},
|
||||
'returns false for a known type': {
|
||||
grantString: 'type=host-set',
|
||||
expected: false,
|
||||
},
|
||||
'returns false for a wildcard type': {
|
||||
grantString: 'type=*',
|
||||
expected: false,
|
||||
},
|
||||
'returns false when there is no type': {
|
||||
grantString: 'ids=hs_123',
|
||||
expected: false,
|
||||
},
|
||||
},
|
||||
function (assert, { grantString, expected }) {
|
||||
const { hasInvalidType } = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
grantString,
|
||||
);
|
||||
assert.strictEqual(hasInvalidType, expected);
|
||||
},
|
||||
);
|
||||
|
||||
test.each(
|
||||
'analyzeGrantString.hasInvalidIds',
|
||||
{
|
||||
'returns true for an unrecognised id prefix': {
|
||||
grantString: 'ids=bad_123',
|
||||
expected: true,
|
||||
},
|
||||
'returns true for mixed ids with conflicting resource types': {
|
||||
grantString: 'ids=hs_123,s_123',
|
||||
expected: true,
|
||||
},
|
||||
'returns false for a recognised id prefix': {
|
||||
grantString: 'ids=hs_123',
|
||||
expected: false,
|
||||
},
|
||||
'returns false for a wildcard id': {
|
||||
grantString: 'ids=*',
|
||||
expected: false,
|
||||
},
|
||||
'returns false for a template id': {
|
||||
grantString: 'ids={{.User.Id}}',
|
||||
expected: false,
|
||||
},
|
||||
},
|
||||
function (assert, { grantString, expected }) {
|
||||
const { hasInvalidIds } = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
grantString,
|
||||
);
|
||||
assert.strictEqual(hasInvalidIds, expected);
|
||||
},
|
||||
);
|
||||
|
||||
test.each(
|
||||
'analyzeGrantString.hasInvalidPinnedIdTypeCombination',
|
||||
{
|
||||
'returns true when a pinned id is paired with its own type': {
|
||||
grantString: 'ids=hcst_1234567890;type=host-catalog',
|
||||
expected: true,
|
||||
},
|
||||
'returns false when a pinned id is paired with a valid child type': {
|
||||
grantString: 'ids=hcst_1234567890;type=host-set',
|
||||
expected: false,
|
||||
},
|
||||
'returns false for wildcard ids with a specific type': {
|
||||
grantString: 'ids=*;type=host-set',
|
||||
expected: false,
|
||||
},
|
||||
'returns false when there is no type': {
|
||||
grantString: 'ids=hcst_1234567890',
|
||||
expected: false,
|
||||
},
|
||||
'returns false when ids are invalid': {
|
||||
grantString: 'ids=bad_123;type=host-set',
|
||||
expected: false,
|
||||
},
|
||||
},
|
||||
function (assert, { grantString, expected }) {
|
||||
const { hasInvalidPinnedIdTypeCombination } = analyzeGrantString(
|
||||
this.grantsSchemaData,
|
||||
grantString,
|
||||
);
|
||||
assert.strictEqual(hasInvalidPinnedIdTypeCombination, expected);
|
||||
},
|
||||
);
|
||||
module('id auto completion', function () {
|
||||
test('idLookup results are included alongside static options', async function (assert) {
|
||||
const source = createGrantCompletionSource(
|
||||
|
||||
Reference in new issue
Block a user