Test Cases: Cover Image, Aggregates & Public Campaign Page (#661 / #662)

Module: Fundraising & Campaigns — Cover Image, Aggregates, Public Page

Endpoints: POST /api/billing/admin/campaigns/:id/cover-image · GET /api/billing/admin/campaigns / GET .../campaigns/:id (aggregates) · GET /api/public/:tenantSlug/campaigns/:slug (public, no auth).

Frontend: admin campaign create/edit forms (cover image field), admin campaign detail (progress bar), public campaign page /:tenantSlug/campaigns/:slug.

Prerequisites

Test Cases

TC-FUND-COVER-01: Cover Image Upload — Size & Type Guards (#661)

StepActionExpected Result
1Upload a cover image > 5MB.POST /api/billing/admin/campaigns/:id/cover-image400 "Image too large. Please upload under 5 MB."
2Upload a non-image type (e.g. a PDF or GIF disguised as data URI).400 "Invalid image type. Use JPG, PNG, or WEBP."
3Upload a valid PNG/JPG/JPEG/WEBP under 5MB.200. FundraisingCampaign.coverImage persists; response is the fully-serialized campaign with the new cover image.

TC-FUND-COVER-02: Edit-Flow Isolation on Upload Failure

StepActionExpected Result
1Edit a campaign's text fields (title/description) AND attach an oversized cover image in the same edit action.The cover-image upload fails (400, per TC-FUND-COVER-01).
2Reload the campaign.The text-field edits SURVIVED and persisted even though the image upload failed — the two are isolated, not an all-or-nothing transaction.

TC-FUND-AGG-01: List/Detail Aggregates (#662)

StepActionExpected Result
1Call GET /api/billing/admin/campaigns (list) and GET .../campaigns/:id (detail).Both responses include raisedAmount = SUM(APPROVED donation amounts) and pendingProofCount = COUNT(PENDING donations).
2Call create/update/status/archive endpoints.These may OMIT the aggregate fields — treat as optional on non-list/detail responses.
3Open the admin campaign detail page.A progress bar renders showing raised vs goal, driven by raisedAmount.
Known caveat: refunding an APPROVED donation elsewhere does NOT reduce raisedAmount — there is no un-approve transition. Document, do not assert a decrease.

TC-FUND-PUBLIC-01: Public Donor List — No PII Leak

StepActionExpected Result
1Call GET /api/public/:tenantSlug/campaigns/:slug logged out.Only APPROVED donations feed raisedAmount/donationCount/donor list. Donor list items are shaped {donorName, amount, createdAt}never donorEmail or donorMemberId.

TC-FUND-PUBLIC-02: Anonymous Donations Null Out donorName

StepActionExpected Result
1Via direct API call, donate with isAnonymous: true and a real donorName in the body, then admin-approve it.Donation approved as usual.
2Fetch the public campaign page.The donor list entry for this donation has donorName: null — REGARDLESS of the name that was submitted on the row.
Known caveat: isAnonymous has no frontend checkbox today — set it via direct API call only. The public page still honours it correctly.

TC-FUND-PUBLIC-03: Members-Only Visibility Soft-Gate

StepActionExpected Result
1On a campaign with visibility: members-only, call the public endpoint logged out.200 with {campaign:{gated:true,...}} — no donor list leaked, but no hard 401/403 either.
2Call the same endpoint as a logged-in member.Full payload returned (donor list included per TC-FUND-PUBLIC-01).

TC-FUND-PUBLIC-04: DRAFT / ARCHIVED Campaigns — No Existence Leak

StepActionExpected Result
1Call the public endpoint for a DRAFT campaign's slug.404 {code:"CAMPAIGN_NOT_FOUND"} — same shape as a nonexistent slug (no existence leak).
2Repeat for an ARCHIVED campaign's slug.Same 404 CAMPAIGN_NOT_FOUND.