Some checks are pending
CI / validate (push) Waiting to run
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>
168 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
});
|