wordeck/packages/wordeck-domain/tests/schemas.test.ts
Till JS 372832d266
Some checks are pending
CI / validate (push) Waiting to run
refactor(big-bang): cards → wordeck im gesamten Code-Layer
Phase 2 des cards→wordeck Big-Bang-Rebrand:

- 4 package.json: @cards/* → @wordeck/*
- packages/cards-domain/ → packages/wordeck-domain/
- 41+12 Files: from '@cards/domain' → '@wordeck/domain'
- pgSchema('cards') → pgSchema('wordeck') (Drizzle-Schema)
- 17 Files: process.env.CARDS_* → process.env.WORDECK_*
- docker-compose Service-Names: cards-* → wordeck-*
- docker-compose Volume: /Volumes/ManaData/cards → wordeck
- env-vars in compose: CARDS_DB_PASSWORD/_API_VERSION/_DSGVO_SERVICE_KEY etc. → WORDECK_*
- Log-Prefixes + Error-Strings + manifest-id 'cards' → 'wordeck'
- CORS-Origin cardecky.mana.how → wordeck.com
- .env.production.example umbenannt + S3-Key entfernt (kein MinIO mehr)

Type-Check 0 Errors in api+domain+web, 51/51 Domain-Tests grün.

DB-Rename + Container/Volume-Rename auf mana-server folgen in nächstem
Commit nach Verzeichnis-Rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:39:42 +02:00

168 lines
4.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
CardCreateSchema,
CardSchema,
CardTypeSchema,
DeckCreateSchema,
DeckSchema,
GradeReviewInputSchema,
validateFieldsForType,
} from '../src/schemas/index.ts';
describe('CardTypeSchema', () => {
it('accepts Wordeck text-only types', () => {
expect(() => CardTypeSchema.parse('basic')).not.toThrow();
expect(() => CardTypeSchema.parse('basic-reverse')).not.toThrow();
expect(() => CardTypeSchema.parse('cloze')).not.toThrow();
expect(() => CardTypeSchema.parse('typing')).not.toThrow();
expect(() => CardTypeSchema.parse('multiple-choice')).not.toThrow();
});
it('rejects removed types (image-occlusion, audio-front)', () => {
expect(() => CardTypeSchema.parse('image-occlusion')).toThrow();
expect(() => CardTypeSchema.parse('audio-front')).toThrow();
expect(() => CardTypeSchema.parse('audio')).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('accepts a cloze card with text field', () => {
const r = CardCreateSchema.safeParse({
deck_id: 'd-1',
type: 'cloze',
fields: { text: '{{c1::Paris}} ist die Hauptstadt.' },
});
expect(r.success).toBe(true);
});
it('rejects unknown type via CardTypeSchema', () => {
const r = CardCreateSchema.safeParse({
deck_id: 'd-1',
type: 'audio',
fields: { audio_ref: '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);
});
});