Tighten formActions guard: allowlist real exceptions and require call sites.

Also rethrow non-FormDataBoundaryError from the activity lifecycle id pre-parse.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-27 08:33:34 -03:00
parent 3c162c5250
commit 98c334785e
2 changed files with 14 additions and 6 deletions

View file

@ -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 }
}

View file

@ -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([])
})