feat: 🎸 Add support for output fields (#3268)
* feat: 🎸 Add support for output fields * refactor: 💡 PR feedback
This commit is contained in:
1 parent
e0b80b1176
commit
7fa59bfd5d
6 files changed
+970
-254
No files matched your search
@@ -11,9 +11,6 @@ import { setupMirage } from 'admin/tests/helpers/mirage';
|
||||
import {
|
||||
createGrantCompletionSource,
|
||||
analyzeGrantString,
|
||||
getChildResourceActions,
|
||||
getCompatibleResourceTypeForIds,
|
||||
normalizeGrantsSchema,
|
||||
} from 'admin/utils/grant-completions';
|
||||
|
||||
const createCompletionContext = (lineText) => ({
|
||||
@@ -185,7 +182,10 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
label === 'NO_SUGGESTIONS' ? this.translate('no-suggestions') : label,
|
||||
);
|
||||
|
||||
assert.deepEqual(getLabels(completionResult), resolvedLabels);
|
||||
assert.deepEqual(
|
||||
[...getLabels(completionResult)].sort(),
|
||||
[...resolvedLabels].sort(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -389,7 +389,10 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
label === 'NO_SUGGESTIONS' ? this.translate('no-suggestions') : label,
|
||||
);
|
||||
|
||||
assert.deepEqual(getLabels(completionResult), resolvedLabels);
|
||||
assert.deepEqual(
|
||||
[...getLabels(completionResult)].sort(),
|
||||
[...resolvedLabels].sort(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -404,21 +407,129 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
lineText: 'ids=',
|
||||
expectedLabels: ['*', '{{.User.Id}}', '{{.Account.Id}}'],
|
||||
},
|
||||
'filter wildcard once an id is already entered': {
|
||||
lineText: 'ids={{.User.Id}},',
|
||||
expectedLabels: ['{{.Account.Id}}'],
|
||||
'filter out wildcard and templates once an id is already entered': {
|
||||
lineText: 'ids=h_123,',
|
||||
expectedLabels: ['NO_SUGGESTIONS'],
|
||||
},
|
||||
'filter already-entered template ids when completing a partial': {
|
||||
lineText: 'ids={{.User.Id}},{{',
|
||||
expectedLabels: ['{{.Account.Id}}'],
|
||||
'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), expectedLabels);
|
||||
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());
|
||||
},
|
||||
);
|
||||
|
||||
@@ -484,42 +595,6 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
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,
|
||||
@@ -583,21 +658,6 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
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',
|
||||
{
|
||||
@@ -650,6 +710,10 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
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(
|
||||
@@ -692,6 +756,7 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
assert.strictEqual(hasInvalidPinnedIdTypeCombination, expected);
|
||||
},
|
||||
);
|
||||
|
||||
module('id auto completion', function () {
|
||||
test('idLookup results are included alongside static options', async function (assert) {
|
||||
const source = createGrantCompletionSource(
|
||||
@@ -847,9 +912,9 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
this.translate,
|
||||
async (partial, compatibleResourceTypes) => {
|
||||
assert.deepEqual(compatibleResourceTypes, [
|
||||
'credential-store',
|
||||
'auth-method',
|
||||
'host-catalog',
|
||||
'credential-store',
|
||||
]);
|
||||
return [];
|
||||
},
|
||||
@@ -879,6 +944,19 @@ module('Unit | Utils | grant-completions', function (hooks) {
|
||||
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,
|
||||
|
||||
@@ -222,7 +222,10 @@ module('Unit | Utility | grant-linter', function (hooks) {
|
||||
['ids=*;type=*;actions=read'], // valid wildcard id
|
||||
['ids=hcst_123,hcplg_456;type=host-set;actions=read'], // valid pinned ids for host-set resource type
|
||||
['ids={{.Account.Id}};actions=read'], // valid template id when type is not specified
|
||||
['ids={{.Account.Id}},{{.User.Id}};actions=read'], // valid template ids when type is not specified
|
||||
[
|
||||
'ids={{.Account.Id}},{{.User.Id}};actions=read',
|
||||
'Template ids cannot be combined with each other',
|
||||
], // mixed template ids is not allowed
|
||||
['ids=csst_1234,csvlt_5678;actions=*'], // valid id prefixes with same type
|
||||
['ids=amldap_123,amoidc_456;type=*;actions=read,list'], // valid ids with wildcard type
|
||||
],
|
||||
@@ -236,4 +239,72 @@ module('Unit | Utility | grant-linter', function (hooks) {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each(
|
||||
'diagnostics for output_fields field values',
|
||||
[
|
||||
['type=target;output_fields=,', 'Invalid syntax'],
|
||||
[
|
||||
'type=target;output_fields=*,id',
|
||||
'Wildcard output field "*" cannot be combined with other fields',
|
||||
],
|
||||
['type=target;output_fields=id,id', 'Duplicate output field "id"'],
|
||||
[
|
||||
'type=target;output_fields=invalid_field',
|
||||
'Invalid output field "invalid_field" for type "target"',
|
||||
],
|
||||
[
|
||||
'ids=ttcp_123;output_fields=member_ids',
|
||||
'Invalid output field "member_ids" for type "target"',
|
||||
],
|
||||
['ids=hcst_123;type=*;actions=read;output_fields=host_catalog_id'], // valid: child type (host) output field
|
||||
['ids=hcst_123;type=*;actions=read;output_fields=host_set_ids'], // valid: child type (host) output field
|
||||
[
|
||||
'ids=hcst_123;type=*;actions=read;output_fields=session_max_seconds',
|
||||
'Invalid output field "session_max_seconds"',
|
||||
], // invalid: not a host or host-set field
|
||||
[
|
||||
'ids=*;type=*;actions=read;output_fields=id',
|
||||
'Invalid output field "id"',
|
||||
], // no specific type context, specific field is invalid
|
||||
['type=target;output_fields=id,name'], // valid output fields
|
||||
['type=target;output_fields=*'], // valid wildcard
|
||||
['ids=ttcp_123;output_fields=scope_id'], // valid inferred type
|
||||
['ids={{.User.Id}};output_fields=email'], // valid user template output field
|
||||
['ids={{user.id}};output_fields=email'], // valid user template alias output field
|
||||
['ids={{.Account.Id}};output_fields=auth_method_id'], // valid account template output field
|
||||
['ids={{account.id}};output_fields=auth_method_id'], // valid account template alias output field
|
||||
[
|
||||
'ids={{.User.Id}};output_fields=auth_method_id',
|
||||
'Invalid output field "auth_method_id" for type "user"',
|
||||
], // account field is invalid for user template
|
||||
[
|
||||
'ids={{.Account.Id}};output_fields=email',
|
||||
'Invalid output field "email" for type "account"',
|
||||
], // user field is invalid for account template
|
||||
[
|
||||
'ids={{.User.Id}},{{.Account.Id}};output_fields=email',
|
||||
'Template ids cannot be combined with each other',
|
||||
], // ids error fires first; mixed templates are disallowed
|
||||
[
|
||||
'ids={{.User.Id}},{{.Account.Id}};output_fields=auth_method_id',
|
||||
'Template ids cannot be combined with each other',
|
||||
], // ids error fires first; mixed templates are disallowed
|
||||
[
|
||||
'ids={{.User.Id}},{{.Account.Id}};output_fields=session_max_seconds',
|
||||
'Template ids cannot be combined with each other',
|
||||
], // ids error fires first; mixed templates are disallowed
|
||||
['type=target;output_fields=id,scope_id,name'], // valid multiple output fields
|
||||
['ids=*;type=*;actions=read;output_fields=*'], // wildcard output fields with wildcard type is valid
|
||||
],
|
||||
function (assert, [grantString, errorMsg]) {
|
||||
const diagnostics = this.grantLinter(createContext(grantString));
|
||||
|
||||
if (errorMsg) {
|
||||
assert.strictEqual(diagnostics[0].message, errorMsg);
|
||||
} else {
|
||||
assert.strictEqual(diagnostics.length, 0);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user