scaffolder-backend: add tests for action rules

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2023-04-12 18:23:12 +02:00
parent f65a32b342
commit 4324322ab4
2 changed files with 404 additions and 21 deletions
@@ -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);
},
);
});
});
+40 -20
View File
@@ -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,
};