feat: db:seed:minimal — minimal synthetic campaign DB for agents/CI
This commit is contained in:
parent
571872f1a7
commit
6ac26404d2
5 changed files with 488 additions and 1 deletions
|
|
@ -8,7 +8,19 @@ Three layers, all local-database-only (see `local-database` skill; tests refuse
|
|||
- **Integration** (`tests/int/**/*.int.spec.ts`, 48 specs): real Payload + Postgres (`teqo_test`); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Shared consent rows with stable keys are leased (`tests/helpers/testDatabaseLease.ts`), never owned.
|
||||
- **E2E** (`tests/e2e`, Playwright, 7 specs): smoke-level; most campaign behavior is pinned at the int layer.
|
||||
|
||||
CI (`.github/workflows/ci.yml`) runs lint (zero warnings), typecheck, knip, unit, migrate, int against a Postgres 17 service.
|
||||
CI (`.github/workflows/ci-pr.yml`) runs lint (zero warnings), typecheck, knip, unit, migrate, **`db:seed:minimal`**, int and build against a Postgres 17 service.
|
||||
|
||||
## Minimal test database (`pnpm db:seed:minimal`)
|
||||
|
||||
The integration suite and any agent/Cloud environment boot from a **minimal synthetic database**: schema from `pnpm migrate` (which seeds the 435-município catalog) plus `pnpm db:seed:minimal` ([`scripts/seed-minimal.mjs`](../scripts/seed-minimal.mjs)), an idempotent upsert of the rows the app refuses to run without:
|
||||
|
||||
- the four fail-closed `Consent` keys (`lideranca-autopreenchimento`, `apoiador-cadastro`, `apoiador-intencao-voto`, `whatsapp-inscricao`);
|
||||
- one `campaignUser` per role (synthetic `@teqo.invalid` emails / `719999999Xx` phones);
|
||||
- deterministic strategy pins (`priority` + `expectedVotes`) on 5 municípios (`salvador-ze-1`, `camacari`, `feira-de-santana`, `vitoria-da-conquista`, `itabuna`) — rows themselves come from the migration-seeded catalog;
|
||||
- 1 `organization`, 1 `stateDeputy`, 2 `leadership`, 3 `supporter` (all synthetic);
|
||||
- the `campaignGoals` global default.
|
||||
|
||||
The content contract is data in [`scripts/lib/seed-minimal-manifest.mjs`](../scripts/lib/seed-minimal-manifest.mjs), pinned by `tests/unit/seedMinimalManifest.unit.spec.ts`. **Contract: a delivery that adds a migration, collection, Consent key or required boot data must update the manifest in the same PR** — PR CI runs migrate → seed → int, so a broken seed fails the gate. The seed refuses non-local `DATABASE_URL` (same `assertLocalDatabase` guard family as the other seeds); agents and Cloud environments never set `ALLOW_REMOTE_DB`.
|
||||
|
||||
## Safety Net Map
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"db:seed:tse": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-tse-results.mjs",
|
||||
"db:seed:consent": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-consent.mjs",
|
||||
"db:seed:projecao": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/import-projecao.mjs",
|
||||
"db:seed:minimal": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-minimal.mjs",
|
||||
"build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs",
|
||||
"build:election-aggregates": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/build-election-aggregates.mjs",
|
||||
"build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs",
|
||||
|
|
|
|||
148
scripts/lib/seed-minimal-manifest.mjs
Normal file
148
scripts/lib/seed-minimal-manifest.mjs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* Canonical content manifest of `pnpm db:seed:minimal` (pure data — imported by
|
||||
* the seed script AND by the unit pin `tests/unit/seedMinimalManifest.unit.spec.ts`,
|
||||
* which is the guardrail against a schema/migration delivery that forgets to
|
||||
* update the minimal DB contract in the same PR).
|
||||
*
|
||||
* Everything here is synthetic (`.invalid` emails, 719999999Xx phones) and
|
||||
* idempotent: the seed upserts by stable keys (slug/email/username/phone/key).
|
||||
* Municipalities are NOT created — migrations seed all 435 catalog rows; the
|
||||
* seed only pins deterministic operational fields on a handful of them.
|
||||
*/
|
||||
|
||||
export const MINIMAL_SEED_VERSION = 1
|
||||
|
||||
/** Consent keys that MUST exist after the seed (the four fail-closed keys). */
|
||||
export const MINIMAL_CONSENT_KEYS = [
|
||||
'lideranca-autopreenchimento',
|
||||
'apoiador-cadastro',
|
||||
'apoiador-intencao-voto',
|
||||
'whatsapp-inscricao',
|
||||
]
|
||||
|
||||
export const MINIMAL_CAMPAIGN_GOALS = { stateGoal: 150_000, margin: 10, baseYear: 2022 }
|
||||
|
||||
export const MINIMAL_CAMPAIGN_USERS = [
|
||||
{
|
||||
role: 'coordinator',
|
||||
name: 'Seed Coordenador',
|
||||
email: 'seed-coordenador@teqo.invalid',
|
||||
password: 'teqo-minimal-seed-password',
|
||||
},
|
||||
{
|
||||
role: 'advisor',
|
||||
name: 'Seed Assessor',
|
||||
email: 'seed-assessor@teqo.invalid',
|
||||
password: 'teqo-minimal-seed-password',
|
||||
},
|
||||
{
|
||||
role: 'candidate',
|
||||
name: 'Seed Candidato',
|
||||
email: 'seed-candidato@teqo.invalid',
|
||||
password: 'teqo-minimal-seed-password',
|
||||
},
|
||||
{
|
||||
role: 'leader',
|
||||
name: 'Seed Liderança',
|
||||
username: '71999999990',
|
||||
phone: '71999999990',
|
||||
password: 'teqo-minimal-seed-password',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Municipality slugs the seed pins deterministic strategy fields on. They must
|
||||
* exist in the catalog (the unit test proves it). Salvador ZE 1 + Camaçari are
|
||||
* required by name in the Onda 0 plan; the other three spread across TIs.
|
||||
*/
|
||||
export const MINIMAL_MUNICIPALITIES = [
|
||||
{
|
||||
slug: 'salvador-ze-1',
|
||||
priority: 'alta',
|
||||
expectedVotes: { pessimistic: 4000, central: 5000, optimistic: 6500 },
|
||||
},
|
||||
{
|
||||
slug: 'camacari',
|
||||
priority: 'alta',
|
||||
expectedVotes: { pessimistic: 2500, central: 3200, optimistic: 4000 },
|
||||
},
|
||||
{
|
||||
slug: 'feira-de-santana',
|
||||
priority: 'normal',
|
||||
expectedVotes: { pessimistic: 2000, central: 2600, optimistic: 3300 },
|
||||
},
|
||||
{
|
||||
slug: 'vitoria-da-conquista',
|
||||
priority: 'normal',
|
||||
expectedVotes: { pessimistic: 2200, central: 2800, optimistic: 3600 },
|
||||
},
|
||||
{
|
||||
slug: 'itabuna',
|
||||
priority: 'normal',
|
||||
expectedVotes: { pessimistic: 1200, central: 1600, optimistic: 2100 },
|
||||
},
|
||||
]
|
||||
|
||||
export const MINIMAL_ORGANIZATION = {
|
||||
name: 'Seed Sindicato dos Trabalhadores',
|
||||
slug: 'seed-sindicato-dos-trabalhadores',
|
||||
kind: 'sindicato',
|
||||
municipalitySlugs: ['camacari'],
|
||||
}
|
||||
|
||||
export const MINIMAL_STATE_DEPUTY = {
|
||||
name: 'Seed Deputada Estadual',
|
||||
slug: 'seed-deputada-estadual',
|
||||
party: 'PT',
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaderships link a synthetic Contact (unique by phone) to municipalities.
|
||||
* `consentKey` references one of MINIMAL_CONSENT_KEYS.
|
||||
*/
|
||||
export const MINIMAL_LEADERSHIPS = [
|
||||
{
|
||||
contactName: 'Seed Liderança Um',
|
||||
contactPhone: '71999999991',
|
||||
municipalitySlugs: ['salvador-ze-1'],
|
||||
supportStatus: 'engajado',
|
||||
exclusive: true,
|
||||
consentKey: 'lideranca-autopreenchimento',
|
||||
},
|
||||
{
|
||||
contactName: 'Seed Liderança Dois',
|
||||
contactPhone: '71999999992',
|
||||
municipalitySlugs: ['camacari', 'itabuna'],
|
||||
supportStatus: 'a_abordar',
|
||||
exclusive: false,
|
||||
consentKey: 'lideranca-autopreenchimento',
|
||||
},
|
||||
]
|
||||
|
||||
export const MINIMAL_SUPPORTERS = [
|
||||
{
|
||||
contactName: 'Seed Apoiador Um',
|
||||
contactPhone: '71999999993',
|
||||
municipalitySlug: 'salvador-ze-1',
|
||||
voteIntention: 'certo',
|
||||
source: 'manual',
|
||||
consentKey: 'apoiador-cadastro',
|
||||
voteIntentionConsentKey: 'apoiador-intencao-voto',
|
||||
},
|
||||
{
|
||||
contactName: 'Seed Apoiador Dois',
|
||||
contactPhone: '71999999994',
|
||||
municipalitySlug: 'camacari',
|
||||
voteIntention: 'indeciso',
|
||||
source: 'evento',
|
||||
consentKey: 'apoiador-cadastro',
|
||||
voteIntentionConsentKey: 'apoiador-intencao-voto',
|
||||
},
|
||||
{
|
||||
contactName: 'Seed Apoiador Três',
|
||||
contactPhone: '71999999995',
|
||||
municipalitySlug: 'feira-de-santana',
|
||||
source: 'lideranca',
|
||||
consentKey: 'apoiador-cadastro',
|
||||
},
|
||||
]
|
||||
240
scripts/seed-minimal.mjs
Normal file
240
scripts/seed-minimal.mjs
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
/**
|
||||
* Minimal synthetic campaign database for agents, Cursor Cloud and PR CI.
|
||||
*
|
||||
* Populates ONLY the rows the app refuses to boot/test without — everything is
|
||||
* synthetic (`.invalid` emails, 719999999Xx phones) and idempotent (upsert by
|
||||
* key/slug/email/username/phone). Municipalities are NOT created: migrations
|
||||
* seed the full 435-row catalog; this script only pins deterministic strategy
|
||||
* fields on the five listed in the manifest.
|
||||
*
|
||||
* The content contract lives in `scripts/lib/seed-minimal-manifest.mjs` and is
|
||||
* pinned by `tests/unit/seedMinimalManifest.unit.spec.ts`. CONTRACT: a delivery
|
||||
* that adds a migration, collection, Consent key or required boot data must
|
||||
* update the manifest in the same PR — PR CI runs migrate → seed:minimal →
|
||||
* test:int, so a broken seed fails the gate.
|
||||
*
|
||||
* Safety: refuses non-local DATABASE_URL unless ALLOW_REMOTE_DB=true. Agents
|
||||
* and Cloud environments must never set that override.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm migrate && pnpm db:seed:minimal
|
||||
*/
|
||||
|
||||
import { getPayload } from 'payload'
|
||||
import { loadCliEnv } from './lib/cli.mjs'
|
||||
import { assertLocalDatabase } from './assert-local-database.mjs'
|
||||
import {
|
||||
MINIMAL_CAMPAIGN_GOALS,
|
||||
MINIMAL_CAMPAIGN_USERS,
|
||||
MINIMAL_CONSENT_KEYS,
|
||||
MINIMAL_LEADERSHIPS,
|
||||
MINIMAL_MUNICIPALITIES,
|
||||
MINIMAL_ORGANIZATION,
|
||||
MINIMAL_STATE_DEPUTY,
|
||||
MINIMAL_SUPPORTERS,
|
||||
} from './lib/seed-minimal-manifest.mjs'
|
||||
|
||||
loadCliEnv()
|
||||
|
||||
assertLocalDatabase(
|
||||
'seed:minimal',
|
||||
'This script upserts synthetic rows. It must never run against stage/prod.',
|
||||
)
|
||||
|
||||
const config = (await import('../src/payload.config.ts')).default
|
||||
const { provisionOnda0ConsentAndPrivacy } = await import('../src/utilities/onda0Provision.ts')
|
||||
const { hashConsentContent } = await import('../src/utilities/consentContentHash.ts')
|
||||
const { WHATSAPP_SUBSCRIPTION_CONSENT_KEY } = await import('../src/lib/campaignConsentKeys.ts')
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const minimalLexical = (text) => ({
|
||||
root: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ type: 'text', text, version: 1 }],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
],
|
||||
direction: null,
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const findOne = async (collection, where) => {
|
||||
const result = await payload.find({ collection, where, depth: 0, limit: 1, pagination: false })
|
||||
return result.docs[0]
|
||||
}
|
||||
|
||||
const upsert = async (collection, where, data, label) => {
|
||||
const existing = await findOne(collection, where)
|
||||
if (existing) {
|
||||
console.log(`[seed:minimal] ok (exists) ${label}`)
|
||||
return existing
|
||||
}
|
||||
const created = await payload.create({ collection, data, depth: 0 })
|
||||
console.log(`[seed:minimal] created ${label}`)
|
||||
return created
|
||||
}
|
||||
|
||||
const municipalityIdBySlug = async (slug) => {
|
||||
const doc = await findOne('municipality', { slug: { equals: slug } })
|
||||
if (!doc) {
|
||||
throw new Error(`Municipality "${slug}" missing — run pnpm migrate before pnpm db:seed:minimal.`)
|
||||
}
|
||||
return doc.id
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Consent keys (fail-closed flows refuse to run without them). The Onda 0
|
||||
// provision covers three of the four keys + the privacy-policy global;
|
||||
// whatsapp-inscricao is upserted here with the same minimal text.
|
||||
await provisionOnda0ConsentAndPrivacy(payload)
|
||||
const whatsappText = minimalLexical(
|
||||
'Autorizo o recebimento de mensagens da campanha via WhatsApp (texto sintético do seed mínimo).',
|
||||
)
|
||||
await upsert(
|
||||
'consent',
|
||||
{ key: { equals: WHATSAPP_SUBSCRIPTION_CONSENT_KEY } },
|
||||
{ key: WHATSAPP_SUBSCRIPTION_CONSENT_KEY, text: whatsappText },
|
||||
`consent ${WHATSAPP_SUBSCRIPTION_CONSENT_KEY}`,
|
||||
)
|
||||
const consentIdByKey = new Map()
|
||||
for (const key of MINIMAL_CONSENT_KEYS) {
|
||||
const doc = await findOne('consent', { key: { equals: key } })
|
||||
if (!doc) throw new Error(`Consent "${key}" missing after provisioning.`)
|
||||
consentIdByKey.set(key, doc)
|
||||
}
|
||||
|
||||
// 2. Campaign users, one per role (synthetic credentials for test DBs only).
|
||||
for (const user of MINIMAL_CAMPAIGN_USERS) {
|
||||
await upsert(
|
||||
'campaignUser',
|
||||
user.email
|
||||
? { email: { equals: user.email } }
|
||||
: { username: { equals: user.username } },
|
||||
{ ...user },
|
||||
`campaignUser ${user.role} (${user.email ?? user.username})`,
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Municipality strategy pins (rows come from the migration-seeded catalog).
|
||||
for (const entry of MINIMAL_MUNICIPALITIES) {
|
||||
const id = await municipalityIdBySlug(entry.slug)
|
||||
await payload.update({
|
||||
collection: 'municipality',
|
||||
id,
|
||||
data: { priority: entry.priority, expectedVotes: entry.expectedVotes },
|
||||
depth: 0,
|
||||
})
|
||||
console.log(`[seed:minimal] pinned municipality ${entry.slug}`)
|
||||
}
|
||||
|
||||
// 4. Organization + state deputy.
|
||||
const organization = await upsert(
|
||||
'organization',
|
||||
{ slug: { equals: MINIMAL_ORGANIZATION.slug } },
|
||||
{
|
||||
name: MINIMAL_ORGANIZATION.name,
|
||||
slug: MINIMAL_ORGANIZATION.slug,
|
||||
kind: MINIMAL_ORGANIZATION.kind,
|
||||
municipalities: await Promise.all(
|
||||
MINIMAL_ORGANIZATION.municipalitySlugs.map(municipalityIdBySlug),
|
||||
),
|
||||
},
|
||||
`organization ${MINIMAL_ORGANIZATION.slug}`,
|
||||
)
|
||||
const stateDeputy = await upsert(
|
||||
'stateDeputy',
|
||||
{ slug: { equals: MINIMAL_STATE_DEPUTY.slug } },
|
||||
{ ...MINIMAL_STATE_DEPUTY },
|
||||
`stateDeputy ${MINIMAL_STATE_DEPUTY.slug}`,
|
||||
)
|
||||
|
||||
// 5. Leaderships (contact upserted by phone; leadership unique by contact).
|
||||
for (const entry of MINIMAL_LEADERSHIPS) {
|
||||
const contact = await upsert(
|
||||
'contact',
|
||||
{ phone: { equals: entry.contactPhone } },
|
||||
{ name: entry.contactName, phone: entry.contactPhone, state: 'BA' },
|
||||
`contact ${entry.contactPhone}`,
|
||||
)
|
||||
const consent = consentIdByKey.get(entry.consentKey)
|
||||
await upsert(
|
||||
'leadership',
|
||||
{ contact: { equals: contact.id } },
|
||||
{
|
||||
contact: contact.id,
|
||||
municipalities: await Promise.all(entry.municipalitySlugs.map(municipalityIdBySlug)),
|
||||
organizations: [organization.id],
|
||||
stateDeputies: [stateDeputy.id],
|
||||
supportStatus: entry.supportStatus,
|
||||
exclusive: entry.exclusive,
|
||||
consent: consent.id,
|
||||
consentContentHash: hashConsentContent(consent.text ?? null),
|
||||
consentedAt: new Date().toISOString(),
|
||||
},
|
||||
`leadership ${entry.contactName}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 6. Supporters (unique by contact+municipality).
|
||||
for (const entry of MINIMAL_SUPPORTERS) {
|
||||
const contact = await upsert(
|
||||
'contact',
|
||||
{ phone: { equals: entry.contactPhone } },
|
||||
{ name: entry.contactName, phone: entry.contactPhone, state: 'BA' },
|
||||
`contact ${entry.contactPhone}`,
|
||||
)
|
||||
const municipalityID = await municipalityIdBySlug(entry.municipalitySlug)
|
||||
const consent = consentIdByKey.get(entry.consentKey)
|
||||
const voteIntentionConsent = entry.voteIntentionConsentKey
|
||||
? consentIdByKey.get(entry.voteIntentionConsentKey)
|
||||
: undefined
|
||||
await upsert(
|
||||
'supporter',
|
||||
{ and: [{ contact: { equals: contact.id } }, { municipality: { equals: municipalityID } }] },
|
||||
{
|
||||
contact: contact.id,
|
||||
municipality: municipalityID,
|
||||
source: entry.source,
|
||||
...(entry.voteIntention ? { voteIntention: entry.voteIntention } : {}),
|
||||
consent: consent.id,
|
||||
consentContentHash: hashConsentContent(consent.text ?? null),
|
||||
consentedAt: new Date().toISOString(),
|
||||
...(voteIntentionConsent
|
||||
? {
|
||||
voteIntentionConsent: voteIntentionConsent.id,
|
||||
voteIntentionConsentContentHash: hashConsentContent(
|
||||
voteIntentionConsent.text ?? null,
|
||||
),
|
||||
voteIntentionConsentedAt: new Date().toISOString(),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
`supporter ${entry.contactName}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 7. Campaign goals global (E8 defaults).
|
||||
await payload.updateGlobal({
|
||||
slug: 'campaignGoals',
|
||||
data: { ...MINIMAL_CAMPAIGN_GOALS },
|
||||
overrideAccess: true,
|
||||
})
|
||||
console.log('[seed:minimal] pinned campaignGoals global')
|
||||
|
||||
console.log('[seed:minimal] OK — minimal synthetic database ready.')
|
||||
} catch (error) {
|
||||
console.error(`\n[seed:minimal] ${error instanceof Error ? error.message : String(error)}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
86
tests/unit/seedMinimalManifest.unit.spec.ts
Normal file
86
tests/unit/seedMinimalManifest.unit.spec.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
// @ts-expect-error — plain .mjs data module, intentionally untyped
|
||||
import {
|
||||
MINIMAL_CAMPAIGN_GOALS,
|
||||
MINIMAL_CAMPAIGN_USERS,
|
||||
MINIMAL_CONSENT_KEYS,
|
||||
MINIMAL_LEADERSHIPS,
|
||||
MINIMAL_MUNICIPALITIES,
|
||||
MINIMAL_ORGANIZATION,
|
||||
MINIMAL_STATE_DEPUTY,
|
||||
MINIMAL_SUPPORTERS,
|
||||
} from '../../scripts/lib/seed-minimal-manifest.mjs'
|
||||
|
||||
import {
|
||||
CAMPAIGN_INVITE_CONSENT_KEY,
|
||||
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
||||
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
|
||||
WHATSAPP_SUBSCRIPTION_CONSENT_KEY,
|
||||
} from '@/lib/campaignConsentKeys'
|
||||
import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog'
|
||||
|
||||
/**
|
||||
* Guardrail for the "migration/schema without seed update" miss pattern: the
|
||||
* minimal-DB contract is data, pinned here, so a delivery that changes a
|
||||
* consent key, renames a municipality slug or drops a seeded role fails the
|
||||
* fast unit gate instead of only the slower PR-CI integration job.
|
||||
*/
|
||||
describe('seed-minimal manifest', () => {
|
||||
it('covers every fail-closed Consent key the app resolves at runtime', () => {
|
||||
expect([...MINIMAL_CONSENT_KEYS].sort()).toEqual(
|
||||
[
|
||||
CAMPAIGN_INVITE_CONSENT_KEY,
|
||||
SUPPORTER_REGISTRATION_CONSENT_KEY,
|
||||
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
|
||||
WHATSAPP_SUBSCRIPTION_CONSENT_KEY,
|
||||
].sort(),
|
||||
)
|
||||
})
|
||||
|
||||
it('seeds exactly one campaign user per role, with synthetic credentials only', () => {
|
||||
const roles = MINIMAL_CAMPAIGN_USERS.map((user: { role: string }) => user.role).sort()
|
||||
expect(roles).toEqual(['advisor', 'candidate', 'coordinator', 'leader'])
|
||||
for (const user of MINIMAL_CAMPAIGN_USERS) {
|
||||
if (user.email) expect(user.email).toMatch(/@teqo\.invalid$/)
|
||||
if (user.username) expect(user.username).toMatch(/^719999999\d\d$/)
|
||||
}
|
||||
})
|
||||
|
||||
it('pins only slugs that exist in the municipality catalog', () => {
|
||||
expect(MINIMAL_MUNICIPALITIES.length).toBeGreaterThanOrEqual(3)
|
||||
expect(MINIMAL_MUNICIPALITIES.length).toBeLessThanOrEqual(5)
|
||||
const slugs = MINIMAL_MUNICIPALITIES.map((entry: { slug: string }) => entry.slug)
|
||||
expect(slugs).toContain('salvador-ze-1')
|
||||
expect(slugs).toContain('camacari')
|
||||
for (const slug of slugs) {
|
||||
expect(getMunicipalityCatalogEntry(slug), `slug ${slug}`).toBeDefined()
|
||||
}
|
||||
for (const entry of MINIMAL_MUNICIPALITIES) {
|
||||
const { pessimistic, central, optimistic } = entry.expectedVotes
|
||||
expect(pessimistic).toBeLessThanOrEqual(central)
|
||||
expect(central).toBeLessThanOrEqual(optimistic)
|
||||
}
|
||||
})
|
||||
|
||||
it('references only pinned municipality slugs from org/leadership/supporter entries', () => {
|
||||
const pinned = new Set(MINIMAL_MUNICIPALITIES.map((entry: { slug: string }) => entry.slug))
|
||||
for (const slug of MINIMAL_ORGANIZATION.municipalitySlugs) expect(pinned.has(slug)).toBe(true)
|
||||
for (const leadership of MINIMAL_LEADERSHIPS) {
|
||||
for (const slug of leadership.municipalitySlugs) expect(pinned.has(slug)).toBe(true)
|
||||
expect(MINIMAL_CONSENT_KEYS).toContain(leadership.consentKey)
|
||||
}
|
||||
for (const supporter of MINIMAL_SUPPORTERS) {
|
||||
expect(pinned.has(supporter.municipalitySlug)).toBe(true)
|
||||
expect(MINIMAL_CONSENT_KEYS).toContain(supporter.consentKey)
|
||||
if (supporter.voteIntentionConsentKey) {
|
||||
expect(supporter.voteIntentionConsentKey).toBe(SUPPORTER_VOTE_INTENTION_CONSENT_KEY)
|
||||
}
|
||||
}
|
||||
expect(MINIMAL_STATE_DEPUTY.slug).toMatch(/^seed-/)
|
||||
})
|
||||
|
||||
it('keeps the E8 campaign goals default sane', () => {
|
||||
expect(MINIMAL_CAMPAIGN_GOALS.stateGoal).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue