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 const 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 {'k': 'v'}, customTags: const {'t': 1}, createTime: DateTime.fromMillisecondsSinceEpoch(1), ); } class _FakeHttpClient extends HttpClient { _FakeHttpClient() : super(_config()); int callCount = 0; Response? response; Object? error; @override Future> post( String path, { Object? data, Map? queryParameters, Map? 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( requestOptions: RequestOptions(path: path), statusCode: 200, data: const {'ok': true}, ); return res as Response; } } Response _response(int statusCode) { return Response( requestOptions: RequestOptions(path: '/AddEventListLog'), statusCode: statusCode, data: {'status': statusCode}, ); } void main() { group('ApiClient', () { test('空事件列表直接返回', () async { final fake = _FakeHttpClient(); final client = ApiClient(_config(), httpClient: fake); await client.sendBatch(const []); expect(fake.callCount, 0); }); test('2xx 视为成功', () async { final fake = _FakeHttpClient()..response = _response(204); final client = ApiClient(_config(), httpClient: fake); await client.sendBatch([_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('BAD')]), throwsA( isA() .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('RETRY')]), throwsA( isA() .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('HTTP_ERR')]), throwsA( isA() .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('NET_ERR')]), throwsA( isA() .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('UNKNOWN_ERR')]), throwsA(isA()), ); }); }); }