e3f40410e9
Build & Deploy / Build & Push Docker Image (push) Successful in 3m10s
Test / Type Check (all packages) (push) Successful in 1m2s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Failing after 48s
Test / Homepage Unit Tests (push) Successful in 45s
Test / Storefront Unit Tests (push) Failing after 41s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Failing after 41s
Test / API Integration Tests (push) Successful in 59s
116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { Response } from 'express'
|
|
import { z } from 'zod'
|
|
import { AppError, ConflictError, ForbiddenError, NotFoundError, UnauthorizedError, ValidationError } from './index'
|
|
import { errorMiddleware } from './errorMiddleware'
|
|
|
|
function createResponseStub() {
|
|
const res = {
|
|
status: vi.fn(),
|
|
json: vi.fn(),
|
|
}
|
|
|
|
res.status.mockReturnValue(res)
|
|
res.json.mockReturnValue(res)
|
|
|
|
return res as unknown as Response & typeof res
|
|
}
|
|
|
|
function handle(error: unknown) {
|
|
const res = createResponseStub()
|
|
errorMiddleware(error, {} as any, res, vi.fn())
|
|
return res
|
|
}
|
|
|
|
describe('AppError subclasses', () => {
|
|
it.each([
|
|
[new ValidationError('Bad payload'), 400, 'validation_error'],
|
|
[new UnauthorizedError(), 401, 'unauthorized'],
|
|
[new ForbiddenError(), 403, 'forbidden'],
|
|
[new NotFoundError(), 404, 'not_found'],
|
|
[new ConflictError(), 409, 'conflict'],
|
|
])('sets stable status and error code for %s', (error, statusCode, code) => {
|
|
expect(error).toBeInstanceOf(AppError)
|
|
expect(error.statusCode).toBe(statusCode)
|
|
expect(error.error).toBe(code)
|
|
})
|
|
})
|
|
|
|
describe('errorMiddleware', () => {
|
|
it('normalizes Zod errors into validation responses', () => {
|
|
const result = z.object({ email: z.string().email() }).safeParse({ email: 'not-an-email' })
|
|
expect(result.success).toBe(false)
|
|
|
|
const res = handle(result.success ? new Error('unexpected') : result.error)
|
|
|
|
expect(res.status).toHaveBeenCalledWith(400)
|
|
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
|
|
error: 'validation_error',
|
|
message: 'Invalid request body',
|
|
statusCode: 400,
|
|
issues: expect.any(Array),
|
|
}))
|
|
})
|
|
|
|
it('normalizes Prisma not found errors', () => {
|
|
const res = handle({ code: 'P2025' })
|
|
|
|
expect(res.status).toHaveBeenCalledWith(404)
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'not_found',
|
|
message: 'Resource not found',
|
|
statusCode: 404,
|
|
})
|
|
})
|
|
|
|
it('normalizes Prisma unique constraint errors', () => {
|
|
const res = handle({ code: 'P2002' })
|
|
|
|
expect(res.status).toHaveBeenCalledWith(409)
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'conflict',
|
|
message: 'A resource with this value already exists',
|
|
statusCode: 409,
|
|
})
|
|
})
|
|
|
|
it.each(['P2021', 'P2022'])('normalizes Prisma schema mismatch errors for %s', (code) => {
|
|
const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
|
|
|
const res = handle({ code })
|
|
|
|
expect(res.status).toHaveBeenCalledWith(503)
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'database_schema_mismatch',
|
|
message: 'Database schema is out of date. Run database migrations and retry.',
|
|
statusCode: 503,
|
|
requestId: undefined,
|
|
})
|
|
|
|
spy.mockRestore()
|
|
})
|
|
|
|
it('preserves AppError metadata in the response body', () => {
|
|
const res = handle(new AppError('Plan required', 402, 'payment_required', { requiredPlan: 'PRO' }))
|
|
|
|
expect(res.status).toHaveBeenCalledWith(402)
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'payment_required',
|
|
message: 'Plan required',
|
|
statusCode: 402,
|
|
requiredPlan: 'PRO',
|
|
})
|
|
})
|
|
|
|
it('falls back to a 500 internal error response', () => {
|
|
const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
|
|
|
const res = handle(new Error('boom'))
|
|
|
|
expect(res.status).toHaveBeenCalledWith(500)
|
|
expect(res.json).toHaveBeenCalledWith({ error: 'internal_error', message: 'Internal server error', statusCode: 500, requestId: undefined })
|
|
|
|
spy.mockRestore()
|
|
})
|
|
})
|