refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)
配置 SSOT(TOML + .env) 统一错误契约 Auth 与事务边界 Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client 可观测性(OpenTelemetry + LGTM)
This commit is contained in:
55
app-expo/tests/core/api/parseApiError.test.ts
Normal file
55
app-expo/tests/core/api/parseApiError.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { parseApiError } from '@/core/api/parseApiError';
|
||||
|
||||
describe('parseApiError', () => {
|
||||
it('prefers unified message field', () => {
|
||||
expect(
|
||||
parseApiError(
|
||||
{
|
||||
error_code: 'NOT_FOUND',
|
||||
message: '资源不存在',
|
||||
request_id: 'r1',
|
||||
},
|
||||
'fallback',
|
||||
),
|
||||
).toEqual({
|
||||
message: '资源不存在',
|
||||
errorCode: 'NOT_FOUND',
|
||||
requestId: 'r1',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to legacy detail string', () => {
|
||||
expect(parseApiError({ detail: '请求无效' }, 'HTTP 400')).toEqual({
|
||||
message: '请求无效',
|
||||
errorCode: undefined,
|
||||
requestId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses legacy detail validation list', () => {
|
||||
expect(
|
||||
parseApiError(
|
||||
{
|
||||
detail: [{ loc: ['body', 'phone'], msg: 'field required' }],
|
||||
},
|
||||
'HTTP 422',
|
||||
),
|
||||
).toEqual({
|
||||
message: 'field required',
|
||||
errorCode: undefined,
|
||||
requestId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses fallback when message is missing', () => {
|
||||
expect(parseApiError({ error_code: 'INTERNAL_ERROR' }, 'HTTP 500')).toEqual({
|
||||
message: 'HTTP 500',
|
||||
errorCode: 'INTERNAL_ERROR',
|
||||
requestId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses fallback when body is null', () => {
|
||||
expect(parseApiError(null, 'HTTP 502')).toEqual({ message: 'HTTP 502' });
|
||||
});
|
||||
});
|
||||
46
app-expo/tests/core/auth/refresh-lock.test.ts
Normal file
46
app-expo/tests/core/auth/refresh-lock.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
resetRefreshLockForTests,
|
||||
withRefreshLock,
|
||||
} from '@/core/auth/refresh-lock';
|
||||
|
||||
describe('withRefreshLock', () => {
|
||||
beforeEach(() => {
|
||||
resetRefreshLockForTests();
|
||||
});
|
||||
|
||||
it('runs concurrent callers through a single in-flight refresh', async () => {
|
||||
let runs = 0;
|
||||
const fn = jest.fn(async () => {
|
||||
runs += 1;
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
return 'ok';
|
||||
});
|
||||
|
||||
const [a, b, c] = await Promise.all([
|
||||
withRefreshLock(fn),
|
||||
withRefreshLock(fn),
|
||||
withRefreshLock(fn),
|
||||
]);
|
||||
|
||||
expect(a).toBe('ok');
|
||||
expect(b).toBe('ok');
|
||||
expect(c).toBe('ok');
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(runs).toBe(1);
|
||||
});
|
||||
|
||||
it('propagates errors to queued waiters', async () => {
|
||||
const fn = jest.fn(async () => {
|
||||
throw new Error('refresh failed');
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
withRefreshLock(fn),
|
||||
withRefreshLock(fn),
|
||||
]);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(results[0].status).toBe('rejected');
|
||||
expect(results[1].status).toBe('rejected');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user