fix(api): report database schema mismatches clearly
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

This commit is contained in:
root
2026-07-02 18:40:42 -04:00
parent c21916559f
commit e3f40410e9
3 changed files with 66 additions and 0 deletions
@@ -74,6 +74,22 @@ describe('errorMiddleware', () => {
})
})
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' }))
@@ -23,6 +23,15 @@ export function errorMiddleware(err: any, req: Request, res: Response, _next: Ne
return res.status(409).json(withRequestId(req, { error: 'conflict', message: 'A resource with this value already exists', statusCode: 409 }))
}
if (err.code === 'P2021' || err.code === 'P2022') {
console.error('[API Error] Database schema mismatch', { requestId: req.requestId, err })
return res.status(503).json(withRequestId(req, {
error: 'database_schema_mismatch',
message: 'Database schema is out of date. Run database migrations and retry.',
statusCode: 503,
}))
}
if (err instanceof AppError) {
if (err.statusCode >= 500) console.error('[API Error]', { requestId: req.requestId, err })
return res.status(err.statusCode).json(withRequestId(req, {
@@ -29,6 +29,10 @@ vi.mock('../../modules/auth/auth.company.service', () => ({
}),
}))
vi.mock('../../modules/auth/auth.account.service', () => ({
startAccount: vi.fn(),
}))
vi.mock('../../modules/auth/auth.renter.service', () => ({
signupDisabled: vi.fn(() => {
const err = new Error('renter disabled') as Error & { statusCode?: number; code?: string }
@@ -49,6 +53,7 @@ vi.mock('../../modules/auth/auth.renter.service', () => ({
import request from 'supertest'
import { createApp } from '../../app'
import * as accountService from '../../modules/auth/auth.account.service'
import * as companyService from '../../modules/auth/auth.company.service'
import * as renterService from '../../modules/auth/auth.renter.service'
@@ -90,6 +95,10 @@ const signupPayload = {
describe('auth API boundaries', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(accountService.startAccount).mockResolvedValue({
message: 'Account created. Please check your email to verify your address before signing in.',
email: 'owner@example.com',
} as never)
vi.mocked(companyService.signup).mockResolvedValue({ companyId: 'company_1', nextStep: 'workspace_created' } as never)
vi.mocked(renterService.getMe).mockResolvedValue({ id: 'renter_1', savedCompanies: [] } as never)
vi.mocked(renterService.updateMe).mockResolvedValue({ id: 'renter_1', firstName: 'Nora' } as never)
@@ -117,6 +126,38 @@ describe('auth API boundaries', () => {
expect(companyService.signup).not.toHaveBeenCalled()
})
it('POST /auth/account/start validates minimal signup and returns 201', async () => {
const res = await request(app).post('/api/v1/auth/account/start').send({
email: 'owner@example.com',
password: 'super-secret',
preferredLanguage: 'fr',
})
expect(res.status).toBe(201)
expect(res.body).toEqual({
data: {
message: 'Account created. Please check your email to verify your address before signing in.',
email: 'owner@example.com',
},
})
expect(accountService.startAccount).toHaveBeenCalledWith({
email: 'owner@example.com',
password: 'super-secret',
preferredLanguage: 'fr',
})
})
it('POST /auth/account/start rejects malformed minimal signup payloads before service execution', async () => {
const res = await request(app).post('/api/v1/auth/account/start').send({
email: 'not-email',
password: 'short',
})
expect(res.status).toBe(400)
expect(res.body.error).toBe('validation_error')
expect(accountService.startAccount).not.toHaveBeenCalled()
})
it('preserves removed company auth compatibility endpoints as explicit 410 responses', async () => {
const complete = await request(app).post('/api/v1/auth/company/complete-signup').send({})
const verify = await request(app).post('/api/v1/auth/company/verify-email').send({})