import { describe, it, expect } from 'vitest'; import { CardCreateSchema, CardSchema, CardTypeSchema, DeckCreateSchema, DeckSchema, GradeReviewInputSchema, validateFieldsForType, } from '../src/schemas/index.ts'; describe('CardTypeSchema', () => { it('accepts MVP types', () => { expect(() => CardTypeSchema.parse('basic')).not.toThrow(); expect(() => CardTypeSchema.parse('basic-reverse')).not.toThrow(); }); it('rejects future types in MVP schema', () => { expect(() => CardTypeSchema.parse('cloze')).toThrow(); }); }); describe('validateFieldsForType', () => { it('basic requires front + back', () => { expect(validateFieldsForType('basic', { front: 'q', back: 'a' })).toEqual({ ok: true }); expect(validateFieldsForType('basic', { front: 'q' })).toEqual({ ok: false, missing: ['back'], }); }); it('cloze requires text', () => { expect(validateFieldsForType('cloze', { text: 'x' })).toEqual({ ok: true }); expect(validateFieldsForType('cloze', {})).toEqual({ ok: false, missing: ['text'] }); }); }); describe('CardCreateSchema', () => { it('accepts a basic card', () => { const r = CardCreateSchema.safeParse({ deck_id: 'd-1', type: 'basic', fields: { front: 'Q', back: 'A' }, }); expect(r.success).toBe(true); }); it('rejects basic card without back', () => { const r = CardCreateSchema.safeParse({ deck_id: 'd-1', type: 'basic', fields: { front: 'Q' }, }); expect(r.success).toBe(false); }); it('rejects unknown type via CardTypeSchema', () => { const r = CardCreateSchema.safeParse({ deck_id: 'd-1', type: 'cloze', fields: { text: 'x' }, }); expect(r.success).toBe(false); }); it('rejects extra fields (strict)', () => { const r = CardCreateSchema.safeParse({ deck_id: 'd-1', type: 'basic', fields: { front: 'Q', back: 'A' }, malicious: 'inject', }); expect(r.success).toBe(false); }); }); describe('DeckCreateSchema', () => { it('minimal valid deck', () => { const r = DeckCreateSchema.safeParse({ name: 'My Deck' }); expect(r.success).toBe(true); }); it('rejects empty name', () => { const r = DeckCreateSchema.safeParse({ name: '' }); expect(r.success).toBe(false); }); it('rejects invalid color', () => { const r = DeckCreateSchema.safeParse({ name: 'X', color: 'red' }); expect(r.success).toBe(false); }); it('accepts hex color', () => { const r = DeckCreateSchema.safeParse({ name: 'X', color: '#ff8800' }); expect(r.success).toBe(true); }); }); describe('GradeReviewInputSchema', () => { it('accepts a grade input', () => { const r = GradeReviewInputSchema.safeParse({ card_id: 'c-1', sub_index: 0, rating: 'good', }); expect(r.success).toBe(true); }); it('rejects unknown rating', () => { const r = GradeReviewInputSchema.safeParse({ card_id: 'c-1', sub_index: 0, rating: 'perfect', }); expect(r.success).toBe(false); }); it('defaults sub_index to 0', () => { const r = GradeReviewInputSchema.parse({ card_id: 'c-1', rating: 'good' }); expect(r.sub_index).toBe(0); }); }); describe('strict variants reject extras', () => { it('DeckSchema rejects extra props', () => { const r = DeckSchema.safeParse({ id: 'd-1', user_id: 'u-1', name: 'D', visibility: 'private', fsrs_settings: {}, created_at: '2026-05-08T10:00:00.000Z', updated_at: '2026-05-08T10:00:00.000Z', leaks: 'no', }); expect(r.success).toBe(false); }); it('CardSchema rejects extra props', () => { const r = CardSchema.safeParse({ id: 'c-1', deck_id: 'd-1', user_id: 'u-1', type: 'basic', fields: { front: 'Q', back: 'A' }, media_refs: [], created_at: '2026-05-08T10:00:00.000Z', updated_at: '2026-05-08T10:00:00.000Z', leaked: 'yes', }); expect(r.success).toBe(false); }); });