teqo/tests/int/collectionAccessLockdown.int.spec.ts
Francisco Ignácio Fontoura Solla 0e4e6855dc Add the tooling gate: knip, CI, zero-warning lint, as-never ban
- knip (files/deps at error level; unused exports ledgered as warn) with
  config for Payload/Next entries; deleted the dead modules it confirmed:
  RecentlyVisited + UpcomingActionPlansCard + actionPlanUpcomingPreview,
  lazy-dialog helpers, tseZone, federalCandidateTotalsAggregate (test-only),
  unused ui kit files, one-shot rename script; pruned unused radix deps and
  typescript-eslint; added pg/@types/pg (pinned to drizzle's 8.10.2)
- GitHub Actions CI on the mirror: lint, typecheck, knip, unit, migrate, int
  against a Postgres 17 service
- eslint: unused-vars with ^_ ignore patterns (migrations args exempt),
  repo-wide ban on `as never` (no-restricted-syntax), lint gate at
  --max-warnings=0; fixed all 54 pre-existing warnings (dead imports,
  hooks deps, tracking-pixel img disable with impeccable config exception)
- tsconfig: include consolidated from 219 lines to 5 (wildcard covers all
  .next dist variants), noUnusedLocals on
- typed replacements for every as-never: hookFilledCreateData (actions
  create data completed by collection hooks), drizzle scopeColumn narrowing,
  tests' stub<T> helper (property-level checking kept, completeness waived)
- test infra: leased-consent generalization used by onda0 spec (shared
  leases over the stable consent keys; window race ledgered), purge fix
- docs: IMPROVE-CODE-QUALITY-PLAN.md tracker, TECH-DEBT.md ledger,
  TESTING.md safety-net map (package.json also gains the Phase 2
  build:election-aggregates script entry; the script lands next commit)
2026-07-23 23:06:12 -03:00

210 lines
7.3 KiB
TypeScript

// @vitest-environment node
import { getPayload, type Payload, type RequiredDataFromCollectionSlug } from 'payload'
import { beforeAll, describe, expect, it } from 'vitest'
import { createLeaderSupporterRecord } from '@/app/(campaign)/campanha/actions/leaderSupporter'
import config from '@/payload.config'
import { SUPPORTER_REGISTRATION_CONSENT_KEY } from '@/utilities/campaignConsent'
import { relationshipId } from '@/utilities/relationship'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
import { stub } from '../helpers/stub'
import {
SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
withLeasedConsent,
} from '../helpers/testDatabaseLease'
let payload: Payload
const campaignFixtures = installCampaignFixtures({
getPayload: () => payload,
setPayload: (nextPayload) => {
payload = nextPayload
},
})
/** Runs `operation` with the shared supporter-registration consent guaranteed present. */
const withSupporterRegistrationConsent = <Result>(
operation: () => Promise<Result>,
): Promise<Result> =>
withLeasedConsent(
payload,
{
consentKey: SUPPORTER_REGISTRATION_CONSENT_KEY,
leaseKey: SUPPORTER_REGISTRATION_CONSENT_LEASE_KEY,
},
operation,
)
/**
* Collections without explicit `access` fall back to Payload's "any
* authenticated user" default — which includes campaign users presenting the
* `campaign-token` JWT against `/api/*`. These tests pin the lockdown added to
* the CMS/PII collections: campaign users (even the coordinator) must be
* denied, admins keep working, and the public keeps read-only access where
* intended.
*/
describe('CMS/PII collection access lockdown', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it('denies campaign users any access to admin accounts (users)', async () => {
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
await expect(
payload.count({ collection: 'users', where: {}, user: coordinator, overrideAccess: false }),
).rejects.toThrow(/permissão|not allowed/i)
await expect(
payload.create({
collection: 'users',
data: { email: 'escalated@example.com', password: 'owned-by-attacker' },
user: coordinator,
overrideAccess: false,
}),
).rejects.toThrow(/permissão|not allowed/i)
})
it('lets admin users keep managing the users collection', async () => {
const fixtures = campaignFixtures()
const admin = await fixtures.createAdminUser()
const result = await payload.count({
collection: 'users',
where: { id: { equals: admin.id } },
user: admin,
overrideAccess: false,
})
expect(result.totalDocs).toBe(1)
})
it.each(['signature', 'subscription', 'consent'] as const)(
'denies campaign users and anonymous requests access to %s (PII/legal)',
async (collection) => {
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
await expect(
payload.count({ collection, where: {}, user: coordinator, overrideAccess: false }),
).rejects.toThrow(/permissão|not allowed/i)
await expect(payload.count({ collection, where: {}, overrideAccess: false })).rejects.toThrow(
/permissão|not allowed/i,
)
},
)
it('lets admin users keep reading signatures and subscriptions', async () => {
const fixtures = campaignFixtures()
const admin = await fixtures.createAdminUser()
await expect(
payload.count({ collection: 'signature', where: {}, user: admin, overrideAccess: false }),
).resolves.toMatchObject({ totalDocs: expect.any(Number) })
await expect(
payload.count({ collection: 'subscription', where: {}, user: admin, overrideAccess: false }),
).resolves.toMatchObject({ totalDocs: expect.any(Number) })
})
it.each(['post', 'tag', 'petition', 'media'] as const)(
'denies campaign users create access to public-content collection %s',
async (collection) => {
const coordinator = await campaignFixtures().createCampaignUser('coordinator')
await expect(
payload.create({
collection,
// Denied before validation: an empty payload stands in for required fields.
data: stub<RequiredDataFromCollectionSlug<typeof collection>>({}),
user: coordinator,
overrideAccess: false,
}),
).rejects.toThrow(/permissão|not allowed/i)
},
)
it.each(['post', 'tag', 'petition', 'media'] as const)(
'keeps anonymous read access to public-content collection %s',
async (collection) => {
await expect(
payload.count({ collection, where: {}, overrideAccess: false }),
).resolves.toMatchObject({ totalDocs: expect.any(Number) })
},
)
})
describe('leader supporter creation scope', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it('lets an engaged leader register a supporter in a linked municipality', async () => {
const fixtures = campaignFixtures()
const leaderAccount = await fixtures.createCampaignUser('leader')
const leaderContact = await fixtures.createContact()
const linked = await fixtures.getMunicipality()
await fixtures.createLeadership({
contact: leaderContact.id,
municipalities: [linked.id],
user: leaderAccount.id,
supportStatus: 'engajado',
})
const supporter = await withSupporterRegistrationConsent(() =>
createLeaderSupporterRecord(payload, leaderAccount, {
name: fixtures.value('Apoiador'),
phone: fixtures.phone(),
city: 'Salvador',
municipality: linked.id,
consentAccepted: true,
}),
)
fixtures.own('supporter', supporter.id)
expect(supporter.source).toBe('lideranca')
expect(relationshipId(supporter.createdBy)).toBe(leaderAccount.id)
expect(relationshipId(supporter.municipality)).toBe(linked.id)
})
it('rejects a supporter outside the leadership municipalities', async () => {
const fixtures = campaignFixtures()
const leaderAccount = await fixtures.createCampaignUser('leader')
const leaderContact = await fixtures.createContact()
const linked = await fixtures.getMunicipality()
const outside = await fixtures.getMunicipality()
await fixtures.createLeadership({
contact: leaderContact.id,
municipalities: [linked.id],
user: leaderAccount.id,
supportStatus: 'engajado',
})
await expect(
withSupporterRegistrationConsent(() =>
createLeaderSupporterRecord(payload, leaderAccount, {
name: fixtures.value('Apoiador'),
phone: fixtures.phone(),
city: 'Salvador',
municipality: outside.id,
consentAccepted: true,
}),
),
).rejects.toThrow('Você só pode cadastrar contatos nos municípios da sua liderança.')
})
it('rejects staff accounts on the leader-only contact tool', async () => {
const fixtures = campaignFixtures()
const advisor = await fixtures.createCampaignUser('advisor')
const municipality = await fixtures.getMunicipality()
await expect(
createLeaderSupporterRecord(payload, advisor, {
name: fixtures.value('Apoiador'),
phone: fixtures.phone(),
city: 'Salvador',
municipality: municipality.id,
consentAccepted: true,
}),
).rejects.toThrow('Somente lideranças podem cadastrar contatos por aqui.')
})
})