Public Access
feat: enhance testing framework with new test cases and configurations
This commit is contained in:
1 parent
a37c093cc4
commit
ef8fe6f938
18 files changed
+793
-6
No files matched your search
@@ -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
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user