Skip to content

Commit c7f0beb

Browse files
test(orchestrator): add scaffolder backend module test coverage (#3641)
* test(orchestrator): add scaffolder backend module test coverage Adds unit tests for utils, getWorkflowParams, and runWorkflow actions. Fixes undefined token fallback in utils and dry-run early return in runWorkflow. Co-authored-by: Cursor <cursoragent@cursor.com> * test: address PR review comments for orchestrator scaffolder actions - Remove duplicated getError function - Add test for undefined token response in getRequestConfigOption - Remove redundant isDryRun assignment in runWorkflow.test.ts - Add missing blank lines after license headers - Use 'as any' instead of 'as never' in utils.test.ts - Add changeset Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5551345 commit c7f0beb

7 files changed

Lines changed: 605 additions & 34 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-orchestrator': patch
3+
---
4+
5+
Added unit test coverage for orchestrator scaffolder actions and fixed token fallback logic.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
18+
import { JsonObject } from '@backstage/types';
19+
20+
import { createGetWorkflowParamsAction } from './getWorkflowParams';
21+
import { getOrchestratorApi, getRequestConfigOption } from './utils';
22+
23+
jest.mock('axios', () => ({
24+
isAxiosError: (error: { isAxiosError?: boolean }) =>
25+
Boolean(error?.isAxiosError),
26+
}));
27+
28+
jest.mock('./utils', () => ({
29+
...jest.requireActual('./utils'),
30+
getOrchestratorApi: jest.fn(),
31+
getRequestConfigOption: jest.fn(),
32+
}));
33+
34+
const mockedGetOrchestratorApi = jest.mocked(getOrchestratorApi);
35+
const mockedGetRequestConfigOption = jest.mocked(getRequestConfigOption);
36+
37+
describe('createGetWorkflowParamsAction', () => {
38+
const discoveryService = {} as any;
39+
const authService = {} as any;
40+
const reqConfigOption = {
41+
headers: {
42+
Authorization: 'Bearer token',
43+
},
44+
};
45+
const mockApi = {
46+
getWorkflowOverviewById: jest.fn(),
47+
getWorkflowInputSchemaById: jest.fn(),
48+
};
49+
50+
beforeEach(() => {
51+
jest.clearAllMocks();
52+
mockedGetOrchestratorApi.mockResolvedValue(mockApi as any);
53+
mockedGetRequestConfigOption.mockResolvedValue(reqConfigOption as any);
54+
});
55+
56+
it('throws when workflow_id is missing', async () => {
57+
const action = createGetWorkflowParamsAction(discoveryService, authService);
58+
const ctx = createMockActionContext({
59+
input: {},
60+
});
61+
62+
await expect(action.handler(ctx as any)).rejects.toThrow(
63+
'Missing workflow_id required input parameter.',
64+
);
65+
expect(mockedGetOrchestratorApi).not.toHaveBeenCalled();
66+
});
67+
68+
it('outputs fallback title, empty description, and indented parameters yaml', async () => {
69+
const workflowId = 'greeting';
70+
const inputSchema = {
71+
type: 'object',
72+
properties: {
73+
greeting: {
74+
type: 'string',
75+
},
76+
},
77+
} satisfies JsonObject;
78+
79+
mockApi.getWorkflowOverviewById.mockResolvedValue({
80+
data: {
81+
name: '',
82+
},
83+
});
84+
mockApi.getWorkflowInputSchemaById.mockResolvedValue({
85+
data: {
86+
inputSchema,
87+
},
88+
});
89+
90+
const action = createGetWorkflowParamsAction(discoveryService, authService);
91+
const ctx = createMockActionContext({
92+
input: {
93+
workflow_id: workflowId,
94+
indent: 4,
95+
},
96+
});
97+
98+
await action.handler(ctx);
99+
100+
expect(mockApi.getWorkflowOverviewById).toHaveBeenCalledWith(
101+
workflowId,
102+
reqConfigOption,
103+
);
104+
expect(mockApi.getWorkflowInputSchemaById).toHaveBeenCalledWith(
105+
workflowId,
106+
undefined,
107+
reqConfigOption,
108+
);
109+
expect(ctx.output).toHaveBeenCalledWith('title', workflowId);
110+
expect(ctx.output).toHaveBeenCalledWith('description', '');
111+
112+
const parametersOutput = (ctx.output as jest.Mock).mock.calls.find(
113+
([key]) => key === 'parameters',
114+
)?.[1];
115+
expect(parametersOutput).toContain('greeting:');
116+
expect(parametersOutput.startsWith('\n -')).toBe(true);
117+
});
118+
119+
it('outputs empty parameters when the schema does not define properties', async () => {
120+
mockApi.getWorkflowOverviewById.mockResolvedValue({
121+
data: {
122+
name: 'Greeting workflow',
123+
description: 'Collect greeting details',
124+
},
125+
});
126+
mockApi.getWorkflowInputSchemaById.mockResolvedValue({
127+
data: {
128+
inputSchema: {},
129+
},
130+
});
131+
132+
const action = createGetWorkflowParamsAction(discoveryService, authService);
133+
const ctx = createMockActionContext({
134+
input: {
135+
workflow_id: 'greeting',
136+
},
137+
});
138+
139+
await action.handler(ctx);
140+
141+
expect(ctx.output).toHaveBeenCalledWith('title', 'Greeting workflow');
142+
expect(ctx.output).toHaveBeenCalledWith(
143+
'description',
144+
'Collect greeting details',
145+
);
146+
expect(ctx.output).toHaveBeenCalledWith('parameters', '{}');
147+
});
148+
149+
it('throws when the workflow is not found', async () => {
150+
mockApi.getWorkflowOverviewById.mockResolvedValue({
151+
data: undefined,
152+
});
153+
154+
const action = createGetWorkflowParamsAction(discoveryService, authService);
155+
const ctx = createMockActionContext({
156+
input: {
157+
workflow_id: 'missing-workflow',
158+
},
159+
});
160+
161+
await expect(action.handler(ctx)).rejects.toThrow(
162+
'Can not find workflow missing-workflow',
163+
);
164+
});
165+
166+
it('maps axios-shaped errors to orchestrator error details', async () => {
167+
mockApi.getWorkflowOverviewById.mockRejectedValue({
168+
isAxiosError: true,
169+
response: {
170+
data: {
171+
error: {
172+
message: 'Workflow service unavailable',
173+
name: 'UpstreamWorkflowError',
174+
},
175+
},
176+
},
177+
});
178+
179+
const action = createGetWorkflowParamsAction(discoveryService, authService);
180+
const ctx = createMockActionContext({
181+
input: {
182+
workflow_id: 'greeting',
183+
},
184+
});
185+
186+
await expect(action.handler(ctx)).rejects.toMatchObject({
187+
message: 'Workflow service unavailable',
188+
name: 'UpstreamWorkflowError',
189+
});
190+
});
191+
});

workspaces/orchestrator/plugins/scaffolder-backend-module-orchestrator/src/actions/getWorkflowParams.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,9 @@ import { DiscoveryApi } from '@backstage/plugin-permission-common';
1818
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
1919
import { JsonObject } from '@backstage/types';
2020

21-
import { isAxiosError } from 'axios';
2221
import { dump } from 'js-yaml';
2322

24-
import { getOrchestratorApi, getRequestConfigOption } from './utils';
25-
26-
const getError = (err: unknown): Error => {
27-
if (
28-
isAxiosError<{ error: { message: string; name: string } }>(err) &&
29-
err.response?.data?.error?.message
30-
) {
31-
const error = new Error(err.response?.data?.error?.message);
32-
error.name = err.response?.data?.error?.name || 'Error';
33-
return error;
34-
}
35-
return err as Error;
36-
};
23+
import { getError, getOrchestratorApi, getRequestConfigOption } from './utils';
3724

3825
const indentString = (str: string, indent: number) =>
3926
indent ? str.replace(/^/gm, ' '.repeat(indent)) : str;

0 commit comments

Comments
 (0)