From c6ae5b2e332d96dd86af3e8ea16a101b39cbe877 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 13 Feb 2025 09:27:02 +0100 Subject: [PATCH] chore: refactor a little bit and fix tests Signed-off-by: blam --- .../tasks/NunjucksWorkflowRunner.test.ts | 203 ++++++++++++------ plugins/scaffolder-node/src/actions/util.ts | 27 +-- 2 files changed, 153 insertions(+), 77 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 8614f99388..26b784b512 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -104,33 +104,37 @@ describe('NunjucksWorkflowRunner', () => { fakeActionHandler = jest.fn(); fakeTaskLog = jest.fn(); - actionRegistry.register({ - id: 'jest-mock-action', - description: 'Mock action for testing', - handler: fakeActionHandler, - }); - - actionRegistry.register({ - id: 'jest-validated-action', - description: 'Mock action for testing', - supportsDryRun: true, - handler: fakeActionHandler, - schema: { - input: { - type: 'object', - required: ['foo'], - properties: { - foo: { - type: 'number', - }, - }, - }, - }, - }); + actionRegistry.register( + createTemplateAction({ + id: 'jest-mock-action', + description: 'Mock action for testing', + handler: fakeActionHandler, + }), + ); actionRegistry.register( createTemplateAction({ - id: 'jest-zod-validated-action', + id: 'jest-validated-action', + description: 'Mock action for testing', + supportsDryRun: true, + handler: fakeActionHandler, + schema: { + input: { + type: 'object', + required: ['foo'], + properties: { + foo: { + type: 'number', + }, + }, + }, + }, + }), + ); + + actionRegistry.register( + createTemplateAction({ + id: 'jest-legacy-zod-validated-action', description: 'Mock action for testing', handler: fakeActionHandler, supportsDryRun: true, @@ -142,52 +146,82 @@ describe('NunjucksWorkflowRunner', () => { }) as TemplateAction, ); - actionRegistry.register({ - id: 'output-action', - description: 'Mock action for testing', - handler: async ctx => { - ctx.output('mock', 'backstage'); - ctx.output('shouldRun', true); - }, - }); + actionRegistry.register( + createTemplateAction({ + id: 'jest-zod-validated-action', + description: 'Mock ac', + supportsDryRun: true, + schema: { + input: { + foo: zod => zod.number(), + }, + output: { + test: zod => zod.string(), + }, + }, + handler: fakeActionHandler, + }) as TemplateAction, + ); - actionRegistry.register({ - id: 'checkpoints-action', - description: 'Mock action with checkpoints', - handler: async ctx => { - const key1 = await ctx.checkpoint({ - key: 'key1', - fn: async () => 'updated', - }); - const key2 = await ctx.checkpoint({ - key: 'key2', - fn: async () => 'updated', - }); - const key3 = await ctx.checkpoint({ - key: 'key3', - fn: async () => 'updated', - }); + actionRegistry.register( + createTemplateAction({ + id: 'output-action', + description: 'Mock action for testing', + handler: async ctx => { + ctx.output('mock', 'backstage'); + ctx.output('shouldRun', true); + }, + }), + ); - const key4 = await ctx.checkpoint({ - key: 'key4', - fn: () => {}, - }); + actionRegistry.register( + createTemplateAction({ + id: 'checkpoints-action', + description: 'Mock action with checkpoints', + schema: { + output: z.object({ + key1: z.string(), + key2: z.string(), + key3: z.string(), + key4: z.string(), + key5: z.string(), + }), + }, + handler: async ctx => { + const key1 = await ctx.checkpoint({ + key: 'key1', + fn: async () => 'updated', + }); + const key2 = await ctx.checkpoint({ + key: 'key2', + fn: async () => 'updated', + }); + const key3 = await ctx.checkpoint({ + key: 'key3', + fn: async () => 'updated', + }); - const key5 = await ctx.checkpoint({ - key: 'key5', - fn: async () => {}, - }); + const key4 = await ctx.checkpoint({ + key: 'key4', + fn: () => {}, + }); - ctx.output('key1', key1); - ctx.output('key2', key2); - ctx.output('key3', key3); + const key5 = await ctx.checkpoint({ + key: 'key5', + fn: async () => {}, + }); - // @ts-expect-error - this is void return - ctx.output('key4', key4); - // @ts-expect-error - this is void return - ctx.output('key5', key5); - }, - }); + ctx.output('key1', key1); + ctx.output('key2', key2); + ctx.output('key3', key3); + + // @ts-expect-error - this is void return + ctx.output('key4', key4); + // @ts-expect-error - this is void return + ctx.output('key5', key5); + }, + }), + ); mockedPermissionApi.authorizeConditional.mockResolvedValue([ { result: AuthorizeResult.ALLOW }, @@ -244,6 +278,25 @@ describe('NunjucksWorkflowRunner', () => { ); }); + it('should throw an error if the action has legacy zod schema and the input does not match', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + parameters: {}, + output: {}, + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-legacy-zod-validated-action', + }, + ], + }); + + await expect(runner.execute(task)).rejects.toThrow( + /Invalid input passed to action jest-legacy-zod-validated-action, instance requires property \"foo\"/, + ); + }); + it('should run the action when the zod validation passes', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', @@ -264,6 +317,26 @@ describe('NunjucksWorkflowRunner', () => { expect(fakeActionHandler).toHaveBeenCalledTimes(1); }); + it('should run the action when the zod validation passes with legacy zod', async () => { + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + parameters: {}, + output: {}, + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-legacy-zod-validated-action', + input: { foo: 1 }, + }, + ], + }); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledTimes(1); + }); + it('should run the action when the validation passes', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', diff --git a/plugins/scaffolder-node/src/actions/util.ts b/plugins/scaffolder-node/src/actions/util.ts index 64728d9cd0..0d7bcd6772 100644 --- a/plugins/scaffolder-node/src/actions/util.ts +++ b/plugins/scaffolder-node/src/actions/util.ts @@ -147,30 +147,33 @@ export const parseSchemas = (action: TemplateActionOptions) => { return { inputSchema: undefined, outputSchema: undefined }; } - if (isZodSchema(action.schema.input) && isZodSchema(action.schema.output)) { + if (isZodSchema(action.schema.input)) { return { inputSchema: zodToJsonSchema(action.schema.input), - outputSchema: zodToJsonSchema(action.schema.output), + outputSchema: isZodSchema(action.schema.output) + ? zodToJsonSchema(action.schema.output) + : undefined, }; } - if ( - isNativeZodSchema(action.schema.input) && - isNativeZodSchema(action.schema.output) - ) { + if (isNativeZodSchema(action.schema.input)) { const input = z.object( Object.fromEntries( Object.entries(action.schema.input).map(([k, v]) => [k, v(z)]), ), ); - const output = z.object( - Object.fromEntries( - Object.entries(action.schema.output).map(([k, v]) => [k, v(z)]), - ), - ); + return { inputSchema: zodToJsonSchema(input), - outputSchema: zodToJsonSchema(output), + outputSchema: isNativeZodSchema(action.schema.output) + ? zodToJsonSchema( + z.object( + Object.fromEntries( + Object.entries(action.schema.output).map(([k, v]) => [k, v(z)]), + ), + ), + ) + : undefined, }; }