app scaffold

This commit is contained in:
Guangfei.Zhao
2026-08-17 15:29:55 +08:00
commit 681688dfae
301 changed files with 18414 additions and 0 deletions
@@ -0,0 +1,121 @@
// core_network 的高价值断言:
// 1. ApiClient 真的把 AppException 从 DioException 的壳里抛出来了——
// 这条不成立的话,业务层所有 `on BusinessException` 都是死代码。
// 2. code != 0 不会以"成功响应"的形态流到业务层。
import 'dart:convert';
import 'dart:typed_data';
import 'package:core_foundation/core_foundation.dart';
import 'package:core_network/core_network.dart';
import 'package:core_network/src/api_result_interceptor.dart';
import 'package:core_network/src/error_mapping_interceptor.dart';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
/// 直接吐一段 JSON 的 adapter,省掉起 http server。
class _StubAdapter implements HttpClientAdapter {
_StubAdapter(this.status, this.body);
int status;
Map<String, dynamic> body;
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) async => ResponseBody.fromString(
jsonEncode(body),
status,
headers: <String, List<String>>{
Headers.contentTypeHeader: <String>[Headers.jsonContentType],
},
);
@override
void close({bool force = false}) {}
}
void main() {
late _StubAdapter adapter;
late ApiClient api;
setUp(() {
adapter = _StubAdapter(200, <String, dynamic>{});
final Dio dio = Dio(BaseOptions(baseUrl: 'https://example.test'))
..httpClientAdapter = adapter
..interceptors.addAll(<Interceptor>[
ApiResultInterceptor((String _) {}),
ErrorMappingInterceptor(),
]);
api = ApiClient(dio);
});
test('code == 0 时外层包装被剥掉,repository 拿到的就是 data 本身', () async {
adapter.body = <String, dynamic>{
'code': 0,
'message': 'ok',
'traceId': 't-1',
'data': <String, dynamic>{'storeId': 7},
};
final Map<String, dynamic> data = await api.get<Map<String, dynamic>>('/x');
expect(data, <String, dynamic>{'storeId': 7});
});
test('code != 0 抛 BusinessException 而不是当成功返回,且 traceId 留存', () async {
adapter.body = <String, dynamic>{
'code': 40001,
'message': '门店不可访问',
'traceId': 't-2',
'data': null,
};
await expectLater(
api.get<Map<String, dynamic>>('/x'),
throwsA(
isA<BusinessException>()
.having((BusinessException e) => e.code, 'code', 40001)
.having((BusinessException e) => e.message, 'message', '门店不可访问')
// 用户报一个 traceId 后端就能定位这次请求,丢了就没法排查。
.having((BusinessException e) => e.traceId, 'traceId', 't-2'),
),
);
});
test('5xx 归一化成 ServerException——业务层只认 AppException 体系', () async {
adapter
..status = 500
..body = <String, dynamic>{'error': 'boom'};
await expectLater(
api.get<Map<String, dynamic>>('/x'),
throwsA(
isA<ServerException>().having((ServerException e) => e.statusCode, 'statusCode', 500),
),
);
});
test('取消抛 RequestCancelledExceptionUI 必须静默处理', () async {
final CancelToken token = CancelToken();
final Future<Map<String, dynamic>> future = api.get<Map<String, dynamic>>(
'/x',
cancelToken: token,
);
token.cancel();
await expectLater(future, throwsA(isA<RequestCancelledException>()));
});
test('非 ApiResult 结构(文件下载等)原样透传,不被误解包', () async {
adapter.body = <String, dynamic>{
'items': <int>[1, 2],
};
final Map<String, dynamic> data = await api.get<Map<String, dynamic>>('/x');
expect(data, <String, dynamic>{
'items': <int>[1, 2],
});
});
}