diff --git a/packages/@n8n/api-types/src/dto/workflow-reviews/__tests__/create-workflow-review-request.dto.test.ts b/packages/@n8n/api-types/src/dto/workflow-reviews/__tests__/create-workflow-review-request.dto.test.ts index c8f88f59f64..a33e30a4275 100644 --- a/packages/@n8n/api-types/src/dto/workflow-reviews/__tests__/create-workflow-review-request.dto.test.ts +++ b/packages/@n8n/api-types/src/dto/workflow-reviews/__tests__/create-workflow-review-request.dto.test.ts @@ -65,6 +65,42 @@ describe('CreateWorkflowReviewRequestDto', () => { expect(result.data?.workflows[0].workflowVersionDescription).toBe(workflowVersionDescription); }); + test('should trim the review description', () => { + const result = CreateWorkflowReviewRequestDto.safeParse({ + ...base, + description: ' Please take a look ', + workflows: pinnedWorkflow, + }); + + expect(result.success).toBe(true); + expect(result.data?.description).toBe('Please take a look'); + }); + + test.each([ + { name: 'an empty', description: '' }, + { name: 'a whitespace-only', description: ' ' }, + ])('should reduce $name review description to an empty string', ({ description }) => { + const result = CreateWorkflowReviewRequestDto.safeParse({ + ...base, + description, + workflows: pinnedWorkflow, + }); + + expect(result.success).toBe(true); + expect(result.data?.description).toBe(''); + }); + + test('should reject a review description longer than 512 characters', () => { + const result = CreateWorkflowReviewRequestDto.safeParse({ + ...base, + description: 'a'.repeat(513), + workflows: pinnedWorkflow, + }); + + expect(result.success).toBe(false); + expect(result.error?.issues[0].path).toEqual(['description']); + }); + test('should reject a version description longer than 2048 characters', () => { const result = CreateWorkflowReviewRequestDto.safeParse({ ...base, diff --git a/packages/@n8n/api-types/src/dto/workflow-reviews/create-workflow-review-request.dto.ts b/packages/@n8n/api-types/src/dto/workflow-reviews/create-workflow-review-request.dto.ts index 5187db2f5e8..611fbeae8f9 100644 --- a/packages/@n8n/api-types/src/dto/workflow-reviews/create-workflow-review-request.dto.ts +++ b/packages/@n8n/api-types/src/dto/workflow-reviews/create-workflow-review-request.dto.ts @@ -9,7 +9,8 @@ import { Z } from '../../zod-class'; export class CreateWorkflowReviewRequestDto extends Z.class({ title: z.string().trim().min(1).max(128), - description: z.string().max(512).optional(), + // An empty/whitespace string is stored as no description + description: z.string().trim().max(512).optional(), workflows: z .array( z.object({ diff --git a/packages/cli/src/modules/workflow-reviews.ee/__tests__/workflow-review-requests.controller.integration.test.ts b/packages/cli/src/modules/workflow-reviews.ee/__tests__/workflow-review-requests.controller.integration.test.ts index 1631efbf436..db9d4d0795f 100644 --- a/packages/cli/src/modules/workflow-reviews.ee/__tests__/workflow-review-requests.controller.integration.test.ts +++ b/packages/cli/src/modules/workflow-reviews.ee/__tests__/workflow-review-requests.controller.integration.test.ts @@ -390,6 +390,47 @@ describe('POST /workflow-review-requests', () => { }).expect(400); }); + test('trims the review description on create', async () => { + const { workflow, versionId } = await createReviewableWorkflow(); + + await postReview(ownerAgent, { + title: 'Please review my workflow', + description: ' It is ready ', + workflows: [ + { + workflowId: workflow.id, + workflowVersionId: versionId, + workflowVersionName: 'Release candidate', + }, + ], + }).expect(201); + + const requests = await requestRepository.find(); + expect(requests[0].description).toBe('It is ready'); + }); + + test.each([ + { name: 'an empty', description: '' }, + { name: 'a whitespace-only', description: ' ' }, + ])('stores $name review description as null on create', async ({ description }) => { + const { workflow, versionId } = await createReviewableWorkflow(); + + await postReview(ownerAgent, { + title: 'Please review my workflow', + description, + workflows: [ + { + workflowId: workflow.id, + workflowVersionId: versionId, + workflowVersionName: 'Release candidate', + }, + ], + }).expect(201); + + const requests = await requestRepository.find(); + expect(requests[0].description).toBeNull(); + }); + test('returns 400 for a description exceeding 512 characters', async () => { const { workflow, versionId } = await createReviewableWorkflow(); @@ -1261,7 +1302,10 @@ describe('POST /workflow-review-requests/:workflowReviewRequestId/update-version }); }); - test('clears the review description when an empty string is sent', async () => { + test.each([ + { name: 'an empty string', description: '' }, + { name: 'a whitespace-only string', description: ' ' }, + ])('clears the review description when $name is sent', async ({ description }) => { const { workflow, versionId } = await createReviewableWorkflow(); const request = await seedOpenRequest(workflow.id, versionId, owner, ownerProject.id, { description: 'Original review description', @@ -1273,7 +1317,7 @@ describe('POST /workflow-review-requests/:workflowReviewRequestId/update-version workflowId: workflow.id, workflowVersionId: versionId, workflowVersionName: 'Release candidate', - description: '', + description, }) .expect(200); diff --git a/packages/cli/src/modules/workflow-reviews.ee/workflow-review-request.service.ts b/packages/cli/src/modules/workflow-reviews.ee/workflow-review-request.service.ts index 473b430ef5c..ad4865fcc58 100644 --- a/packages/cli/src/modules/workflow-reviews.ee/workflow-review-request.service.ts +++ b/packages/cli/src/modules/workflow-reviews.ee/workflow-review-request.service.ts @@ -370,7 +370,7 @@ export class WorkflowReviewRequestService { { projectId: project.id, title: dto.title, - description: dto.description ?? null, + description: normalizeDescription(dto.description) ?? null, createdById: user.id, }, ctx,