feat(GOV-009): 建立前端 OpenAPI 客户端边界
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { components } from '../generated/schema';
|
||||
import { toHealthViewModel } from './health.adapter';
|
||||
|
||||
type HealthResponseDto = components['schemas']['HealthResponseDto'];
|
||||
|
||||
const healthyDto: HealthResponseDto = {
|
||||
status: 'healthy',
|
||||
process: 'api',
|
||||
checkedAt: '2026-08-13T09:30:00.000Z',
|
||||
dependencies: {
|
||||
mysql: { status: 'healthy', detail: 'MySQL 5.7 ready' },
|
||||
redis: { status: 'healthy', detail: 'PONG' },
|
||||
rocketMq: { status: 'healthy', detail: 'RocketMQ ready' },
|
||||
},
|
||||
};
|
||||
|
||||
describe('健康响应页面适配器', () => {
|
||||
it('把全部可用的传输状态映射为健康页面模型', () => {
|
||||
expect(toHealthViewModel(healthyDto)).toEqual({
|
||||
state: 'healthy',
|
||||
stateLabel: '运行正常',
|
||||
checkedAt: new Date('2026-08-13T09:30:00.000Z'),
|
||||
services: [
|
||||
{ key: 'redis', name: 'Redis', available: true, note: 'PONG' },
|
||||
{ key: 'mysql', name: 'MySQL', available: true, note: 'MySQL 5.7 ready' },
|
||||
{ key: 'rocketMq', name: 'RocketMQ', available: true, note: 'RocketMQ ready' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('把可选能力停用映射为降级而不是后端枚举', () => {
|
||||
const dto: HealthResponseDto = {
|
||||
...healthyDto,
|
||||
dependencies: {
|
||||
...healthyDto.dependencies,
|
||||
rocketMq: { status: 'disabled', detail: '当前版本未启用' },
|
||||
},
|
||||
};
|
||||
|
||||
const viewModel = toHealthViewModel(dto);
|
||||
|
||||
expect(viewModel.state).toBe('degraded');
|
||||
expect(viewModel.stateLabel).toBe('部分能力停用');
|
||||
expect(viewModel.services[2]).toEqual({
|
||||
key: 'rocketMq',
|
||||
name: 'RocketMQ',
|
||||
available: false,
|
||||
note: '当前版本未启用',
|
||||
});
|
||||
});
|
||||
|
||||
it('把任一依赖不可用映射为不可用', () => {
|
||||
const dto: HealthResponseDto = {
|
||||
...healthyDto,
|
||||
status: 'unavailable',
|
||||
dependencies: {
|
||||
...healthyDto.dependencies,
|
||||
redis: { status: 'unavailable', detail: '检查失败' },
|
||||
},
|
||||
};
|
||||
|
||||
expect(toHealthViewModel(dto).state).toBe('unavailable');
|
||||
expect(toHealthViewModel(dto).stateLabel).toBe('服务异常');
|
||||
});
|
||||
|
||||
it('拒绝运行时出现的未知依赖状态', () => {
|
||||
const malformedDto = structuredClone(healthyDto) as unknown as {
|
||||
dependencies: { redis: { status: string } };
|
||||
};
|
||||
malformedDto.dependencies.redis.status = 'unknown';
|
||||
|
||||
expect(() => toHealthViewModel(malformedDto as never)).toThrow(/未知依赖状态/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { components } from '../generated/schema';
|
||||
|
||||
type HealthResponseDto = components['schemas']['HealthResponseDto'];
|
||||
type DependencyHealthDto = components['schemas']['DependencyHealthDto'];
|
||||
|
||||
export type HealthViewState = 'healthy' | 'degraded' | 'unavailable';
|
||||
|
||||
export interface HealthServiceViewModel {
|
||||
key: 'redis' | 'mysql' | 'rocketMq';
|
||||
name: 'Redis' | 'MySQL' | 'RocketMQ';
|
||||
available: boolean;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface HealthViewModel {
|
||||
state: HealthViewState;
|
||||
stateLabel: '运行正常' | '部分能力停用' | '服务异常';
|
||||
checkedAt: Date;
|
||||
services: HealthServiceViewModel[];
|
||||
}
|
||||
|
||||
function assertDependencyStatus(status: string): asserts status is DependencyHealthDto['status'] {
|
||||
if (status !== 'healthy' && status !== 'unavailable' && status !== 'disabled') {
|
||||
throw new Error(`未知依赖状态:${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function toServiceViewModel(
|
||||
key: HealthServiceViewModel['key'],
|
||||
name: HealthServiceViewModel['name'],
|
||||
dependency: DependencyHealthDto,
|
||||
): HealthServiceViewModel {
|
||||
assertDependencyStatus(dependency.status);
|
||||
return {
|
||||
key,
|
||||
name,
|
||||
available: dependency.status === 'healthy',
|
||||
note: dependency.detail,
|
||||
};
|
||||
}
|
||||
|
||||
export function toHealthViewModel(dto: HealthResponseDto): HealthViewModel {
|
||||
const dependencies = Object.values(dto.dependencies);
|
||||
for (const dependency of dependencies) assertDependencyStatus(dependency.status);
|
||||
|
||||
const hasUnavailable = dependencies.some((dependency) => dependency.status === 'unavailable');
|
||||
const hasDisabled = dependencies.some((dependency) => dependency.status === 'disabled');
|
||||
const state: HealthViewState =
|
||||
dto.status === 'unavailable' || hasUnavailable
|
||||
? 'unavailable'
|
||||
: hasDisabled
|
||||
? 'degraded'
|
||||
: 'healthy';
|
||||
|
||||
const stateLabel = {
|
||||
healthy: '运行正常',
|
||||
degraded: '部分能力停用',
|
||||
unavailable: '服务异常',
|
||||
} as const;
|
||||
|
||||
// 页面只消费稳定的展示模型,后端枚举和依赖对象结构不会直接扩散到 Vue 组件。
|
||||
return {
|
||||
state,
|
||||
stateLabel: stateLabel[state],
|
||||
checkedAt: new Date(dto.checkedAt),
|
||||
services: [
|
||||
toServiceViewModel('redis', 'Redis', dto.dependencies.redis),
|
||||
toServiceViewModel('mysql', 'MySQL', dto.dependencies.mysql),
|
||||
toServiceViewModel('rocketMq', 'RocketMQ', dto.dependencies.rocketMq),
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user