ActionsRegistry: Enable filtering of Actions from sources (#32134)
* feat(backend-defaults): add config schema for action filtering Signed-off-by: benjdlambert <ben@blam.sh> * feat(backend-defaults): implement action filtering with glob patterns and attributes Signed-off-by: benjdlambert <ben@blam.sh> * test(backend-defaults): add edge case tests for action filtering Signed-off-by: benjdlambert <ben@blam.sh> * chore: add changeset for action filtering feature Signed-off-by: benjdlambert <ben@blam.sh> * feat: code review comments Signed-off-by: benjdlambert <ben@blam.sh> * chore: should always exclude Signed-off-by: benjdlambert <ben@blam.sh> --------- Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
---
|
||||
'@backstage/backend-defaults': minor
|
||||
---
|
||||
|
||||
Added action filtering support with glob patterns and attribute constraints.
|
||||
|
||||
The `ActionsService` now supports filtering actions based on configuration. This allows controlling which actions are exposed to consumers like the MCP backend.
|
||||
|
||||
Configuration example:
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
actions:
|
||||
pluginSources:
|
||||
- catalog
|
||||
- scaffolder
|
||||
filter:
|
||||
include:
|
||||
- id: 'catalog:*'
|
||||
attributes:
|
||||
destructive: false
|
||||
- id: 'scaffolder:*'
|
||||
exclude:
|
||||
- id: '*:delete-*'
|
||||
- attributes:
|
||||
readOnly: false
|
||||
```
|
||||
|
||||
Filtering logic:
|
||||
|
||||
- `include`: Rules for actions to include. Each rule can specify an `id` glob pattern and/or `attributes` constraints. An action must match at least one rule to be included. If no include rules are specified, all actions are included by default.
|
||||
- `exclude`: Rules for actions to exclude. Takes precedence over include rules.
|
||||
- Each rule combines `id` and `attributes` with AND logic (both must match if specified).
|
||||
+98
@@ -154,6 +154,104 @@ export interface Config {
|
||||
* List of plugin sources to load actions from.
|
||||
*/
|
||||
pluginSources?: string[];
|
||||
|
||||
/**
|
||||
* Filter configuration for actions. Allows controlling which actions
|
||||
* are exposed to consumers based on patterns and attributes.
|
||||
*/
|
||||
filter?: {
|
||||
/**
|
||||
* Rules for actions to include. An action must match at least one rule to be included.
|
||||
* Each rule can specify an id pattern and/or attribute constraints.
|
||||
* If no include rules are specified, all actions are included by default.
|
||||
*
|
||||
* @example
|
||||
* ```yaml
|
||||
* include:
|
||||
* - id: 'catalog:*'
|
||||
* attributes:
|
||||
* destructive: false
|
||||
* - id: 'scaffolder:*'
|
||||
* ```
|
||||
*/
|
||||
include?: Array<{
|
||||
/**
|
||||
* Glob pattern for action IDs to match.
|
||||
* Action IDs have the format `{pluginId}:{actionName}`.
|
||||
* @example 'catalog:*'
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Attribute constraints. All specified attributes must match.
|
||||
* Actions are compared against their resolved attributes (with defaults applied).
|
||||
*/
|
||||
attributes?: {
|
||||
/**
|
||||
* If specified, only match actions where destructive matches this value.
|
||||
* Actions default to destructive: true if not explicitly set.
|
||||
*/
|
||||
destructive?: boolean;
|
||||
|
||||
/**
|
||||
* If specified, only match actions where readOnly matches this value.
|
||||
* Actions default to readOnly: false if not explicitly set.
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
|
||||
/**
|
||||
* If specified, only match actions where idempotent matches this value.
|
||||
* Actions default to idempotent: false if not explicitly set.
|
||||
*/
|
||||
idempotent?: boolean;
|
||||
};
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Rules for actions to exclude. Exclusions take precedence over inclusions.
|
||||
* Each rule can specify an id pattern and/or attribute constraints.
|
||||
*
|
||||
* @example
|
||||
* ```yaml
|
||||
* exclude:
|
||||
* - id: '*:delete-*'
|
||||
* - attributes:
|
||||
* readOnly: false
|
||||
* ```
|
||||
*/
|
||||
exclude?: Array<{
|
||||
/**
|
||||
* Glob pattern for action IDs to match.
|
||||
* Action IDs have the format `{pluginId}:{actionName}`.
|
||||
* @example '*:delete-*'
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Attribute constraints. All specified attributes must match.
|
||||
* Actions are compared against their resolved attributes (with defaults applied).
|
||||
*/
|
||||
attributes?: {
|
||||
/**
|
||||
* If specified, only match actions where destructive matches this value.
|
||||
* Actions default to destructive: true if not explicitly set.
|
||||
*/
|
||||
destructive?: boolean;
|
||||
|
||||
/**
|
||||
* If specified, only match actions where readOnly matches this value.
|
||||
* Actions default to readOnly: false if not explicitly set.
|
||||
*/
|
||||
readOnly?: boolean;
|
||||
|
||||
/**
|
||||
* If specified, only match actions where idempotent matches this value.
|
||||
* Actions default to idempotent: false if not explicitly set.
|
||||
*/
|
||||
idempotent?: boolean;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
ActionsService,
|
||||
ActionsServiceAction,
|
||||
} from '@backstage/backend-plugin-api/alpha';
|
||||
import { Minimatch } from 'minimatch';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
export class DefaultActionsService implements ActionsService {
|
||||
private readonly discovery: DiscoveryService;
|
||||
@@ -86,7 +88,7 @@ export class DefaultActionsService implements ActionsService {
|
||||
}),
|
||||
);
|
||||
|
||||
return { actions: remoteActionsList.flat() };
|
||||
return { actions: this.applyFilters(remoteActionsList.flat()) };
|
||||
}
|
||||
|
||||
async invoke(opts: {
|
||||
@@ -148,4 +150,106 @@ export class DefaultActionsService implements ActionsService {
|
||||
}
|
||||
return id.substring(0, colonIndex);
|
||||
}
|
||||
|
||||
private applyFilters(
|
||||
actions: ActionsServiceAction[],
|
||||
): ActionsServiceAction[] {
|
||||
const filterConfig = this.config.getOptionalConfig(
|
||||
'backend.actions.filter',
|
||||
);
|
||||
|
||||
if (!filterConfig) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
const includeRules = this.parseFilterRules(
|
||||
filterConfig.getOptionalConfigArray('include') ?? [],
|
||||
);
|
||||
const excludeRules = this.parseFilterRules(
|
||||
filterConfig.getOptionalConfigArray('exclude') ?? [],
|
||||
);
|
||||
|
||||
return actions.filter(action => {
|
||||
const excluded = excludeRules.some(rule =>
|
||||
this.matchesRule(action, rule),
|
||||
);
|
||||
|
||||
if (excluded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no include rules, include by default
|
||||
if (includeRules.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Must match at least one include rule
|
||||
return includeRules.some(rule => this.matchesRule(action, rule));
|
||||
});
|
||||
}
|
||||
|
||||
private parseFilterRules(configArray: Array<Config>): Array<{
|
||||
idMatcher?: Minimatch;
|
||||
attributes?: Partial<
|
||||
Record<'destructive' | 'readOnly' | 'idempotent', boolean>
|
||||
>;
|
||||
}> {
|
||||
return configArray.map(ruleConfig => {
|
||||
const idPattern = ruleConfig.getOptionalString('id');
|
||||
const attributesConfig = ruleConfig.getOptionalConfig('attributes');
|
||||
|
||||
const rule: {
|
||||
idMatcher?: Minimatch;
|
||||
attributes?: Partial<
|
||||
Record<'destructive' | 'readOnly' | 'idempotent', boolean>
|
||||
>;
|
||||
} = {};
|
||||
|
||||
if (idPattern) {
|
||||
rule.idMatcher = new Minimatch(idPattern);
|
||||
}
|
||||
|
||||
if (attributesConfig) {
|
||||
rule.attributes = {};
|
||||
for (const key of ['destructive', 'readOnly', 'idempotent'] as const) {
|
||||
const value = attributesConfig.getOptionalBoolean(key);
|
||||
if (value !== undefined) {
|
||||
rule.attributes[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rule;
|
||||
});
|
||||
}
|
||||
|
||||
private matchesRule(
|
||||
action: ActionsServiceAction,
|
||||
rule: {
|
||||
idMatcher?: Minimatch;
|
||||
attributes?: Partial<
|
||||
Record<'destructive' | 'readOnly' | 'idempotent', boolean>
|
||||
>;
|
||||
},
|
||||
): boolean {
|
||||
// If id pattern is specified, it must match
|
||||
if (rule.idMatcher && !rule.idMatcher.match(action.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If attributes are specified, all must match
|
||||
if (rule.attributes) {
|
||||
for (const [key, value] of Object.entries(rule.attributes)) {
|
||||
if (
|
||||
action.attributes[
|
||||
key as 'destructive' | 'readOnly' | 'idempotent'
|
||||
] !== value
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+470
@@ -115,6 +115,476 @@ describe('actionsServiceFactory', () => {
|
||||
expect(mockActionsListEndpoint).toHaveBeenCalledTimes(1);
|
||||
expect(mockNotFoundActionsListEndpoint).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should filter actions based on include patterns', async () => {
|
||||
const multipleActions: ActionsServiceAction[] = [
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:get-entity',
|
||||
name: 'get-entity',
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:delete-entity',
|
||||
name: 'delete-entity',
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'other-plugin:get-thing',
|
||||
name: 'get-thing',
|
||||
},
|
||||
];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
actions: multipleActions.filter(a =>
|
||||
a.id.startsWith('my-plugin:'),
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost:0/api/other-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
actions: multipleActions.filter(a =>
|
||||
a.id.startsWith('other-plugin:'),
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const subject = await ServiceFactoryTester.from(actionsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['my-plugin', 'other-plugin'],
|
||||
filter: {
|
||||
include: [{ id: 'my-plugin:*' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
actionsServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.discovery.factory(),
|
||||
actionsRegistryServiceFactory,
|
||||
],
|
||||
}).getSubject();
|
||||
|
||||
const { actions } = await subject.list({
|
||||
credentials: mockCredentials.service('user:default/mock'),
|
||||
});
|
||||
|
||||
expect(actions.map(a => a.id)).toEqual([
|
||||
'my-plugin:get-entity',
|
||||
'my-plugin:delete-entity',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter actions based on exclude patterns', async () => {
|
||||
const multipleActions: ActionsServiceAction[] = [
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:get-entity',
|
||||
name: 'get-entity',
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:delete-entity',
|
||||
name: 'delete-entity',
|
||||
},
|
||||
];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) => res(ctx.json({ actions: multipleActions })),
|
||||
),
|
||||
);
|
||||
|
||||
const subject = await ServiceFactoryTester.from(actionsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['my-plugin'],
|
||||
filter: {
|
||||
exclude: [{ id: '*:delete-*' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
actionsServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.discovery.factory(),
|
||||
actionsRegistryServiceFactory,
|
||||
],
|
||||
}).getSubject();
|
||||
|
||||
const { actions } = await subject.list({
|
||||
credentials: mockCredentials.service('user:default/mock'),
|
||||
});
|
||||
|
||||
expect(actions.map(a => a.id)).toEqual(['my-plugin:get-entity']);
|
||||
});
|
||||
|
||||
it('should have exclude take precedence over include', async () => {
|
||||
const multipleActions: ActionsServiceAction[] = [
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:get-entity',
|
||||
name: 'get-entity',
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:delete-entity',
|
||||
name: 'delete-entity',
|
||||
},
|
||||
];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) => res(ctx.json({ actions: multipleActions })),
|
||||
),
|
||||
);
|
||||
|
||||
const subject = await ServiceFactoryTester.from(actionsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['my-plugin'],
|
||||
filter: {
|
||||
include: [{ id: 'my-plugin:*' }],
|
||||
exclude: [{ id: 'my-plugin:delete-entity' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
actionsServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.discovery.factory(),
|
||||
actionsRegistryServiceFactory,
|
||||
],
|
||||
}).getSubject();
|
||||
|
||||
const { actions } = await subject.list({
|
||||
credentials: mockCredentials.service('user:default/mock'),
|
||||
});
|
||||
|
||||
expect(actions.map(a => a.id)).toEqual(['my-plugin:get-entity']);
|
||||
});
|
||||
|
||||
it('should always apply exclude rules even when action matches include', async () => {
|
||||
// This tests that exclude is checked FIRST and always wins,
|
||||
// regardless of whether the action would match an include rule
|
||||
const multipleActions: ActionsServiceAction[] = [
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:safe-action',
|
||||
name: 'safe-action',
|
||||
attributes: {
|
||||
destructive: false,
|
||||
readOnly: true,
|
||||
idempotent: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:dangerous-action',
|
||||
name: 'dangerous-action',
|
||||
attributes: {
|
||||
destructive: true,
|
||||
readOnly: false,
|
||||
idempotent: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) => res(ctx.json({ actions: multipleActions })),
|
||||
),
|
||||
);
|
||||
|
||||
const subject = await ServiceFactoryTester.from(actionsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['my-plugin'],
|
||||
filter: {
|
||||
// Include all my-plugin actions
|
||||
include: [{ id: 'my-plugin:*' }],
|
||||
// But exclude any destructive ones
|
||||
exclude: [{ attributes: { destructive: true } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
actionsServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.discovery.factory(),
|
||||
actionsRegistryServiceFactory,
|
||||
],
|
||||
}).getSubject();
|
||||
|
||||
const { actions } = await subject.list({
|
||||
credentials: mockCredentials.service('user:default/mock'),
|
||||
});
|
||||
|
||||
// dangerous-action matches include (my-plugin:*) but is excluded due to destructive: true
|
||||
expect(actions.map(a => a.id)).toEqual(['my-plugin:safe-action']);
|
||||
});
|
||||
|
||||
it('should filter actions based on attribute constraints', async () => {
|
||||
const multipleActions: ActionsServiceAction[] = [
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:read-action',
|
||||
name: 'read-action',
|
||||
attributes: {
|
||||
destructive: false,
|
||||
readOnly: true,
|
||||
idempotent: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:write-action',
|
||||
name: 'write-action',
|
||||
attributes: {
|
||||
destructive: true,
|
||||
readOnly: false,
|
||||
idempotent: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) => res(ctx.json({ actions: multipleActions })),
|
||||
),
|
||||
);
|
||||
|
||||
const subject = await ServiceFactoryTester.from(actionsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['my-plugin'],
|
||||
filter: {
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
actionsServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.discovery.factory(),
|
||||
actionsRegistryServiceFactory,
|
||||
],
|
||||
}).getSubject();
|
||||
|
||||
const { actions } = await subject.list({
|
||||
credentials: mockCredentials.service('user:default/mock'),
|
||||
});
|
||||
|
||||
expect(actions.map(a => a.id)).toEqual(['my-plugin:read-action']);
|
||||
});
|
||||
|
||||
it('should combine pattern and attribute filtering with AND logic', async () => {
|
||||
const multipleActions: ActionsServiceAction[] = [
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:safe-read',
|
||||
name: 'safe-read',
|
||||
attributes: {
|
||||
destructive: false,
|
||||
readOnly: true,
|
||||
idempotent: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:dangerous-read',
|
||||
name: 'dangerous-read',
|
||||
attributes: {
|
||||
destructive: true,
|
||||
readOnly: true,
|
||||
idempotent: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'other-plugin:safe-read',
|
||||
name: 'safe-read',
|
||||
attributes: {
|
||||
destructive: false,
|
||||
readOnly: true,
|
||||
idempotent: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
actions: multipleActions.filter(a =>
|
||||
a.id.startsWith('my-plugin:'),
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'http://localhost:0/api/other-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
actions: multipleActions.filter(a =>
|
||||
a.id.startsWith('other-plugin:'),
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const subject = await ServiceFactoryTester.from(actionsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['my-plugin', 'other-plugin'],
|
||||
filter: {
|
||||
include: [
|
||||
{
|
||||
id: 'my-plugin:*',
|
||||
attributes: {
|
||||
destructive: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
actionsServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.discovery.factory(),
|
||||
actionsRegistryServiceFactory,
|
||||
],
|
||||
}).getSubject();
|
||||
|
||||
const { actions } = await subject.list({
|
||||
credentials: mockCredentials.service('user:default/mock'),
|
||||
});
|
||||
|
||||
// Only my-plugin:safe-read matches: my-plugin:* pattern AND destructive: false
|
||||
expect(actions.map(a => a.id)).toEqual(['my-plugin:safe-read']);
|
||||
});
|
||||
|
||||
it('should return all actions when no filter config is provided', async () => {
|
||||
const multipleActions: ActionsServiceAction[] = [
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:action-one',
|
||||
name: 'action-one',
|
||||
},
|
||||
{
|
||||
...mockActionsDefinition,
|
||||
id: 'my-plugin:action-two',
|
||||
name: 'action-two',
|
||||
},
|
||||
];
|
||||
|
||||
server.use(
|
||||
rest.get(
|
||||
'http://localhost:0/api/my-plugin/.backstage/actions/v1/actions',
|
||||
(_req, res, ctx) => res(ctx.json({ actions: multipleActions })),
|
||||
),
|
||||
);
|
||||
|
||||
const subject = await ServiceFactoryTester.from(actionsServiceFactory, {
|
||||
dependencies: [
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
actions: {
|
||||
pluginSources: ['my-plugin'],
|
||||
// No filter config
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
actionsServiceFactory,
|
||||
httpRouterServiceFactory,
|
||||
mockServices.httpAuth.factory({
|
||||
defaultCredentials: mockCredentials.service('user:default/mock'),
|
||||
}),
|
||||
mockServices.discovery.factory(),
|
||||
actionsRegistryServiceFactory,
|
||||
],
|
||||
}).getSubject();
|
||||
|
||||
const { actions } = await subject.list({
|
||||
credentials: mockCredentials.service('user:default/mock'),
|
||||
});
|
||||
|
||||
expect(actions.map(a => a.id)).toEqual([
|
||||
'my-plugin:action-one',
|
||||
'my-plugin:action-two',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invoke', () => {
|
||||
|
||||
Reference in New Issue
Block a user