scaffolder-backend: add tests for action rules
Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
@@ -14,7 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { hasTag } from './rules';
|
||||
import { JsonObject, JsonPrimitive } from '@backstage/types';
|
||||
import {
|
||||
hasActionId,
|
||||
hasBooleanProperty,
|
||||
hasNumberProperty,
|
||||
hasProperty,
|
||||
hasStringProperty,
|
||||
hasTag,
|
||||
} from './rules';
|
||||
|
||||
describe('hasTag', () => {
|
||||
describe('apply', () => {
|
||||
@@ -79,3 +87,358 @@ describe('hasTag', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasActionId', () => {
|
||||
describe('apply', () => {
|
||||
it('returns false when actionId is not matched', () => {
|
||||
expect(
|
||||
hasActionId.apply(
|
||||
{
|
||||
action: 'action',
|
||||
input: {},
|
||||
},
|
||||
{
|
||||
actionId: 'not-matched',
|
||||
},
|
||||
),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
it('returns true when actionId is matched', () => {
|
||||
expect(
|
||||
hasActionId.apply(
|
||||
{
|
||||
action: 'action',
|
||||
input: {},
|
||||
},
|
||||
{
|
||||
actionId: 'action',
|
||||
},
|
||||
),
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const input: JsonObject = {
|
||||
propwithstring: '1',
|
||||
propwithnumber: 2,
|
||||
propwithobject: {},
|
||||
propwithnull: null,
|
||||
propwithfalse: false,
|
||||
propwithtrue: true,
|
||||
propwitharray: ['item', 0, true, false],
|
||||
nested: { propwithstring: '1', nested: { propwithnumber: 1 } },
|
||||
};
|
||||
|
||||
describe('hasProperty', () => {
|
||||
describe('apply', () => {
|
||||
it.each([
|
||||
'foo',
|
||||
'bar',
|
||||
'prop.prop',
|
||||
'nested.nonexisting',
|
||||
'',
|
||||
'propwitharray.100',
|
||||
])(`returns false when a property doesn't exist in the input`, key => {
|
||||
expect(hasProperty.apply({ action: 'action', input }, { key })).toEqual(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'propwithstring',
|
||||
'propwithnumber',
|
||||
'propwithobject',
|
||||
'propwithnull',
|
||||
'propwithfalse',
|
||||
'propwithtrue',
|
||||
'propwitharray',
|
||||
'propwitharray.1',
|
||||
'nested.propwithstring',
|
||||
'nested.nested',
|
||||
'nested.nested.propwithnumber',
|
||||
])(`returns true when a property exists, property=%s`, key => {
|
||||
expect(hasProperty.apply({ action: 'action', input }, { key })).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['propwithstring', 1],
|
||||
['propwithnumber', '2'],
|
||||
['propwithnumber', true],
|
||||
['propwithobject', [{}]],
|
||||
['propwithnull', false],
|
||||
['propwithfalse', true],
|
||||
['propwithtrue', null],
|
||||
['propwitharray', 'nonexistingitem'],
|
||||
['propwitharray.0', 'nonmatchingitem'],
|
||||
['nested.propwithstring', 'x'],
|
||||
['nested.nested', '1'],
|
||||
['nested.nested.propwithnumber', 'ops'],
|
||||
])(
|
||||
`returns false when a property exists but the value doesn't match, key=%s value=%o`,
|
||||
(key, value) => {
|
||||
expect(
|
||||
hasProperty.apply(
|
||||
{ action: 'action', input },
|
||||
{ key, value: value as JsonPrimitive },
|
||||
),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['propwithstring', '1'],
|
||||
['propwithnumber', 2],
|
||||
['propwithnull', null],
|
||||
['propwithfalse', false],
|
||||
['propwithtrue', true],
|
||||
['propwitharray.0', 'item'],
|
||||
['nested.propwithstring', '1'],
|
||||
['nested.nested.propwithnumber', 1],
|
||||
])(
|
||||
`returns true when a property exists and the value matches, key=%s value=%o`,
|
||||
(key, value) => {
|
||||
expect(
|
||||
hasProperty.apply({ action: 'action', input }, { key, value }),
|
||||
).toEqual(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasBooleanProperty', () => {
|
||||
describe('apply', () => {
|
||||
it.each(['foo', 'bar', 'prop.prop', 'nested.nonexisting', ''])(
|
||||
`returns false when a property doesn't exist in the input`,
|
||||
key => {
|
||||
expect(
|
||||
hasBooleanProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'propwithstring',
|
||||
'propwithnumber',
|
||||
'propwithobject',
|
||||
'propwithnull',
|
||||
'propwitharray',
|
||||
'propwitharray.0',
|
||||
'nested.propwithstring',
|
||||
'nested.nested',
|
||||
'nested.nested.propwithnumber',
|
||||
])(
|
||||
`returns false when a property exists and is not a boolean, property=%s`,
|
||||
key => {
|
||||
expect(
|
||||
hasBooleanProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'propwithfalse',
|
||||
'propwithtrue',
|
||||
'propwitharray.2',
|
||||
'propwitharray.3',
|
||||
])(`returns true when a property exists, property=%s`, key => {
|
||||
expect(
|
||||
hasBooleanProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['propwithstring', true],
|
||||
['propwithnumber', true],
|
||||
['propwithnumber', true],
|
||||
['propwithobject', true],
|
||||
['propwithnull', true],
|
||||
['propwithfalse', true],
|
||||
['propwithtrue', false],
|
||||
['propwitharray', true],
|
||||
['propwitharray.2', false],
|
||||
['propwitharray.3', true],
|
||||
['nested.propwithstring', true],
|
||||
['nested.nested', true],
|
||||
['nested.nested.propwithnumber', true],
|
||||
])(
|
||||
`returns false when a property exists but the value doesn't match, key=%s value=%o`,
|
||||
(key, value) => {
|
||||
expect(
|
||||
hasBooleanProperty.apply({ action: 'action', input }, { key, value }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['propwithfalse', false],
|
||||
['propwithtrue', true],
|
||||
['propwitharray.2', true],
|
||||
['propwitharray.3', false],
|
||||
])(
|
||||
`returns true when a property exists and the value matches, key=%s value=%o`,
|
||||
(key, value) => {
|
||||
expect(
|
||||
hasBooleanProperty.apply({ action: 'action', input }, { key, value }),
|
||||
).toEqual(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasNumberProperty', () => {
|
||||
describe('apply', () => {
|
||||
it.each(['foo', 'bar', 'prop.prop', 'nested.nonexisting', ''])(
|
||||
`returns false when a property doesn't exist in the input`,
|
||||
key => {
|
||||
expect(
|
||||
hasNumberProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'propwithstring',
|
||||
'propwithobject',
|
||||
'propwithnull',
|
||||
'propwithfalse',
|
||||
'propwithtrue',
|
||||
'propwitharray',
|
||||
'propwitharray.0',
|
||||
'nested.propwithstring',
|
||||
'nested.nested',
|
||||
])(
|
||||
`returns false when a property exists and is not a number, property=%s`,
|
||||
key => {
|
||||
expect(
|
||||
hasNumberProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'propwithnumber',
|
||||
'nested.nested.propwithnumber',
|
||||
'propwitharray.1',
|
||||
])(`returns true when a property exists, property=%s`, key => {
|
||||
expect(
|
||||
hasNumberProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['propwithstring', 1],
|
||||
['propwithnumber', 1000],
|
||||
['propwithnumber', 101],
|
||||
['propwithobject', 1],
|
||||
['propwithnull', 1],
|
||||
['propwithfalse', 1],
|
||||
['propwithtrue', 1],
|
||||
['propwitharray', 1],
|
||||
['propwitharray.2', 1],
|
||||
['propwitharray.3', 1],
|
||||
['nested.propwithstring', 1],
|
||||
['nested.nested', 1],
|
||||
['nested.nested.propwithnumber', 100],
|
||||
])(
|
||||
`returns false when a property exists but the value doesn't match, key=%s value=%o`,
|
||||
(key, value) => {
|
||||
expect(
|
||||
hasNumberProperty.apply({ action: 'action', input }, { key, value }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['propwithnumber', 2],
|
||||
['nested.nested.propwithnumber', 1],
|
||||
['propwitharray.1', 0],
|
||||
])(
|
||||
`returns true when a property exists and the value matches, key=%s value=%o`,
|
||||
(key, value) => {
|
||||
expect(
|
||||
hasNumberProperty.apply({ action: 'action', input }, { key, value }),
|
||||
).toEqual(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasStringProperty', () => {
|
||||
describe('apply', () => {
|
||||
it.each(['foo', 'bar', 'prop.prop', 'nested.nonexisting', ''])(
|
||||
`returns false when a property doesn't exist in the input`,
|
||||
key => {
|
||||
expect(
|
||||
hasStringProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'propwithnumber',
|
||||
'propwithobject',
|
||||
'propwithnull',
|
||||
'propwithfalse',
|
||||
'propwithtrue',
|
||||
'propwitharray',
|
||||
'propwitharray.1',
|
||||
'nested.nested.propwithnumber',
|
||||
'nested.nested',
|
||||
])(
|
||||
`returns false when a property exists and is not a string, property=%s`,
|
||||
key => {
|
||||
expect(
|
||||
hasStringProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['propwithstring', 'nested.propwithstring', 'propwitharray.0'])(
|
||||
`returns true when a property exists, property=%s`,
|
||||
key => {
|
||||
expect(
|
||||
hasStringProperty.apply({ action: 'action', input }, { key }),
|
||||
).toEqual(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['propwithstring', 'nonmatchingstring'],
|
||||
['propwithnumber', 's'],
|
||||
['propwithnumber', 's'],
|
||||
['propwithobject', 's'],
|
||||
['propwithnull', 's'],
|
||||
['propwithfalse', 's'],
|
||||
['propwithtrue', 's'],
|
||||
['propwitharray', 's'],
|
||||
['propwitharray.2', 's'],
|
||||
['propwitharray.3', 's'],
|
||||
['nested.nested', 's'],
|
||||
['nested.nested.propwithnumber', 's'],
|
||||
])(
|
||||
`returns false when a property exists but the value doesn't match, key=%s value=%o`,
|
||||
(key, value) => {
|
||||
expect(
|
||||
hasStringProperty.apply({ action: 'action', input }, { key, value }),
|
||||
).toEqual(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['propwithstring', '1'],
|
||||
['nested.propwithstring', '1'],
|
||||
['propwitharray.0', 'item'],
|
||||
])(
|
||||
`returns true when a property exists and the value matches, key=%s value=%o`,
|
||||
(key, value) => {
|
||||
expect(
|
||||
hasStringProperty.apply({ action: 'action', input }, { key, value }),
|
||||
).toEqual(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-common';
|
||||
|
||||
import { z } from 'zod';
|
||||
import { JsonObject, JsonPrimitive, JsonValue } from '@backstage/types';
|
||||
import { String, get } from 'lodash';
|
||||
import { JsonObject, JsonPrimitive } from '@backstage/types';
|
||||
import { get } from 'lodash';
|
||||
|
||||
export const createTemplatePermissionRule = makeCreatePermissionRule<
|
||||
TemplateEntityStepV1beta3 | TemplateParametersV1beta3,
|
||||
@@ -70,16 +70,36 @@ export const hasActionId = createActionPermissionRule({
|
||||
toQuery: () => ({}),
|
||||
});
|
||||
|
||||
export const hasBooleanProperty = buildHasProperty(z.boolean());
|
||||
export const hasNullProperty = buildHasProperty(z.null());
|
||||
export const hasNumberProperty = buildHasProperty(z.number());
|
||||
export const hasStringProperty = buildHasProperty(z.string());
|
||||
export const hasProperty = buildHasProperty({
|
||||
name: 'HAS_PROPERTY',
|
||||
valueSchema: z.union([z.string(), z.number(), z.boolean(), z.null()]),
|
||||
validateProperty: false,
|
||||
});
|
||||
|
||||
function buildHasProperty<Schema extends z.ZodType<JsonPrimitive>>(
|
||||
valueSchema: Schema,
|
||||
) {
|
||||
export const hasBooleanProperty = buildHasProperty({
|
||||
name: 'HAS_BOOLEAN_PROPERTY',
|
||||
valueSchema: z.boolean(),
|
||||
});
|
||||
export const hasNumberProperty = buildHasProperty({
|
||||
name: 'HAS_NUMBER_PROPERTY',
|
||||
valueSchema: z.number(),
|
||||
});
|
||||
export const hasStringProperty = buildHasProperty({
|
||||
name: 'HAS_STRING_PROPERTY',
|
||||
valueSchema: z.string(),
|
||||
});
|
||||
|
||||
function buildHasProperty<Schema extends z.ZodType<JsonPrimitive>>({
|
||||
name,
|
||||
valueSchema,
|
||||
validateProperty = true,
|
||||
}: {
|
||||
name: string;
|
||||
valueSchema: Schema;
|
||||
validateProperty?: boolean;
|
||||
}) {
|
||||
return createActionPermissionRule({
|
||||
name: `HAS_STRING_PROPERTY`,
|
||||
name,
|
||||
description: `Allow actions with the specified property`,
|
||||
resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION,
|
||||
paramsSchema: z.object({
|
||||
@@ -87,20 +107,21 @@ function buildHasProperty<Schema extends z.ZodType<JsonPrimitive>>(
|
||||
.string()
|
||||
.describe(`Property within the action parameters to match on`),
|
||||
value: valueSchema.describe(`Value of the given property to match on`),
|
||||
}) as unknown as z.ZodType<{ key: string; value: z.infer<Schema> }>,
|
||||
}) as unknown as z.ZodType<{ key: string; value?: z.infer<Schema> }>,
|
||||
apply: (resource, { key, value }) => {
|
||||
const foundValue = get(resource.input, key);
|
||||
|
||||
if (Array.isArray(foundValue)) {
|
||||
if (value !== undefined) {
|
||||
return foundValue.includes(value);
|
||||
if (validateProperty && !valueSchema.safeParse(foundValue).success) {
|
||||
return false;
|
||||
}
|
||||
if (value !== undefined) {
|
||||
if (valueSchema.safeParse(value).success) {
|
||||
return value === foundValue;
|
||||
}
|
||||
return foundValue.length > 0;
|
||||
return false;
|
||||
}
|
||||
if (value !== undefined && z.string().safeParse(value).success) {
|
||||
return value === foundValue;
|
||||
}
|
||||
return !!foundValue;
|
||||
|
||||
return foundValue !== undefined;
|
||||
},
|
||||
toQuery: () => ({}),
|
||||
});
|
||||
@@ -110,7 +131,6 @@ export const scaffolderTemplateRules = { hasTag };
|
||||
export const scaffolderActionRules = {
|
||||
hasActionId,
|
||||
hasBooleanProperty,
|
||||
hasNullProperty,
|
||||
hasNumberProperty,
|
||||
hasStringProperty,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user