1106 lines
34 KiB
JavaScript
1106 lines
34 KiB
JavaScript
/**
|
|
* Copyright IBM Corp. 2021, 2026
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import { module, test } from 'qunit';
|
|
import { setupTest } from 'ember-qunit';
|
|
import { setupIntl } from 'ember-intl/test-support';
|
|
import { setupMirage } from 'admin/tests/helpers/mirage';
|
|
|
|
import {
|
|
createGrantCompletionSource,
|
|
analyzeGrantString,
|
|
} from 'admin/utils/grant-completions';
|
|
|
|
const createCompletionContext = (lineText) => ({
|
|
pos: lineText.length,
|
|
state: {
|
|
doc: {
|
|
lineAt() {
|
|
return {
|
|
text: lineText,
|
|
from: 0,
|
|
};
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const getLabels = (completionResult) =>
|
|
completionResult.options.map(({ label }) => label);
|
|
const getInfos = (completionResult) =>
|
|
completionResult.options.map(({ info }) => info ?? '');
|
|
|
|
let allActionLabels;
|
|
|
|
module('Unit | Utils | grant-completions', function (hooks) {
|
|
setupTest(hooks);
|
|
setupMirage(hooks);
|
|
setupIntl(hooks, 'en-us');
|
|
|
|
hooks.beforeEach(async function () {
|
|
this.intl = this.owner.lookup('service:intl');
|
|
|
|
this.translate = (key) =>
|
|
this.intl.t(`resources.role.edit-grants.completion-info.${key}`);
|
|
|
|
this.idLookup = async () => [];
|
|
this.getIsLoading = () => false;
|
|
|
|
const response = await fetch('/grants-schema.json');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch grants schema: ${response.status}`);
|
|
}
|
|
|
|
const grantsSchemaData = await response.json();
|
|
this.grantsSchemaData = grantsSchemaData;
|
|
|
|
allActionLabels = grantsSchemaData.resource_types.reduce(
|
|
(labels, resource) => {
|
|
const resourceLabels = [
|
|
...(resource.collection_actions ?? []),
|
|
...(resource.id_actions ?? []),
|
|
];
|
|
return [...new Set([...labels, ...resourceLabels])];
|
|
},
|
|
[],
|
|
);
|
|
allActionLabels = ['*', ...allActionLabels];
|
|
|
|
this.grantCompletionSource = createGrantCompletionSource(
|
|
grantsSchemaData,
|
|
this.translate,
|
|
this.idLookup,
|
|
this.getIsLoading,
|
|
);
|
|
});
|
|
|
|
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',
|
|
{
|
|
'without ids only include top-level collection resource types': {
|
|
lineText: 'type=',
|
|
expectedLabels: [
|
|
'*',
|
|
'auth-method',
|
|
'session',
|
|
'credential-store',
|
|
'alias',
|
|
'user',
|
|
'auth-token',
|
|
'target',
|
|
'worker',
|
|
'group',
|
|
'role',
|
|
'host-catalog',
|
|
'storage-bucket',
|
|
'policy',
|
|
'scope',
|
|
'session-recording',
|
|
],
|
|
},
|
|
'with specific ids only include child resource types': {
|
|
lineText: 'ids=hcst_1234567890;type=',
|
|
expectedLabels: ['*', 'host', 'host-set'],
|
|
},
|
|
'with a partial id prefix include child resource types for the matched parent':
|
|
{
|
|
lineText: 'ids=hcs;type=',
|
|
expectedLabels: ['*', 'host', 'host-set'],
|
|
},
|
|
'with specific ids and no child resources show a no suggestions placeholder':
|
|
{
|
|
lineText: 'ids=hs_123;type=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'with mixed resource ids show a no suggestions placeholder': {
|
|
lineText: 'ids=hs_123,s_123;type=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'with specific ids and a partial type with no child resources show a no suggestions placeholder':
|
|
{
|
|
lineText: 'ids=hs_123;type=h',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'{{.User.Id}} template has no child resource types so shows no suggestions':
|
|
{
|
|
lineText: 'ids={{.User.Id}};type=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'{{.Account.Id}} template has no child resource types so shows no suggestions':
|
|
{
|
|
lineText: 'ids={{.Account.Id}};type=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'{{user.id}} template has no child resource types so shows no suggestions':
|
|
{
|
|
lineText: 'ids={{user.id}};type=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'{{account.id}} template has no child resource types so shows no suggestions':
|
|
{
|
|
lineText: 'ids={{account.id}};type=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
},
|
|
async function (assert, { lineText, expectedLabels }) {
|
|
const completionResult = await this.grantCompletionSource(
|
|
createCompletionContext(lineText),
|
|
);
|
|
const resolvedLabels = expectedLabels.map((label) =>
|
|
label === 'NO_SUGGESTIONS' ? this.translate('no-suggestions') : label,
|
|
);
|
|
|
|
assert.deepEqual(
|
|
[...getLabels(completionResult)].sort(),
|
|
[...resolvedLabels].sort(),
|
|
);
|
|
},
|
|
);
|
|
|
|
test.each(
|
|
'action suggestions',
|
|
{
|
|
'type-only grants only suggest collection actions for top-level resource types':
|
|
{
|
|
lineText: 'type=scope;actions=',
|
|
expectedLabels: [
|
|
'destroy-key-version',
|
|
'create',
|
|
'list',
|
|
'list-keys',
|
|
'rotate-keys',
|
|
'list-key-version-destruction-jobs',
|
|
],
|
|
},
|
|
'type-only grants suggest nothing for child resource types': {
|
|
lineText: 'type=host;actions=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'specific ids without a type only suggest id actions for the resource type':
|
|
{
|
|
lineText: 'ids=hs_123;actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'delete',
|
|
'add-hosts',
|
|
'set-hosts',
|
|
'remove-hosts',
|
|
'read',
|
|
'update',
|
|
],
|
|
},
|
|
'comma-separated ids of the same resource type suggest that type id actions':
|
|
{
|
|
lineText: 'ids=hs_123,hs_456;actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'delete',
|
|
'add-hosts',
|
|
'set-hosts',
|
|
'remove-hosts',
|
|
'read',
|
|
'update',
|
|
],
|
|
},
|
|
'ids with a matching type show a no suggestions placeholder': {
|
|
lineText: 'ids=s_123;type=session;actions=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'ids that conflict with the selected type show a no suggestions placeholder':
|
|
{
|
|
lineText: 'ids=hs_123;type=session;actions=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'comma-separated ids with conflicting resource types show a no suggestions placeholder':
|
|
{
|
|
lineText: 'ids=hs_123,s_123;actions=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'pinned ids with a specific child resource type suggest that type actions':
|
|
{
|
|
lineText: 'ids=hcst_1234567890;type=host-set;actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'create',
|
|
'list',
|
|
'delete',
|
|
'add-hosts',
|
|
'set-hosts',
|
|
'remove-hosts',
|
|
'read',
|
|
'update',
|
|
],
|
|
},
|
|
'wildcard ids with a specific child resource type suggest that type actions':
|
|
{
|
|
lineText: 'ids=*;type=host-set;actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'create',
|
|
'list',
|
|
'delete',
|
|
'add-hosts',
|
|
'set-hosts',
|
|
'remove-hosts',
|
|
'read',
|
|
'update',
|
|
],
|
|
},
|
|
'ids that are a top level resource that has child resources with a wildcard type suggest the union of child resource actions':
|
|
{
|
|
lineText: 'ids=hcst_1234567890;type=*;actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'create',
|
|
'list',
|
|
'read',
|
|
'update',
|
|
'delete',
|
|
'add-hosts',
|
|
'set-hosts',
|
|
'remove-hosts',
|
|
],
|
|
},
|
|
'a partial parent id prefix with a wildcard type suggests child resource actions':
|
|
{
|
|
lineText: 'ids=hcs;type=*;actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'create',
|
|
'list',
|
|
'read',
|
|
'update',
|
|
'delete',
|
|
'add-hosts',
|
|
'set-hosts',
|
|
'remove-hosts',
|
|
],
|
|
},
|
|
'a partial parent id prefix with the same type shows a no suggestions placeholder':
|
|
{
|
|
lineText: 'ids=hcs;type=host-catalog;actions=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'ids from different parent resource types with a wildcard type show a no suggestions placeholder':
|
|
{
|
|
lineText: 'ids=hcst_1234567890,o_123;type=*;actions=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'wildcard ids without a type suggest all actions': {
|
|
lineText: 'ids=*;actions=',
|
|
expectedLabels: () => allActionLabels,
|
|
},
|
|
'wildcard ids with a wildcard type suggest all actions': {
|
|
lineText: 'ids=*;type=*;actions=',
|
|
expectedLabels: () => allActionLabels,
|
|
},
|
|
'{{.User.Id}} template without a type suggests user id actions': {
|
|
lineText: 'ids={{.User.Id}};actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'list-resolvable-aliases',
|
|
'read',
|
|
'update',
|
|
'delete',
|
|
'add-accounts',
|
|
'set-accounts',
|
|
'remove-accounts',
|
|
],
|
|
},
|
|
'{{.Account.Id}} template without a type suggests account id actions': {
|
|
lineText: 'ids={{.Account.Id}};actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'set-password',
|
|
'change-password',
|
|
'read',
|
|
'update',
|
|
'delete',
|
|
],
|
|
},
|
|
'{{user.id}} template without a type suggests user id actions': {
|
|
lineText: 'ids={{user.id}};actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'list-resolvable-aliases',
|
|
'read',
|
|
'update',
|
|
'delete',
|
|
'add-accounts',
|
|
'set-accounts',
|
|
'remove-accounts',
|
|
],
|
|
},
|
|
'{{account.id}} template without a type suggests account id actions': {
|
|
lineText: 'ids={{account.id}};actions=',
|
|
expectedLabels: [
|
|
'*',
|
|
'set-password',
|
|
'change-password',
|
|
'read',
|
|
'update',
|
|
'delete',
|
|
],
|
|
},
|
|
'mixed user and account templates show a no suggestions placeholder': {
|
|
lineText: 'ids={{.User.Id}},{{.Account.Id}};actions=',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
},
|
|
async function (assert, { lineText, expectedLabels }) {
|
|
const completionResult = await this.grantCompletionSource(
|
|
createCompletionContext(lineText),
|
|
);
|
|
const resolvedLabels = (
|
|
typeof expectedLabels === 'function' ? expectedLabels() : expectedLabels
|
|
).map((label) =>
|
|
label === 'NO_SUGGESTIONS' ? this.translate('no-suggestions') : label,
|
|
);
|
|
|
|
assert.deepEqual(
|
|
[...getLabels(completionResult)].sort(),
|
|
[...resolvedLabels].sort(),
|
|
);
|
|
},
|
|
);
|
|
|
|
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 out wildcard and templates once an id is already entered': {
|
|
lineText: 'ids=h_123,',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
'filter out wildcard and templates once a template is already entered': {
|
|
lineText: 'ids={{.User.Id}},',
|
|
expectedLabels: ['NO_SUGGESTIONS'],
|
|
},
|
|
},
|
|
async function (assert, { lineText, expectedLabels }) {
|
|
const completionResult = await this.grantCompletionSource(
|
|
createCompletionContext(lineText),
|
|
);
|
|
const resolvedLabels = expectedLabels.map((label) =>
|
|
label === 'NO_SUGGESTIONS' ? this.translate('no-suggestions') : label,
|
|
);
|
|
|
|
assert.deepEqual(getLabels(completionResult), resolvedLabels);
|
|
},
|
|
);
|
|
|
|
test.each(
|
|
'output_fields suggestions',
|
|
{
|
|
'with a specific type suggests wildcard and that type output fields': {
|
|
lineText: 'type=target;output_fields=',
|
|
getExpected: (schema) => {
|
|
const { output_fields } = schema.resource_types.find(
|
|
(r) => r.type === 'target',
|
|
);
|
|
return ['*', ...output_fields];
|
|
},
|
|
},
|
|
'with specific ids infers the type and suggests its output fields': {
|
|
lineText: 'ids=ttcp_123;output_fields=',
|
|
getExpected: (schema) => {
|
|
const { output_fields } = schema.resource_types.find(
|
|
(r) => r.type === 'target',
|
|
);
|
|
return ['*', ...output_fields];
|
|
},
|
|
},
|
|
'with a user template id suggests user output fields': {
|
|
lineText: 'ids={{.User.Id}};output_fields=',
|
|
getExpected: (schema) => {
|
|
const { output_fields } = schema.resource_types.find(
|
|
(r) => r.type === 'user',
|
|
);
|
|
return ['*', ...output_fields];
|
|
},
|
|
},
|
|
'with the user.id template suggests user output fields': {
|
|
lineText: 'ids={{user.id}};output_fields=',
|
|
getExpected: (schema) => {
|
|
const { output_fields } = schema.resource_types.find(
|
|
(r) => r.type === 'user',
|
|
);
|
|
return ['*', ...output_fields];
|
|
},
|
|
},
|
|
'with an account template id suggests account output fields': {
|
|
lineText: 'ids={{.Account.Id}};output_fields=',
|
|
getExpected: (schema) => {
|
|
const { output_fields } = schema.resource_types.find(
|
|
(r) => r.type === 'account',
|
|
);
|
|
return ['*', ...output_fields];
|
|
},
|
|
},
|
|
'with the account.id template suggests account output fields': {
|
|
lineText: 'ids={{account.id}};output_fields=',
|
|
getExpected: (schema) => {
|
|
const { output_fields } = schema.resource_types.find(
|
|
(r) => r.type === 'account',
|
|
);
|
|
return ['*', ...output_fields];
|
|
},
|
|
},
|
|
'mixed user and account template ids suggests only wildcard': {
|
|
lineText: 'ids={{.User.Id}},{{.Account.Id}};output_fields=',
|
|
getExpected: () => ['*'],
|
|
},
|
|
'pinned parent id with wildcard type suggests union of child output fields':
|
|
{
|
|
lineText: 'ids=hcst_123;type=*;output_fields=',
|
|
getExpected: (schema) => {
|
|
const childFields = schema.resource_types
|
|
.filter((r) => r.parent_type === 'host-catalog')
|
|
.flatMap((r) => r.output_fields);
|
|
return ['*', ...new Set(childFields)];
|
|
},
|
|
},
|
|
'excludes already entered output fields': {
|
|
lineText: 'type=target;output_fields=id,',
|
|
getExpected: (schema) => {
|
|
const { output_fields } = schema.resource_types.find(
|
|
(r) => r.type === 'target',
|
|
);
|
|
return output_fields.filter((f) => f !== 'id');
|
|
},
|
|
},
|
|
'partial prefix filters output fields': {
|
|
lineText: 'type=target;output_fields=sc',
|
|
getExpected: (schema) => {
|
|
const { output_fields } = schema.resource_types.find(
|
|
(r) => r.type === 'target',
|
|
);
|
|
return output_fields.filter((f) => f.startsWith('sc'));
|
|
},
|
|
},
|
|
'without type or ids suggests only wildcard': {
|
|
lineText: 'output_fields=',
|
|
getExpected: () => ['*'],
|
|
},
|
|
},
|
|
async function (assert, { lineText, getExpected }) {
|
|
const completionResult = await this.grantCompletionSource(
|
|
createCompletionContext(lineText),
|
|
);
|
|
const labels = getLabels(completionResult);
|
|
const expected = getExpected(this.grantsSchemaData);
|
|
|
|
assert.deepEqual([...labels].sort(), [...expected].sort());
|
|
},
|
|
);
|
|
|
|
test('completion info labels are translated values', async function (assert) {
|
|
const typeCompletionResult = await this.grantCompletionSource(
|
|
createCompletionContext('type='),
|
|
);
|
|
const idsCompletionResult = await this.grantCompletionSource(
|
|
createCompletionContext('ids='),
|
|
);
|
|
const outputFieldsCompletionResult = await this.grantCompletionSource(
|
|
createCompletionContext('output_fields='),
|
|
);
|
|
const actionsCompletionResult = await this.grantCompletionSource(
|
|
createCompletionContext('actions='),
|
|
);
|
|
|
|
assert.strictEqual(
|
|
typeCompletionResult.options[0].info,
|
|
this.translate('wildcard-types'),
|
|
);
|
|
|
|
assert.deepEqual(getInfos(idsCompletionResult), [
|
|
this.translate('wildcard-ids'),
|
|
this.translate('template-value'),
|
|
this.translate('template-value'),
|
|
]);
|
|
|
|
assert.strictEqual(
|
|
outputFieldsCompletionResult.options[0].info,
|
|
this.translate('all-fields'),
|
|
);
|
|
|
|
assert.strictEqual(
|
|
actionsCompletionResult.options[0].info,
|
|
this.translate('wildcard-actions'),
|
|
);
|
|
});
|
|
|
|
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('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.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,
|
|
},
|
|
'returns false for multiple ids sharing the same resource type': {
|
|
grantString: 'ids=hs_123,hs_456',
|
|
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(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [
|
|
{ id: 'ttcp_abc123', name: 'My Target' },
|
|
{ id: 'ttcp_def456', name: 'Another Target' },
|
|
],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids='));
|
|
|
|
const labels = getLabels(result);
|
|
assert.true(labels.includes('*'));
|
|
assert.true(labels.includes('{{.User.Id}}'));
|
|
assert.true(labels.includes('{{.Account.Id}}'));
|
|
assert.true(labels.includes('ttcp_abc123'));
|
|
assert.true(labels.includes('ttcp_def456'));
|
|
});
|
|
|
|
test('idLookup results have detail set to the resource name', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [{ id: 'ttcp_abc123', name: 'My Target' }],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids='));
|
|
|
|
const idOption = result.options.find((o) => o.label === 'ttcp_abc123');
|
|
assert.ok(idOption, 'found the ID option');
|
|
assert.strictEqual(idOption.detail, 'My Target');
|
|
});
|
|
|
|
test('static options appear before idLookup results', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [{ id: 'ttcp_abc123', name: 'My Target' }],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids='));
|
|
const labels = getLabels(result);
|
|
|
|
assert.true(
|
|
labels.indexOf('*') < labels.indexOf('ttcp_abc123'),
|
|
'wildcard appears before DB results',
|
|
);
|
|
});
|
|
|
|
test('loading placeholder is appended when getIsLoading returns true', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [{ id: 'ttcp_abc123', name: 'My Target' }],
|
|
() => true,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids='));
|
|
const labels = getLabels(result);
|
|
|
|
assert.true(labels.includes(this.translate('loading-ids')));
|
|
assert.strictEqual(
|
|
labels.at(-1),
|
|
this.translate('loading-ids'),
|
|
'loading placeholder is the last option',
|
|
);
|
|
});
|
|
|
|
test('loading placeholder is NOT present when getIsLoading returns false', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids='));
|
|
const labels = getLabels(result);
|
|
|
|
assert.false(labels.includes(this.translate('loading-ids')));
|
|
});
|
|
|
|
test('wildcard and templates are hidden when any IDs are already entered', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids=ttcp_abc123,'));
|
|
const labels = getLabels(result);
|
|
|
|
assert.false(labels.includes('*'));
|
|
assert.false(labels.includes('{{.User.Id}}'));
|
|
assert.false(labels.includes('{{.Account.Id}}'));
|
|
});
|
|
|
|
test('templates are hidden when a type field is present, even with no IDs entered', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('type=user;ids='));
|
|
const labels = getLabels(result);
|
|
|
|
assert.false(labels.includes('{{.User.Id}}'));
|
|
assert.false(labels.includes('{{.Account.Id}}'));
|
|
});
|
|
|
|
test('wildcard is still suggested when a type field is present and no IDs are entered', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('type=user;ids='));
|
|
const labels = getLabels(result);
|
|
|
|
assert.true(labels.includes('*'));
|
|
});
|
|
|
|
test('templates are suggested when no type field and no IDs are entered', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids='));
|
|
const labels = getLabels(result);
|
|
|
|
assert.true(labels.includes('{{.User.Id}}'));
|
|
assert.true(labels.includes('{{.Account.Id}}'));
|
|
});
|
|
|
|
test('when type=* idLookup is called with all parent resource types', async function (assert) {
|
|
assert.expect(1);
|
|
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async (partial, compatibleResourceTypes) => {
|
|
assert.deepEqual(compatibleResourceTypes, [
|
|
'credential-store',
|
|
'auth-method',
|
|
'host-catalog',
|
|
]);
|
|
return [];
|
|
},
|
|
() => false,
|
|
);
|
|
|
|
await source(createCompletionContext('type=*;ids='));
|
|
});
|
|
|
|
test('when type is a child type idLookup is called with the parent type and its results are included', async function (assert) {
|
|
assert.expect(2);
|
|
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async (partial, compatibleResourceTypes) => {
|
|
assert.deepEqual(compatibleResourceTypes, ['host-catalog']);
|
|
return [{ id: 'hcst_parentid', name: 'My Host Catalog' }];
|
|
},
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(
|
|
createCompletionContext('type=host-set;ids='),
|
|
);
|
|
|
|
assert.true(getLabels(result).includes('hcst_parentid'));
|
|
});
|
|
|
|
test('no ID suggestions are shown when a template id is already entered', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [{ id: 'u_abc123', name: 'My User' }],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids={{.User.Id}},'));
|
|
|
|
assert.deepEqual(getLabels(result), [this.translate('no-suggestions')]);
|
|
});
|
|
|
|
test('already-entered IDs are not offered again', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids={{.User.Id}},'));
|
|
const labels = getLabels(result);
|
|
|
|
assert.false(labels.includes('{{.User.Id}}'));
|
|
});
|
|
|
|
test('empty idLookup result still returns static options', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids='));
|
|
|
|
assert.deepEqual(getLabels(result), [
|
|
'*',
|
|
'{{.User.Id}}',
|
|
'{{.Account.Id}}',
|
|
]);
|
|
});
|
|
|
|
test('partial prefix filters static options correctly', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids={{'));
|
|
|
|
const labels = getLabels(result);
|
|
assert.false(labels.includes('*'), 'wildcard filtered out by prefix');
|
|
assert.deepEqual(labels, ['{{.User.Id}}', '{{.Account.Id}}']);
|
|
});
|
|
|
|
test('idLookup is called when partial contains uppercase letters and spaces, enabling name-based search', async function (assert) {
|
|
assert.expect(2);
|
|
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async (partial) => {
|
|
assert.strictEqual(partial, 'My Target');
|
|
return [{ id: 'ttcp_abc123', name: 'My Target' }];
|
|
},
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids=My Target'));
|
|
const labels = getLabels(result);
|
|
|
|
assert.true(labels.includes('ttcp_abc123'));
|
|
});
|
|
|
|
test('idLookup results with undefined name still work (detail is undefined)', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [{ id: 'ttcp_abc123', name: undefined }],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids='));
|
|
const idOption = result.options.find((o) => o.label === 'ttcp_abc123');
|
|
|
|
assert.ok(idOption);
|
|
assert.strictEqual(idOption.detail, undefined);
|
|
});
|
|
|
|
test('idLookup is called with compatibleResourceTypes when entered IDs all share a single resource type', async function (assert) {
|
|
assert.expect(1);
|
|
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async (partial, compatibleResourceTypes) => {
|
|
assert.deepEqual(compatibleResourceTypes, ['target']);
|
|
return [];
|
|
},
|
|
() => false,
|
|
);
|
|
|
|
await source(createCompletionContext('ids=ttcp_abc123,'));
|
|
});
|
|
|
|
test('no suggestions shown when entered IDs have mixed resource types', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(
|
|
createCompletionContext('ids=ttcp_abc123,hs_456,'),
|
|
);
|
|
|
|
assert.deepEqual(getLabels(result), [this.translate('no-suggestions')]);
|
|
});
|
|
|
|
test('idLookup results matching already-entered IDs are excluded', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [
|
|
{ id: 'ttcp_abc123', name: 'Already Entered' },
|
|
{ id: 'ttcp_def456', name: 'New Target' },
|
|
],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids=ttcp_abc123,'));
|
|
const labels = getLabels(result);
|
|
|
|
assert.false(
|
|
labels.includes('ttcp_abc123'),
|
|
'already-entered ID excluded',
|
|
);
|
|
assert.true(labels.includes('ttcp_def456'), 'new ID included');
|
|
});
|
|
|
|
test('no suggestions shown when idLookup returns empty results with entered IDs', async function (assert) {
|
|
const source = createGrantCompletionSource(
|
|
this.grantsSchemaData,
|
|
this.translate,
|
|
async () => [],
|
|
() => false,
|
|
);
|
|
|
|
const result = await source(createCompletionContext('ids=ttcp_abc123,'));
|
|
|
|
assert.deepEqual(getLabels(result), [this.translate('no-suggestions')]);
|
|
});
|
|
});
|
|
});
|