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

+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)
})
})