app scaffold
This commit is contained in:
@@ -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('取消抛 RequestCancelledException,UI 必须静默处理', () 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],
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user