Files
Moheeb Zara ea8d7a5f91 feat: governance kernel, deployment hardening, local-dev stack
Adds proposals + proposal_votes + policies + incidents + incident_updates
tables with full RLS, async voting flow, dismissal-appeal pathway,
versioned policies with provenance, and an anonymous-incident token mode.
Adds per-space configurable areas, financial visibility settings, member
self-profile + COI disclosure, and a mission statement that renders in
the app footer.

Reorganises lib/actions.ts into per-domain files (tasks, projects,
members, payments, contacts, knowledge-base, secrets, area-leads,
settings, imports, proposals, incidents, policies, areas) plus a barrel.
Adds lib/permissions.ts and lib/auth-helpers.ts so action bodies stop
duplicating the user-to-member-to-role check. Wires every action that has
a Zod schema; introduces a flexibleDateTime preprocessor so HTML date
pickers no longer hit "Invalid datetime".

Hardens RLS: drops NOT NULL on space_members.user_id for offline members,
adds a privilege-escalation trigger that blocks self-edits to role / tier
/ status / approved / has_card_access / space_id, closes four
cross-tenant holes in the governance kernel, and adds a partial unique
index so two admins cannot concurrently activate two drafts of the same
policy slug.

Ships every deployment artifact: Dockerfile, docker-compose,
.do/app.yaml, .env.example, /api/health, plus a managed-Supabase guide
(docs/DEPLOYMENT.md), an end-to-end self-hosted Droplet guide
(docs/DEPLOY_DO_SELFHOSTED.md), and a Supabase-CLI local-dev quickstart
(docs/LOCAL_DEV.md). Replaces middleware.ts with proxy.ts for Next.js 16.
Adds scripts/schema.sql as the canonical idempotent full deploy plus
fourteen numbered incremental migrations.

Adds scripts/seed.sql so a fresh local DB renders something instead of
empty pages: a demo space, five members across all roles, a v1 code of
conduct, three proposals in different states with seven votes already
cast, an open incident, a project, three tasks, a KB entry, and matching
activity-log rows.

Replaces v0-generated test scaffolding with 266 real tests across
__tests__/{actions,auth-helpers,components,governance,permissions,
security,utils,validations}.test.ts. Adds react-markdown + remark-gfm
for proposal / incident / policy body rendering. Deletes four dead v0
fix-* scripts, the unused components/app-shell.tsx, and the duplicate
app/auth/login/page.tsx.

Routes (31 total): /api/health, /api/paypal/sync, /auth/callback, /,
/login, /signup, /signup/confirm, /dashboard, /tasks, /projects,
/members, /payments, /financials, /contacts, /ops, /ops/[id], /ops/new,
/proposals, /proposals/[id], /proposals/new, /incidents, /incidents/[id],
/incidents/new, /policies, /policies/[slug], /policies/new, /profile,
/recruitment, /settings, /comms, /import.

Full pass-by-pass history is in docs/HANDOFF.md; the roadmap is in
docs/GOVERNANCE_FEATURES.md.
2026-05-14 04:52:11 -07:00

328 lines
10 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { z } from 'zod'
import {
flexibleDateTime,
createTaskSchema,
taskIdSchema,
createProjectSchema,
updateProjectStatusSchema,
addMemberSchema,
updateMemberSchema,
createContactSchema,
updateContactSchema,
createKbEntrySchema,
updateKbEntrySchema,
createSecretSchema,
logCashPaymentSchema,
linkPaymentSchema,
updateSpaceSettingsSchema,
saveIntegrationSchema,
upsertAreaLeadSchema,
uuidSchema,
} from '@/lib/validations'
const VALID_UUID = '11111111-2222-3333-4444-555555555555'
describe('flexibleDateTime', () => {
const schema = z.object({ when: flexibleDateTime().optional() })
it('accepts a YYYY-MM-DD date-only string from <input type="date">', () => {
const r = schema.safeParse({ when: '2026-05-22' })
expect(r.success).toBe(true)
if (r.success) {
expect(r.data.when).toBe('2026-05-22T00:00:00.000Z')
}
})
it('accepts a YYYY-MM-DDTHH:MM datetime-local string', () => {
const r = schema.safeParse({ when: '2026-05-22T14:30' })
expect(r.success).toBe(true)
if (r.success && r.data.when) {
// JS Date interprets "2026-05-22T14:30" as local time; the helper
// outputs UTC ISO. We just assert it's a valid ISO datetime.
expect(r.data.when).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
}
})
it('passes through a full RFC 3339 datetime unchanged', () => {
const iso = '2026-05-22T14:30:00.000Z'
const r = schema.safeParse({ when: iso })
expect(r.success).toBe(true)
if (r.success) expect(r.data.when).toBe(iso)
})
it('maps empty string to null (treats it as unset)', () => {
const r = schema.safeParse({ when: '' })
expect(r.success).toBe(true)
if (r.success) expect(r.data.when).toBeNull()
})
it('maps undefined to undefined / null', () => {
const r = schema.safeParse({})
expect(r.success).toBe(true)
})
it('rejects obvious garbage', () => {
const r = schema.safeParse({ when: 'tomorrow at three' })
expect(r.success).toBe(false)
})
})
describe('createTaskSchema', () => {
it('accepts minimal valid input', () => {
const r = createTaskSchema.safeParse({ title: 'Fix laser cooling' })
expect(r.success).toBe(true)
})
it('rejects empty title', () => {
const r = createTaskSchema.safeParse({ title: '' })
expect(r.success).toBe(false)
})
it('rejects title over 200 chars', () => {
const r = createTaskSchema.safeParse({ title: 'x'.repeat(201) })
expect(r.success).toBe(false)
})
it('accepts every database recurrence value, including biweekly', () => {
for (const recurrence of ['none', 'daily', 'weekly', 'biweekly', 'monthly']) {
const r = createTaskSchema.safeParse({ title: 'x', recurrence })
expect(r.success, `recurrence=${recurrence}`).toBe(true)
}
})
it('accepts due_date as YYYY-MM-DD (the bug fix from pass 9)', () => {
const r = createTaskSchema.safeParse({ title: 'x', due_date: '2026-05-22' })
expect(r.success).toBe(true)
if (r.success) expect(r.data.due_date).toBe('2026-05-22T00:00:00.000Z')
})
it('rejects unknown recurrence values', () => {
const r = createTaskSchema.safeParse({ title: 'x', recurrence: 'fortnightly' })
expect(r.success).toBe(false)
})
it('accepts task and chore as type, rejects other strings', () => {
expect(createTaskSchema.safeParse({ title: 'x', type: 'task' }).success).toBe(true)
expect(createTaskSchema.safeParse({ title: 'x', type: 'chore' }).success).toBe(true)
expect(createTaskSchema.safeParse({ title: 'x', type: 'errand' }).success).toBe(false)
})
})
describe('taskIdSchema', () => {
it('accepts a valid UUID', () => {
expect(taskIdSchema.safeParse(VALID_UUID).success).toBe(true)
})
it('rejects non-UUID strings', () => {
expect(taskIdSchema.safeParse('not-a-uuid').success).toBe(false)
})
})
describe('uuidSchema', () => {
it('accepts a valid UUID', () => {
expect(uuidSchema.safeParse(VALID_UUID).success).toBe(true)
})
it('rejects empty string', () => {
expect(uuidSchema.safeParse('').success).toBe(false)
})
})
describe('createProjectSchema', () => {
it('accepts title only', () => {
expect(createProjectSchema.safeParse({ title: 'Build CNC mount' }).success).toBe(true)
})
it('accepts optional tags', () => {
expect(
createProjectSchema.safeParse({ title: 'x', tags: ['fab', 'urgent'] }).success,
).toBe(true)
})
})
describe('updateProjectStatusSchema', () => {
it('accepts every real project_status enum value', () => {
for (const status of ['backlog', 'in_progress', 'review', 'done', 'blocked']) {
const r = updateProjectStatusSchema.safeParse({ projectId: VALID_UUID, status })
expect(r.success, `status=${status}`).toBe(true)
}
})
it('rejects legacy status names that the database does not have', () => {
for (const status of ['planning', 'active', 'paused', 'completed', 'cancelled']) {
const r = updateProjectStatusSchema.safeParse({ projectId: VALID_UUID, status })
expect(r.success, `status=${status}`).toBe(false)
}
})
it('rejects invalid projectId', () => {
expect(
updateProjectStatusSchema.safeParse({ projectId: 'nope', status: 'done' }).success,
).toBe(false)
})
})
describe('addMemberSchema', () => {
it('accepts the associate role (added in pass 2)', () => {
const r = addMemberSchema.safeParse({
email: '[email protected]',
display_name: 'Alex',
role: 'associate',
tier: 'basic',
})
expect(r.success).toBe(true)
})
it('rejects unknown role', () => {
const r = addMemberSchema.safeParse({
email: '[email protected]',
display_name: 'Alex',
role: 'overlord',
tier: 'basic',
})
expect(r.success).toBe(false)
})
it('rejects invalid email', () => {
const r = addMemberSchema.safeParse({
email: 'not-an-email',
display_name: 'Alex',
})
expect(r.success).toBe(false)
})
})
describe('updateMemberSchema', () => {
it('accepts a partial update with just memberId', () => {
expect(
updateMemberSchema.safeParse({ memberId: VALID_UUID }).success,
).toBe(true)
})
it('rejects unknown status', () => {
expect(
updateMemberSchema.safeParse({ memberId: VALID_UUID, status: 'banned' }).success,
).toBe(false)
})
})
describe('createContactSchema', () => {
it('accepts every database contact_type', () => {
for (const t of ['vendor', 'supplier', 'partner', 'landlord', 'city']) {
const r = createContactSchema.safeParse({ name: 'X', contact_type: t })
expect(r.success, `contact_type=${t}`).toBe(true)
}
})
it('rejects legacy contact_type values', () => {
for (const t of ['sponsor', 'media', 'other']) {
const r = createContactSchema.safeParse({ name: 'X', contact_type: t })
expect(r.success, `contact_type=${t}`).toBe(false)
}
})
})
describe('updateContactSchema', () => {
it('requires contactId', () => {
expect(updateContactSchema.safeParse({ name: 'X' }).success).toBe(false)
})
})
describe('createKbEntrySchema', () => {
it('accepts the real visibility enum', () => {
for (const v of ['all_members', 'board', 'admin_only']) {
const r = createKbEntrySchema.safeParse({ title: 't', content: 'c', visibility: v })
expect(r.success, `visibility=${v}`).toBe(true)
}
})
it('rejects legacy visibility values', () => {
for (const v of ['public', 'members', 'admin']) {
const r = createKbEntrySchema.safeParse({ title: 't', content: 'c', visibility: v })
expect(r.success, `visibility=${v}`).toBe(false)
}
})
it('defaults visibility to all_members', () => {
const r = createKbEntrySchema.safeParse({ title: 't', content: 'c' })
expect(r.success).toBe(true)
if (r.success) expect(r.data.visibility).toBe('all_members')
})
})
describe('updateKbEntrySchema', () => {
it('requires entryId', () => {
expect(updateKbEntrySchema.safeParse({ title: 't' }).success).toBe(false)
})
})
describe('createSecretSchema', () => {
it('requires title and value', () => {
expect(createSecretSchema.safeParse({ title: 't', value: 'v' }).success).toBe(true)
expect(createSecretSchema.safeParse({ title: 't' }).success).toBe(false)
expect(createSecretSchema.safeParse({ value: 'v' }).success).toBe(false)
})
})
describe('logCashPaymentSchema', () => {
it('accepts a positive amount and a non-empty note', () => {
const r = logCashPaymentSchema.safeParse({ amount: 25, from_note: 'May dues' })
expect(r.success).toBe(true)
})
it('rejects zero or negative amounts', () => {
expect(logCashPaymentSchema.safeParse({ amount: 0, from_note: 'x' }).success).toBe(false)
expect(logCashPaymentSchema.safeParse({ amount: -5, from_note: 'x' }).success).toBe(false)
})
it('member_id must be a UUID if present', () => {
expect(
logCashPaymentSchema.safeParse({ amount: 1, from_note: 'x', member_id: 'nope' }).success,
).toBe(false)
expect(
logCashPaymentSchema.safeParse({ amount: 1, from_note: 'x', member_id: VALID_UUID }).success,
).toBe(true)
})
})
describe('linkPaymentSchema', () => {
it('requires both UUIDs', () => {
expect(
linkPaymentSchema.safeParse({ paymentId: VALID_UUID, memberId: VALID_UUID }).success,
).toBe(true)
expect(linkPaymentSchema.safeParse({ paymentId: 'x', memberId: VALID_UUID }).success).toBe(false)
})
})
describe('updateSpaceSettingsSchema', () => {
it('accepts a partial update', () => {
expect(updateSpaceSettingsSchema.safeParse({ name: 'New name' }).success).toBe(true)
})
it('enforces slug character set when slug is present', () => {
expect(updateSpaceSettingsSchema.safeParse({ slug: 'good-slug' }).success).toBe(false)
expect(updateSpaceSettingsSchema.safeParse({ slug: 'goodslug' }).success).toBe(true)
})
})
describe('saveIntegrationSchema', () => {
it('requires platform and a config record', () => {
expect(
saveIntegrationSchema.safeParse({ platform: 'paypal', config: { client_id: 'x' } }).success,
).toBe(true)
expect(saveIntegrationSchema.safeParse({ platform: 'paypal' }).success).toBe(false)
})
})
describe('upsertAreaLeadSchema', () => {
it('accepts the real area_lead_status values', () => {
for (const s of ['active', 'vacant', 'handoff']) {
const r = upsertAreaLeadSchema.safeParse({
area_code: 'LASER',
area_name: 'Laser',
status: s,
})
expect(r.success, `status=${s}`).toBe(true)
}
})
it('rejects unknown status', () => {
const r = upsertAreaLeadSchema.safeParse({
area_code: 'LASER',
area_name: 'Laser',
status: 'pending',
})
expect(r.success).toBe(false)
})
})