feat: enhance testing framework with new test cases and configurations
Test / backend (push) Failing after 9s
Test / frontend (push) Failing after 14s

This commit is contained in:
LaurensBartelds committed 2026-04-26 21:45:02 +02:00
1 parent a37c093cc4
commit ef8fe6f938
18 files changed
+793 -6

No files matched your search

+22 -4
View File
@@ -6,11 +6,17 @@ on:
- '**'
jobs:
backend-tests:
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./backend
env:
JWT_SECRET: ci-test-secret
NODE_ENV: test
DB_PATH: ':memory:'
CORS_ORIGIN: 'http://localhost:5173'
APP_URL: 'http://localhost:5173'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -19,9 +25,14 @@ jobs:
cache: 'npm'
cache-dependency-path: ./backend/package-lock.json
- run: npm ci
- run: npm test
- name: Type check
run: npx tsc --noEmit
- name: Build
run: npm run build
- name: Run tests
run: npm test
frontend-tests:
frontend:
runs-on: ubuntu-latest
defaults:
run:
@@ -34,4 +45,11 @@ jobs:
cache: 'npm'
cache-dependency-path: ./frontend/package-lock.json
- run: npm ci
- run: npm run test:run
- name: Lint
run: npm run lint
- name: Type check
run: npx tsc -b --noEmit
- name: Build
run: npm run build
- name: Run tests
run: npm run test:run
+62
View File
@@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest'
describe('env helpers', () => {
it('uses default value when env var is missing', () => {
delete process.env.TEST_VAR_UNIQUE
const env = (key: string, defaultValue?: string): string => {
const value = process.env[key]
if (value === undefined || value === '') {
if (defaultValue !== undefined) return defaultValue
throw new Error(`Missing ${key}`)
}
return value
}
expect(env('TEST_VAR_UNIQUE', 'fallback')).toBe('fallback')
})
it('returns env value when present', () => {
process.env.TEST_VAR_UNIQUE = 'value'
const env = (key: string, defaultValue?: string): string => {
const value = process.env[key]
if (value === undefined || value === '') {
if (defaultValue !== undefined) return defaultValue
throw new Error(`Missing ${key}`)
}
return value
}
expect(env('TEST_VAR_UNIQUE')).toBe('value')
delete process.env.TEST_VAR_UNIQUE
})
it('envInt parses integer', () => {
process.env.TEST_INT = '42'
const envInt = (key: string, defaultValue?: number): number => {
const raw = process.env[key]
if (raw === undefined || raw === '') {
if (defaultValue !== undefined) return defaultValue
throw new Error(`Missing ${key}`)
}
const parsed = parseInt(raw, 10)
if (Number.isNaN(parsed)) throw new Error(`Invalid int ${key}`)
return parsed
}
expect(envInt('TEST_INT')).toBe(42)
delete process.env.TEST_INT
})
it('envInt throws on NaN', () => {
process.env.TEST_INT = 'abc'
const envInt = (key: string, defaultValue?: number): number => {
const raw = process.env[key]
if (raw === undefined || raw === '') {
if (defaultValue !== undefined) return defaultValue
throw new Error(`Missing ${key}`)
}
const parsed = parseInt(raw, 10)
if (Number.isNaN(parsed)) throw new Error(`Invalid int ${key}`)
return parsed
}
expect(() => envInt('TEST_INT')).toThrow('Invalid int TEST_INT')
delete process.env.TEST_INT
})
})
+21
View File
@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest'
import { AppError } from './error'
describe('AppError', () => {
it('is instance of Error and AppError', () => {
const err = new AppError(404, 'Not found')
expect(err).toBeInstanceOf(AppError)
expect(err).toBeInstanceOf(Error)
})
it('sets statusCode and message', () => {
const err = new AppError(500, 'Server error')
expect(err.statusCode).toBe(500)
expect(err.message).toBe('Server error')
})
it('sets name to AppError', () => {
const err = new AppError(400, 'Bad request')
expect(err.name).toBe('AppError')
})
})
+2 -1
View File
@@ -5,7 +5,8 @@ export class AppError extends Error {
public isOperational = true
) {
super(message);
Object.setPrototypeOf(this, new Error().constructor);
this.name = 'AppError';
Object.setPrototypeOf(this, AppError.prototype);
}
}
+37
View File
@@ -0,0 +1,37 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { asyncHandler } from './asyncHandler'
import type { Request, Response, NextFunction } from 'express'
const mockReq = {} as Request
const mockRes = {} as Response
let mockNext: NextFunction
beforeEach(() => {
mockNext = vi.fn() as unknown as NextFunction
})
describe('asyncHandler', () => {
it('calls next with error when async handler throws', async () => {
const error = new Error('boom')
const handler = asyncHandler(async () => {
throw error
})
await handler(mockReq, mockRes, mockNext)
expect(mockNext).toHaveBeenCalledWith(error)
})
it('does not call next when handler resolves', async () => {
const handler = asyncHandler(async () => {
// success
})
await handler(mockReq, mockRes, mockNext)
expect(mockNext).not.toHaveBeenCalled()
})
it('calls next with error when handler rejects', async () => {
const error = new Error('rejected')
const handler = asyncHandler(() => Promise.reject(error))
await handler(mockReq, mockRes, mockNext)
expect(mockNext).toHaveBeenCalledWith(error)
})
})
+44
View File
@@ -0,0 +1,44 @@
import { describe, it, expect } from 'vitest'
describe('tokens', () => {
it('round-trips JWT payload', async () => {
process.env.JWT_SECRET = 'test-secret-for-unit-tests-only'
const { signToken, verifyToken } = await import('./tokens')
const payload = { userId: 1, email: 'a@b.com', displayName: 'Test', role: 'user' as const }
const token = signToken(payload)
const decoded = verifyToken(token)
expect(decoded.userId).toBe(1)
expect(decoded.email).toBe('a@b.com')
expect(decoded.displayName).toBe('Test')
expect(decoded.role).toBe('user')
})
it('throws on invalid token', async () => {
process.env.JWT_SECRET = 'test-secret-for-unit-tests-only'
const { verifyToken } = await import('./tokens')
expect(() => verifyToken('invalid.token.here')).toThrow()
})
it('throws on tampered token', async () => {
process.env.JWT_SECRET = 'test-secret-for-unit-tests-only'
const { signToken, verifyToken } = await import('./tokens')
const payload = { userId: 1, email: 'a@b.com', displayName: 'Test', role: 'user' as const }
const token = signToken(payload)
const tampered = token.slice(0, -5) + 'xxxxx'
expect(() => verifyToken(tampered)).toThrow()
})
it('generates hex verification token of expected length', async () => {
const { generateVerificationToken } = await import('./tokens')
const token = generateVerificationToken()
expect(token).toMatch(/^[a-f0-9]+$/)
expect(token.length).toBe(64)
})
it('generates unique verification tokens', async () => {
const { generateVerificationToken } = await import('./tokens')
const t1 = generateVerificationToken()
const t2 = generateVerificationToken()
expect(t1).not.toBe(t2)
})
})
@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest'
import { updateUserRoleSchema, updateSettingSchema, listUsersQuerySchema, userIdParamSchema } from './adminSchemas'
describe('updateUserRoleSchema', () => {
it('accepts valid role change', () => {
const result = updateUserRoleSchema.safeParse({ user_id: 1, role: 'admin' })
expect(result.success).toBe(true)
})
it('rejects invalid role', () => {
const result = updateUserRoleSchema.safeParse({ user_id: 1, role: 'superadmin' })
expect(result.success).toBe(false)
})
it('rejects zero user_id', () => {
const result = updateUserRoleSchema.safeParse({ user_id: 0, role: 'admin' })
expect(result.success).toBe(false)
})
})
describe('updateSettingSchema', () => {
it('accepts valid setting', () => {
const result = updateSettingSchema.safeParse({ key: 'app_name', value: 'BrainWave' })
expect(result.success).toBe(true)
})
it('rejects empty key', () => {
const result = updateSettingSchema.safeParse({ key: '', value: 'BrainWave' })
expect(result.success).toBe(false)
})
it('rejects empty value', () => {
const result = updateSettingSchema.safeParse({ key: 'app_name', value: '' })
expect(result.success).toBe(false)
})
})
describe('listUsersQuerySchema', () => {
it('accepts empty query', () => {
const result = listUsersQuerySchema.safeParse({})
expect(result.success).toBe(true)
expect((result as any).data.page).toBe(1)
})
it('accepts page string', () => {
const result = listUsersQuerySchema.safeParse({ page: '3' })
expect(result.success).toBe(true)
expect((result as any).data.page).toBe(3)
})
it('rejects non-numeric page', () => {
const result = listUsersQuerySchema.safeParse({ page: 'abc' })
expect(result.success).toBe(false)
})
it('accepts search', () => {
const result = listUsersQuerySchema.safeParse({ search: 'john' })
expect(result.success).toBe(true)
})
})
describe('userIdParamSchema', () => {
it('accepts numeric id', () => {
const result = userIdParamSchema.safeParse({ id: '7' })
expect(result.success).toBe(true)
})
it('rejects non-numeric id', () => {
const result = userIdParamSchema.safeParse({ id: 'seven' })
expect(result.success).toBe(false)
})
})
+115
View File
@@ -0,0 +1,115 @@
import { describe, it, expect } from 'vitest'
import { registerSchema, loginSchema, verifyEmailSchema, resetPasswordSchema, updateProfileSchema, changePasswordSchema, forgotPasswordSchema } from './authSchemas'
describe('registerSchema', () => {
it('accepts valid registration', () => {
const result = registerSchema.safeParse({
email: 'test@example.com',
password: 'password123',
displayName: 'Test User',
})
expect(result.success).toBe(true)
})
it('rejects invalid email', () => {
const result = registerSchema.safeParse({
email: 'not-an-email',
password: 'password123',
displayName: 'Test User',
})
expect(result.success).toBe(false)
})
it('rejects short password', () => {
const result = registerSchema.safeParse({
email: 'test@example.com',
password: '123',
displayName: 'Test User',
})
expect(result.success).toBe(false)
})
it('rejects empty display name', () => {
const result = registerSchema.safeParse({
email: 'test@example.com',
password: 'password123',
displayName: '',
})
expect(result.success).toBe(false)
})
})
describe('loginSchema', () => {
it('accepts valid login', () => {
const result = loginSchema.safeParse({ email: 'test@example.com', password: 'pass' })
expect(result.success).toBe(true)
})
it('rejects missing email', () => {
const result = loginSchema.safeParse({ password: 'pass' })
expect(result.success).toBe(false)
})
})
describe('verifyEmailSchema', () => {
it('accepts valid token', () => {
const result = verifyEmailSchema.safeParse({ token: 'abc123' })
expect(result.success).toBe(true)
})
it('rejects empty token', () => {
const result = verifyEmailSchema.safeParse({ token: '' })
expect(result.success).toBe(false)
})
})
describe('resetPasswordSchema', () => {
it('accepts valid reset', () => {
const result = resetPasswordSchema.safeParse({ token: 'abc', newPassword: 'newpass123' })
expect(result.success).toBe(true)
})
it('rejects short password', () => {
const result = resetPasswordSchema.safeParse({ token: 'abc', password: '123' })
expect(result.success).toBe(false)
})
})
describe('updateProfileSchema', () => {
it('accepts valid profile', () => {
const result = updateProfileSchema.safeParse({ displayName: 'New Name' })
expect(result.success).toBe(true)
})
it('rejects empty displayName', () => {
const result = updateProfileSchema.safeParse({ displayName: '' })
expect(result.success).toBe(false)
})
})
describe('changePasswordSchema', () => {
it('accepts valid change', () => {
const result = changePasswordSchema.safeParse({
currentPassword: 'oldpass',
newPassword: 'newpass123',
})
expect(result.success).toBe(true)
})
})
describe('forgotPasswordSchema', () => {
it('accepts valid email', () => {
const result = forgotPasswordSchema.safeParse({ email: 'test@example.com' })
expect(result.success).toBe(true)
})
it('accepts empty object', () => {
const result = forgotPasswordSchema.safeParse({})
expect(result.success).toBe(true)
})
it('rejects invalid email', () => {
const result = forgotPasswordSchema.safeParse({ email: 'bad' })
expect(result.success).toBe(false)
})
})
@@ -0,0 +1,85 @@
import { describe, it, expect } from 'vitest'
import { examSchema, saveExamSchema, updateExamSchema, examIdParamSchema } from './examSchemas'
describe('examSchema', () => {
it('accepts minimal valid exam', () => {
const result = examSchema.safeParse({
title: 'My Exam',
questions: [{ id: '1', type: 'essay', prompt: 'Q' }],
})
expect(result.success).toBe(true)
})
it('rejects missing title', () => {
const result = examSchema.safeParse({
questions: [{ id: '1', type: 'essay', prompt: 'Q' }],
})
expect(result.success).toBe(false)
})
it('rejects empty questions array', () => {
const result = examSchema.safeParse({
title: 'My Exam',
questions: [],
})
expect(result.success).toBe(false)
})
it('rejects question missing id', () => {
const result = examSchema.safeParse({
title: 'My Exam',
questions: [{ type: 'essay', prompt: 'Q' }],
})
expect(result.success).toBe(false)
})
it('accepts optional id', () => {
const result = examSchema.safeParse({
id: 'abc',
title: 'My Exam',
questions: [{ id: '1', type: 'essay', prompt: 'Q' }],
})
expect(result.success).toBe(true)
})
})
describe('saveExamSchema', () => {
it('accepts valid save payload', () => {
const result = saveExamSchema.safeParse({
exam: {
title: 'My Exam',
questions: [{ id: '1', type: 'essay', prompt: 'Q' }],
},
})
expect(result.success).toBe(true)
})
it('rejects missing exam', () => {
const result = saveExamSchema.safeParse({})
expect(result.success).toBe(false)
})
})
describe('updateExamSchema', () => {
it('accepts valid update payload', () => {
const result = updateExamSchema.safeParse({
exam: {
title: 'Updated',
questions: [{ id: '1', type: 'essay', prompt: 'Q' }],
},
})
expect(result.success).toBe(true)
})
})
describe('examIdParamSchema', () => {
it('accepts numeric id string', () => {
const result = examIdParamSchema.safeParse({ id: '42' })
expect(result.success).toBe(true)
})
it('rejects non-numeric id', () => {
const result = examIdParamSchema.safeParse({ id: 'abc' })
expect(result.success).toBe(false)
})
})
@@ -0,0 +1,103 @@
import { describe, it, expect } from 'vitest'
import { submitResultSchema, resultIdParamSchema } from './resultSchemas'
describe('submitResultSchema', () => {
it('accepts valid result', () => {
const result = submitResultSchema.safeParse({
title: 'My Exam',
score_points: 8,
total_points: 10,
correct_count: 8,
graded_count: 10,
elapsed_ms: 120000,
state: {
answers: { q1: 'a' },
gradedAnswers: { q1: { correct: true, points: 1 } },
answeredCount: 10,
score: 80,
correctCount: 8,
gradedCount: 10,
},
exam: {
title: 'My Exam',
questions: [],
},
})
expect(result.success).toBe(true)
})
it('rejects missing title', () => {
const result = submitResultSchema.safeParse({
score_points: 8,
total_points: 10,
correct_count: 8,
graded_count: 10,
elapsed_ms: 120000,
state: {
answers: {},
gradedAnswers: {},
answeredCount: 0,
score: 0,
correctCount: 0,
gradedCount: 0,
},
exam: { title: 'My Exam', questions: [] },
})
expect(result.success).toBe(false)
})
it('accepts optional exam_id', () => {
const result = submitResultSchema.safeParse({
exam_id: 1,
title: 'My Exam',
score_points: 8,
total_points: 10,
correct_count: 8,
graded_count: 10,
elapsed_ms: 120000,
state: {
answers: {},
gradedAnswers: {},
answeredCount: 0,
score: 0,
correctCount: 0,
gradedCount: 0,
},
exam: { title: 'My Exam', questions: [] },
})
expect(result.success).toBe(true)
})
it('accepts negative score_points because schema allows any number', () => {
const result = submitResultSchema.safeParse({
title: 'My Exam',
score_points: -1,
total_points: 10,
correct_count: 8,
graded_count: 10,
elapsed_ms: 120000,
state: {
answers: {},
gradedAnswers: {},
answeredCount: 0,
score: 0,
correctCount: 0,
gradedCount: 0,
},
exam: { title: 'My Exam', questions: [] },
})
expect(result.success).toBe(true)
})
})
describe('resultIdParamSchema', () => {
it('accepts numeric id string', () => {
const result = resultIdParamSchema.safeParse({ id: '42' })
expect(result.success).toBe(true)
})
it('rejects non-numeric id', () => {
const result = resultIdParamSchema.safeParse({ id: 'abc' })
expect(result.success).toBe(false)
})
})
@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest'
import { setupSchema, setupStatusSchema } from './setupSchemas'
describe('setupSchema', () => {
it('accepts valid setup', () => {
const result = setupSchema.safeParse({
email: 'admin@example.com',
password: 'adminpass123',
displayName: 'Admin',
siteTitle: 'BrainWave',
language: 'en',
})
expect(result.success).toBe(true)
})
it('rejects invalid email', () => {
const result = setupSchema.safeParse({
email: 'bad',
password: 'adminpass123',
displayName: 'Admin',
})
expect(result.success).toBe(false)
})
it('rejects short password', () => {
const result = setupSchema.safeParse({
email: 'admin@example.com',
password: '123',
displayName: 'Admin',
})
expect(result.success).toBe(false)
})
})
describe('setupStatusSchema', () => {
it('accepts empty object', () => {
const result = setupStatusSchema.safeParse({})
expect(result.success).toBe(true)
})
it('accepts optional email', () => {
const result = setupStatusSchema.safeParse({ email: 'admin@example.com' })
expect(result.success).toBe(true)
})
it('rejects invalid optional email', () => {
const result = setupStatusSchema.safeParse({ email: 'bad' })
expect(result.success).toBe(false)
})
})
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
setupFiles: ['./vitest.setup.ts'],
},
})
+7
View File
@@ -0,0 +1,7 @@
import { vi } from 'vitest'
vi.stubEnv('JWT_SECRET', 'test-secret-for-unit-tests-only')
vi.stubEnv('NODE_ENV', 'test')
vi.stubEnv('DB_PATH', ':memory:')
vi.stubEnv('CORS_ORIGIN', 'http://localhost:5173')
vi.stubEnv('APP_URL', 'http://localhost:5173')
+20
View File
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest'
import { ApiError } from './api'
describe('ApiError', () => {
it('extends Error', () => {
const err = new ApiError(404, 'Not found')
expect(err).toBeInstanceOf(Error)
})
it('sets status and message', () => {
const err = new ApiError(500, 'Server error')
expect(err.status).toBe(500)
expect(err.message).toBe('Server error')
})
it('sets name to ApiError', () => {
const err = new ApiError(401, 'Unauthorized')
expect(err.name).toBe('ApiError')
})
})
+29 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { slugify } from './slug'
import { slugify, getExamUrlId, extractExamSlug } from './slug'
describe('slugify', () => {
it('lowercases', () => {
@@ -24,3 +24,31 @@ describe('slugify', () => {
expect(slugify('')).toBe('')
})
})
describe('getExamUrlId', () => {
it('returns slug with id when id exists', () => {
expect(getExamUrlId({ id: 'abc12345', title: 'My Exam' })).toBe('my-exam-abc12345')
})
it('returns slug only when id is missing', () => {
expect(getExamUrlId({ title: 'My Exam' })).toBe('my-exam')
})
})
describe('extractExamSlug', () => {
it('extracts slug from slug-id format', () => {
expect(extractExamSlug('my-exam-abc12345')).toBe('my-exam')
})
it('returns full string when no dash', () => {
expect(extractExamSlug('myexam')).toBe('myexam')
})
it('returns full string when suffix is too short to be an id', () => {
expect(extractExamSlug('my-exam-abc')).toBe('my-exam-abc')
})
it('handles slug with multiple dashes', () => {
expect(extractExamSlug('my-cool-exam-abc12345678')).toBe('my-cool-exam')
})
})
+65
View File
@@ -0,0 +1,65 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { loadState, saveState, clearState, loadHistory, appendHistory, clearHistory } from './storage'
const storage: Record<string, string> = {}
beforeEach(() => {
Object.keys(storage).forEach((k) => delete storage[k])
globalThis.localStorage = {
getItem: (key: string) => storage[key] ?? null,
setItem: (key: string, value: string) => { storage[key] = value },
removeItem: (key: string) => { delete storage[key] },
length: 0,
key: () => null,
clear: () => { Object.keys(storage).forEach((k) => delete storage[k]) },
} as Storage
})
describe('loadState / saveState / clearState', () => {
it('returns null when nothing saved', () => {
expect(loadState()).toBeNull()
})
it('returns saved state', () => {
const data = { exam: { id: '1', title: 'E', questions: [] }, state: { currentIdx: 0, answers: {}, flags: {}, notes: '', startedAt: Date.now(), feedbackMode: 'end' as const } }
saveState(data)
expect(loadState()).toEqual(data)
})
it('clears saved state', () => {
saveState({ exam: { id: '1', title: 'E', questions: [] }, state: { currentIdx: 0, answers: {}, flags: {}, notes: '', startedAt: Date.now(), feedbackMode: 'end' as const } })
clearState()
expect(loadState()).toBeNull()
})
it('returns null for corrupted json', () => {
localStorage.setItem('brainwave.exam.state', 'not-json')
expect(loadState()).toBeNull()
})
})
describe('loadHistory / appendHistory / clearHistory', () => {
it('returns empty array when nothing saved', () => {
expect(loadHistory()).toEqual([])
})
it('appends entries and limits to 30', () => {
for (let i = 0; i < 35; i++) {
appendHistory({ id: String(i), title: `Exam ${i}`, completedAt: Date.now(), elapsedMs: 0, gradedCount: 10, correctCount: i, totalPoints: 10, scorePoints: i, passingScore: 5, exam: { title: `Exam ${i}`, questions: [] }, state: { currentIdx: 0, answers: {}, flags: {}, notes: '', startedAt: Date.now(), feedbackMode: 'end' as const } })
}
const hist = loadHistory()
expect(hist.length).toBe(30)
expect(hist[0].id).toBe('34')
})
it('clears history', () => {
appendHistory({ id: '1', title: 'E', completedAt: Date.now(), elapsedMs: 0, gradedCount: 1, correctCount: 1, totalPoints: 1, scorePoints: 1, passingScore: 1, exam: { title: 'E', questions: [] }, state: { currentIdx: 0, answers: {}, flags: {}, notes: '', startedAt: Date.now(), feedbackMode: 'end' as const } })
clearHistory()
expect(loadHistory()).toEqual([])
})
it('returns empty array for corrupted json', () => {
localStorage.setItem('brainwave.exam.history', 'not-json')
expect(loadHistory()).toEqual([])
})
})
+51
View File
@@ -0,0 +1,51 @@
import { describe, it, expect } from 'vitest'
import { validateExam, parseExam } from './validation'
describe('validateExam', () => {
it('returns error for non-object', () => {
expect(validateExam(null)).toBe('Not a JSON object')
expect(validateExam('string')).toBe('Not a JSON object')
expect(validateExam(123)).toBe('Not a JSON object')
})
it('returns error for missing title', () => {
expect(validateExam({ questions: [] })).toBe('Missing "title"')
})
it('returns error for missing questions array', () => {
expect(validateExam({ title: 'Exam' })).toBe('Missing "questions" array')
})
it('returns error for empty questions array', () => {
expect(validateExam({ title: 'Exam', questions: [] })).toBe('Missing "questions" array')
})
it('returns error for question missing id', () => {
expect(validateExam({ title: 'Exam', questions: [{ type: 'essay', prompt: 'Q' }] })).toBe('Question missing id')
})
it('returns error for question missing type', () => {
expect(validateExam({ title: 'Exam', questions: [{ id: '1', prompt: 'Q' }] })).toBe('Question "1" missing type')
})
it('returns error for question missing prompt', () => {
expect(validateExam({ title: 'Exam', questions: [{ id: '1', type: 'essay' }] })).toBe('Question "1" missing prompt')
})
it('returns null for valid exam', () => {
expect(validateExam({ title: 'Exam', questions: [{ id: '1', type: 'essay', prompt: 'Q' }] })).toBe(null)
})
})
describe('parseExam', () => {
it('generates id when missing', () => {
const exam = parseExam({ title: 'T', questions: [] })
expect(exam.id).toBeTruthy()
expect(typeof exam.id).toBe('string')
})
it('preserves existing id', () => {
const exam = parseExam({ id: 'abc123', title: 'T', questions: [] })
expect(exam.id).toBe('abc123')
})
})
@@ -0,0 +1 @@
{"version":"4.1.5","results":[[":frontend/src/utils/grading.test.ts",{"duration":9.39569999999992,"failed":false}],[":frontend/src/utils/latex.test.ts",{"duration":14.06759999999997,"failed":false}],[":backend/src/validators/authSchemas.test.ts",{"duration":10.843899999999962,"failed":false}],[":frontend/src/utils/validation.test.ts",{"duration":7.211099999999988,"failed":false}],[":frontend/src/utils/storage.test.ts",{"duration":12.619899999999916,"failed":false}],[":backend/src/validators/examSchemas.test.ts",{"duration":9.806400000000053,"failed":false}],[":backend/src/validators/adminSchemas.test.ts",{"duration":8.930499999999938,"failed":false}],[":backend/src/middleware/error.test.ts",{"duration":4.177999999999997,"failed":false}],[":frontend/src/utils/slug.test.ts",{"duration":11.914499999999975,"failed":false}],[":frontend/src/utils/formatting.test.ts",{"duration":6.8618000000000166,"failed":false}],[":backend/src/config.test.ts",{"duration":6.659400000000005,"failed":false}],[":backend/src/validators/resultSchemas.test.ts",{"duration":8.660599999999931,"failed":false}],[":backend/src/utils/tokens.test.ts",{"duration":181.15880000000004,"failed":false}],[":backend/src/validators/setupSchemas.test.ts",{"duration":8.40420000000006,"failed":false}],[":backend/src/utils/asyncHandler.test.ts",{"duration":7.896400000000028,"failed":false}],[":frontend/src/utils/api.test.ts",{"duration":4.411400000000015,"failed":false}]]}