179 lines
5.1 KiB
Dart
179 lines
5.1 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:yx_tracking_flutter/src/config/analytics_config.dart';
|
|
import 'package:yx_tracking_flutter/src/model/device_info.dart';
|
|
import 'package:yx_tracking_flutter/src/model/event.dart';
|
|
import 'package:yx_tracking_flutter/src/network/api_client.dart';
|
|
import 'package:yx_tracking_flutter/src/network/http_client.dart';
|
|
|
|
AnalyticsConfig _config() {
|
|
return AnalyticsConfig(
|
|
systemCode: 'SYS',
|
|
endpointBaseUrl: 'https://example.com',
|
|
clientType: 3,
|
|
enableDebug: true,
|
|
);
|
|
}
|
|
|
|
Event _event(String type) {
|
|
return Event(
|
|
systemCode: 'SYS',
|
|
eventType: type,
|
|
userInfo: null,
|
|
clientType: 3,
|
|
clientTimestamp: 1,
|
|
timestamp: '2026-01-01T00:00:00.000Z',
|
|
deviceInfo: const DeviceInfo(
|
|
os: 'test-os',
|
|
model: 'test-model',
|
|
screenResolution: '1x1',
|
|
),
|
|
eventParams: const <String, Object?>{'k': 'v'},
|
|
customTags: const <String, Object?>{'t': 1},
|
|
createTime: DateTime.fromMillisecondsSinceEpoch(1),
|
|
);
|
|
}
|
|
|
|
class _FakeHttpClient extends HttpClient {
|
|
_FakeHttpClient() : super(_config());
|
|
|
|
int callCount = 0;
|
|
Response<dynamic>? response;
|
|
Object? error;
|
|
|
|
@override
|
|
Future<Response<T>> post<T>(
|
|
String path, {
|
|
Object? data,
|
|
Map<String, dynamic>? queryParameters,
|
|
Map<String, Object?>? headers,
|
|
CancelToken? cancelToken,
|
|
}) async {
|
|
callCount += 1;
|
|
if (error != null) {
|
|
final err = error!;
|
|
if (err is Exception) {
|
|
throw err;
|
|
}
|
|
if (err is Error) {
|
|
throw err;
|
|
}
|
|
throw StateError('Unsupported error type: $err');
|
|
}
|
|
final res = response ??
|
|
Response<dynamic>(
|
|
requestOptions: RequestOptions(path: path),
|
|
statusCode: 200,
|
|
data: const <String, Object?>{'ok': true},
|
|
);
|
|
return res as Response<T>;
|
|
}
|
|
}
|
|
|
|
Response<dynamic> _response(int statusCode) {
|
|
return Response<dynamic>(
|
|
requestOptions: RequestOptions(path: '/AddEventListLog'),
|
|
statusCode: statusCode,
|
|
data: <String, Object?>{'status': statusCode},
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
group('ApiClient', () {
|
|
test('空事件列表直接返回', () async {
|
|
final fake = _FakeHttpClient();
|
|
final client = ApiClient(_config(), httpClient: fake);
|
|
|
|
await client.sendBatch(const <Event>[]);
|
|
|
|
expect(fake.callCount, 0);
|
|
});
|
|
|
|
test('2xx 视为成功', () async {
|
|
final fake = _FakeHttpClient()..response = _response(204);
|
|
final client = ApiClient(_config(), httpClient: fake);
|
|
|
|
await client.sendBatch(<Event>[_event('OK')]);
|
|
|
|
expect(fake.callCount, 1);
|
|
});
|
|
|
|
test('4xx 抛出不可重试 ApiException', () async {
|
|
final fake = _FakeHttpClient()..response = _response(400);
|
|
final client = ApiClient(_config(), httpClient: fake);
|
|
|
|
expect(
|
|
() => client.sendBatch(<Event>[_event('BAD')]),
|
|
throwsA(
|
|
isA<ApiException>()
|
|
.having((e) => e.statusCode, 'statusCode', 400)
|
|
.having((e) => e.retryable, 'retryable', isFalse),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('5xx 抛出可重试 ApiException', () async {
|
|
final fake = _FakeHttpClient()..response = _response(503);
|
|
final client = ApiClient(_config(), httpClient: fake);
|
|
|
|
expect(
|
|
() => client.sendBatch(<Event>[_event('RETRY')]),
|
|
throwsA(
|
|
isA<ApiException>()
|
|
.having((e) => e.statusCode, 'statusCode', 503)
|
|
.having((e) => e.retryable, 'retryable', isTrue),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('DioException(HTTP) 会被映射为 ApiException', () async {
|
|
final dioError = DioException(
|
|
requestOptions: RequestOptions(path: '/x'),
|
|
response: _response(500),
|
|
type: DioExceptionType.badResponse,
|
|
);
|
|
final fake = _FakeHttpClient()..error = dioError;
|
|
final client = ApiClient(_config(), httpClient: fake);
|
|
|
|
expect(
|
|
() => client.sendBatch(<Event>[_event('HTTP_ERR')]),
|
|
throwsA(
|
|
isA<ApiException>()
|
|
.having((e) => e.statusCode, 'statusCode', 500)
|
|
.having((e) => e.retryable, 'retryable', isTrue),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('DioException(网络错误) 视为可重试', () async {
|
|
final dioError = DioException(
|
|
requestOptions: RequestOptions(path: '/x'),
|
|
type: DioExceptionType.connectionTimeout,
|
|
error: StateError('timeout'),
|
|
message: 'timeout',
|
|
);
|
|
final fake = _FakeHttpClient()..error = dioError;
|
|
final client = ApiClient(_config(), httpClient: fake);
|
|
|
|
expect(
|
|
() => client.sendBatch(<Event>[_event('NET_ERR')]),
|
|
throwsA(
|
|
isA<ApiException>()
|
|
.having((e) => e.statusCode, 'statusCode', isNull)
|
|
.having((e) => e.retryable, 'retryable', isTrue),
|
|
),
|
|
);
|
|
});
|
|
|
|
test('未知异常会透传', () async {
|
|
final fake = _FakeHttpClient()..error = StateError('boom');
|
|
final client = ApiClient(_config(), httpClient: fake);
|
|
|
|
expect(
|
|
() => client.sendBatch(<Event>[_event('UNKNOWN_ERR')]),
|
|
throwsA(isA<StateError>()),
|
|
);
|
|
});
|
|
});
|
|
}
|