Files
hackerspace.sh/__tests__/forms.test.ts
T
Moheeb Zara 897fa30ec1 feat(forms): server actions, Zod schemas, and dynamic answer validation (Phase 2)
- lib/validations.ts: form field/array schemas (8 field types,
  duplicate-key and select-options refinements) and create/update/
  status/submit/link envelope schemas; slug immutable on update
- lib/forms-schema.ts: pure parseFormSchema + validateAnswers; unknown
  answer keys are discarded so a caller cannot inject arbitrary jsonb
- lib/actions/forms.ts: create/update/setStatus/delete/list/results/
  CSV export (forms.manage via user_has_permission + RLS), submitForm
  (service-client read+write, visibility + waiver consent enforced,
  schema/legal/version snapshot, IP/UA capture), and
  linkSubmissionsForMember (manual admin link; auto path is Phase 5)
- updateForm bumps a published waiver's version on legal/schema change
  (non-blocking; existing submissions stay valid against their snapshot)
- deleteForm refuses when submissions exist (immutable records)
- types/database.ts: type user_has_permission/user_effective_roles
  (stale since migration 023)
- __tests__/forms.test.ts (14); API_REFERENCE + HANDOFF updated

285/285 tests, build clean. No deploy.
2026-05-16 14:51:20 -07:00

124 lines
4.1 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
createFormSchema,
updateFormSchema,
submitFormSchema,
formSchemaArray,
formFieldSchema,
} from '@/lib/validations'
import { parseFormSchema, validateAnswers, type FormField } from '@/lib/forms-schema'
describe('form field schema', () => {
it('accepts a valid text field and defaults required to false', () => {
const r = formFieldSchema.safeParse({ key: 'full_name', type: 'short_text', label: 'Name' })
expect(r.success).toBe(true)
if (r.success) expect(r.data.required).toBe(false)
})
it('rejects a select field with no options', () => {
const r = formFieldSchema.safeParse({ key: 'tier', type: 'select', label: 'Tier' })
expect(r.success).toBe(false)
})
it('rejects an invalid field key', () => {
const r = formFieldSchema.safeParse({ key: 'Full Name', type: 'short_text', label: 'X' })
expect(r.success).toBe(false)
})
it('rejects duplicate field keys in the array', () => {
const r = formSchemaArray.safeParse([
{ key: 'a', type: 'short_text', label: 'A' },
{ key: 'a', type: 'short_text', label: 'A again' },
])
expect(r.success).toBe(false)
})
})
describe('createFormSchema', () => {
it('applies defaults', () => {
const r = createFormSchema.safeParse({ slug: 'liability-waiver', title: 'Waiver' })
expect(r.success).toBe(true)
if (r.success) {
expect(r.data.kind).toBe('form')
expect(r.data.visibility).toBe('members')
expect(r.data.schema).toEqual([])
}
})
it('rejects an uppercase or hyphen-edged slug', () => {
expect(createFormSchema.safeParse({ slug: 'Waiver', title: 'x' }).success).toBe(false)
expect(createFormSchema.safeParse({ slug: '-waiver', title: 'x' }).success).toBe(false)
expect(createFormSchema.safeParse({ slug: 'waiver-', title: 'x' }).success).toBe(false)
})
})
describe('submitFormSchema', () => {
it('requires formId or slug', () => {
expect(submitFormSchema.safeParse({ answers: {} }).success).toBe(false)
expect(submitFormSchema.safeParse({ slug: 'a', answers: {} }).success).toBe(true)
})
})
describe('updateFormSchema', () => {
it('does not accept a slug change', () => {
const r = updateFormSchema.safeParse({
formId: '11111111-1111-1111-1111-111111111111',
slug: 'new-slug',
})
// slug is simply stripped (not in the schema), so parse still succeeds
expect(r.success).toBe(true)
if (r.success) expect('slug' in r.data).toBe(false)
})
})
const fields: FormField[] = [
{ key: 'full_name', type: 'short_text', label: 'Full name', required: true },
{ key: 'agree', type: 'checkbox', label: 'I agree', required: true },
{ key: 'shirt', type: 'select', label: 'Shirt', required: false, options: ['S', 'M', 'L'] },
{ key: 'contact', type: 'email', label: 'Email', required: false },
]
describe('validateAnswers', () => {
it('errors when a required field is missing', () => {
const r = validateAnswers(fields, { agree: true })
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error).toMatch(/Full name/)
})
it('errors when a required checkbox is not checked', () => {
const r = validateAnswers(fields, { full_name: 'Ada', agree: false })
expect(r.ok).toBe(false)
})
it('rejects a select value outside the options', () => {
const r = validateAnswers(fields, { full_name: 'Ada', agree: true, shirt: 'XL' })
expect(r.ok).toBe(false)
})
it('drops unknown keys and normalizes', () => {
const r = validateAnswers(fields, {
full_name: ' Ada ',
agree: 'on',
contact: '[email protected]',
injected: 'evil',
})
expect(r.ok).toBe(true)
if (r.ok) {
expect(r.value).toEqual({ full_name: 'Ada', agree: true, contact: '[email protected]' })
expect('injected' in r.value).toBe(false)
}
})
})
describe('parseFormSchema', () => {
it('returns [] for malformed stored schema', () => {
expect(parseFormSchema('not an array')).toEqual([])
expect(parseFormSchema([{ key: 'a' }])).toEqual([])
})
it('round-trips a valid schema', () => {
const raw = [{ key: 'a', type: 'short_text', label: 'A' }]
expect(parseFormSchema(raw)).toHaveLength(1)
})
})