- 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.
124 lines
4.1 KiB
TypeScript
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)
|
|
})
|
|
})
|