feat(stability): unify readiness and recovery
CI - Build & Test / Backend (.NET) (push) Successful in 45s
CI - Build & Test / Backend integration (PostgreSQL/Toxiproxy) (push) Failing after 1m0s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m49s
CI - Build & Test / Security Check (push) Successful in 7s
CI - Build & Test / Deploy Nexus (push) Has been skipped
CI - Build & Test / Backend (.NET) (push) Successful in 45s
CI - Build & Test / Backend integration (PostgreSQL/Toxiproxy) (push) Failing after 1m0s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m49s
CI - Build & Test / Security Check (push) Successful in 7s
CI - Build & Test / Deploy Nexus (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AppProblem, ApiProblem, asProblemDetails, toAppProblem } from '../src/api/contracts'
|
||||
|
||||
describe('AppProblem', () => {
|
||||
it('preserves structured conflict recovery metadata', () => {
|
||||
const problem = new AppProblem(409, {
|
||||
code: 'conflict',
|
||||
detail: 'The resource changed.',
|
||||
traceId: 'trace-1',
|
||||
operationId: 'operation-1',
|
||||
currentRevision: 7,
|
||||
}, 'fallback')
|
||||
|
||||
expect(problem.kind).toBe('conflict')
|
||||
expect(problem.recoveryAction).toBe('reload-conflict')
|
||||
expect(problem.traceId).toBe('trace-1')
|
||||
expect(problem.operationId).toBe('operation-1')
|
||||
expect(problem.currentRevision).toBe(7)
|
||||
})
|
||||
|
||||
it('maps network failures to a recoverable offline state', () => {
|
||||
const problem = toAppProblem(new TypeError('Failed to fetch'))
|
||||
|
||||
expect(problem.kind).toBe('offline')
|
||||
expect(problem.recoveryAction).toBe('inspect-connection')
|
||||
})
|
||||
|
||||
it('does not turn missing conflict metadata into revision zero', () => {
|
||||
const problem = new AppProblem(409, { code: 'conflict' }, 'fallback')
|
||||
|
||||
expect(problem.currentRevision).toBeNull()
|
||||
expect(problem.retryAfterSeconds).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the legacy ApiProblem compatible with the common contract', () => {
|
||||
const problem = new ApiProblem(401, { detail: 'Session expired.' }, 'fallback')
|
||||
|
||||
expect(problem).toBeInstanceOf(AppProblem)
|
||||
expect(problem.kind).toBe('authentication')
|
||||
expect(problem.recoveryAction).toBe('reauthenticate')
|
||||
})
|
||||
|
||||
it('normalizes a legacy message payload at the frontend boundary', () => {
|
||||
const problem = asProblemDetails({ message: 'Gateway unavailable.' }, 503)
|
||||
|
||||
expect(problem?.detail).toBe('Gateway unavailable.')
|
||||
expect(problem?.status).toBe(503)
|
||||
})
|
||||
|
||||
it('normalizes a legacy error payload without replacing structured details', () => {
|
||||
expect(asProblemDetails({ error: 'Legacy error.' }, 400)?.detail).toBe('Legacy error.')
|
||||
expect(asProblemDetails({ detail: 'Canonical.', error: 'Legacy.' }, 409)?.detail).toBe('Canonical.')
|
||||
})
|
||||
|
||||
it.each([
|
||||
[401, 'authentication', 'reauthenticate'],
|
||||
[403, 'permission', 'none'],
|
||||
[409, 'conflict', 'reload-conflict'],
|
||||
[429, 'rate-limit', 'retry'],
|
||||
[503, 'offline', 'inspect-connection'],
|
||||
[504, 'timeout', 'retry'],
|
||||
] as const)(
|
||||
'maps HTTP %s to the %s recovery presentation',
|
||||
(status, kind, recoveryAction) => {
|
||||
const problem = new AppProblem(status, null, `HTTP ${status}`)
|
||||
|
||||
expect(problem.kind).toBe(kind)
|
||||
expect(problem.recoveryAction).toBe(recoveryAction)
|
||||
},
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user