yx_tracking_flutter/test/api_client_test.dart

178 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',
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>()),
);
});
});
}