From 98c334785e9001b3d87ae6d8f0e49bdee103574b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Mon, 27 Jul 2026 08:33:34 -0300 Subject: [PATCH] Tighten formActions guard: allowlist real exceptions and require call sites. Also rethrow non-FormDataBoundaryError from the activity lifecycle id pre-parse. --- .../atividades/[slug]/lifecycleFormActions.ts | 5 +++-- tests/unit/codebaseConventions.unit.spec.ts | 15 +++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/app/(campaign)/campanha/(app)/atividades/[slug]/lifecycleFormActions.ts b/src/app/(campaign)/campanha/(app)/atividades/[slug]/lifecycleFormActions.ts index cba0b000..401cbd29 100644 --- a/src/app/(campaign)/campanha/(app)/atividades/[slug]/lifecycleFormActions.ts +++ b/src/app/(campaign)/campanha/(app)/atividades/[slug]/lifecycleFormActions.ts @@ -1,7 +1,7 @@ 'use server' import { cancelActivity, markActivityRealized } from '@/app/(campaign)/campanha/actions/activity' -import { requiredRelationshipFormValue } from '@/lib/formData' +import { FormDataBoundaryError, requiredRelationshipFormValue } from '@/lib/formData' import type { Activity } from '@/payload-types' import { runCampaignRedirectFormAction, @@ -21,7 +21,8 @@ const setActivityLifecycleFormAction = async ( let activityId: number try { activityId = requiredRelationshipFormValue(formData, 'id') - } catch { + } catch (error) { + if (!(error instanceof FormDataBoundaryError)) throw error return { message: ACTIVITY_ID_MISSING_MESSAGE } } diff --git a/tests/unit/codebaseConventions.unit.spec.ts b/tests/unit/codebaseConventions.unit.spec.ts index 32a1e567..8a608161 100644 --- a/tests/unit/codebaseConventions.unit.spec.ts +++ b/tests/unit/codebaseConventions.unit.spec.ts @@ -145,6 +145,11 @@ describe('campaign formActions convention', () => { // by useOptimistic in ActivityTaskChecklist — not a (state, formData) ladder, // nothing to route through the shared wrapper. 'src/app/(campaign)/campanha/(app)/atividades/[slug]/taskActions.ts', + // Custom unique-violation → fieldErrors + async duplicate-title fallback + // that links to the existing activity — policy the wrappers don't grow for. + 'src/app/(campaign)/campanha/(app)/atividades/formActions.ts', + // Flattens field errors into message-only states for inline detail controls. + 'src/app/(campaign)/campanha/(app)/apoiadores/[id]/formActions.ts', ]) it('routes every *FormActions / taskActions ladder through a shared wrapper', () => { @@ -152,10 +157,12 @@ describe('campaign formActions convention', () => { .filter((file) => isGuardedActionFile(basename(file))) .map(repoPath) .filter((path) => !allowlist.has(path)) - .filter( - (path) => - !/runCampaign(Redirect)?FormAction/.test(readFileSync(resolve(repoRoot, path), 'utf8')), - ) + .filter((path) => { + // Require a real call site — a comment naming the wrappers must not pass + // (the documented exceptions above used to hide behind that loophole). + const source = readFileSync(resolve(repoRoot, path), 'utf8') + return !/\brunCampaign(Redirect)?FormAction\s*\(/.test(source) + }) expect(offenders, 'wrap with runCampaignFormAction/runCampaignRedirectFormAction').toEqual([]) })