yx_tracking_flutter/test/config_manager_test.dart

275 lines
7.7 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/config/config_manager.dart';
import 'package:yx_tracking_flutter/src/model/system_dim_info.dart';
import 'package:yx_tracking_flutter/src/network/http_client.dart';
import 'package:yx_tracking_flutter/src/storage/config_storage.dart';
class InMemoryConfigStorage implements ConfigStorage {
SystemDimInfo? _stored;
@override
Future<void> init() async {}
@override
Future<void> saveSystemDimInfo(SystemDimInfo info) async {
_stored = info;
}
@override
Future<SystemDimInfo?> loadSystemDimInfo() async => _stored;
@override
Future<void> clear() async {
_stored = null;
}
@override
Future<void> dispose() async {}
}
class FakeHttpClient extends HttpClient {
FakeHttpClient(super.config);
int getCallCount = 0;
dynamic responseData;
Headers responseHeaders = Headers();
DioException? exceptionToThrow;
@override
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Map<String, Object?>? headers,
CancelToken? cancelToken,
}) async {
getCallCount += 1;
final exception = exceptionToThrow;
if (exception != null) {
throw exception;
}
return Response<T>(
data: responseData as T?,
statusCode: 200,
headers: responseHeaders,
requestOptions: RequestOptions(
path: path,
queryParameters: queryParameters,
),
);
}
}
class ThrowingHttpClient extends HttpClient {
ThrowingHttpClient(super.config);
@override
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Map<String, Object?>? headers,
CancelToken? cancelToken,
}) async {
throw StateError('boom');
}
}
AnalyticsConfig _config() {
return AnalyticsConfig(
systemCode: 'TEST_APP',
endpointBaseUrl: 'https://example.com',
enableMetrics: false,
);
}
SystemDimInfo _storedConfig(DateTime fetchedAt) {
return SystemDimInfo(
systemInfo: const SystemInfo(raw: <String, dynamic>{}),
eventDefinitions: const <EventDefinition>[],
tagDefinitions: const <TagDefinition>[],
sdkStrategy: null,
lastFetchedAt: fetchedAt,
);
}
void main() {
group('ConfigManager', () {
test('fetchAndCacheConfig 会解析配置并缓存', () async {
final storage = InMemoryConfigStorage();
final httpClient = FakeHttpClient(_config())
..responseData = <String, dynamic>{
'data': <String, dynamic>{
'systemEventTypes': <Map<String, dynamic>>[
<String, dynamic>{'eventCode': 'EVENT_A', 'eventName': 'A'},
],
'systemCustonTas': <Map<String, dynamic>>[
<String, dynamic>{
'tagName': 'tenantId',
'tagType': 'string',
'isRequired': true,
},
],
'sdkStrategy': <String, dynamic>{
'enabled': true,
'defaultSampleRate': 1,
'eventSettings': <String, dynamic>{
'EVENT_A': <String, dynamic>{
'enabled': true,
'sampleRate': 0.25,
},
},
},
},
}
..responseHeaders = Headers.fromMap(<String, List<String>>{
'x-config-version': <String>['v1'],
});
final manager = ConfigManager(
config: _config(),
refreshInterval: const Duration(hours: 1),
storage: storage,
httpClient: httpClient,
);
await manager.init();
await manager.fetchAndCacheConfig(force: true);
final current = manager.currentConfig;
expect(current, isNotNull);
expect(current!.eventDefinitions.single.eventCode, 'EVENT_A');
expect(current.requiredTags.single.tagName, 'tenantId');
expect(current.sampleRateFor('EVENT_A'), 0.25);
expect(current.version, 'v1');
});
test('配置未过期时会跳过拉取', () async {
final storage = InMemoryConfigStorage();
final httpClient = FakeHttpClient(_config());
final now = DateTime.now();
await storage.saveSystemDimInfo(_storedConfig(now));
final manager = ConfigManager(
config: _config(),
storage: storage,
httpClient: httpClient,
);
await manager.init();
await manager.fetchAndCacheConfig();
expect(httpClient.getCallCount, 0);
expect(manager.currentConfig, isNotNull);
});
test('响应结构不可解析时保持现有配置', () async {
final storage = InMemoryConfigStorage();
final httpClient = FakeHttpClient(_config());
final existing =
_storedConfig(DateTime.now().subtract(const Duration(days: 1)));
await storage.saveSystemDimInfo(existing);
httpClient.responseData = 'not-a-map';
final manager = ConfigManager(
config: _config(),
refreshInterval: const Duration(hours: 1),
storage: storage,
httpClient: httpClient,
);
await manager.init();
await manager.fetchAndCacheConfig(force: true);
expect(manager.currentConfig, isNotNull);
expect(manager.currentConfig!.lastFetchedAt, existing.lastFetchedAt);
});
test('DioException 会被捕获且不抛出', () async {
final storage = InMemoryConfigStorage();
final httpClient = FakeHttpClient(_config())
..exceptionToThrow = DioException(
requestOptions: RequestOptions(path: '/GetSystemAllDimInfo'),
type: DioExceptionType.connectionTimeout,
message: 'timeout',
);
final manager = ConfigManager(
config: _config(),
refreshInterval: Duration.zero,
storage: storage,
httpClient: httpClient,
);
await manager.init();
await manager.fetchAndCacheConfig(force: true);
expect(httpClient.getCallCount, 1);
expect(manager.currentConfig, isNull);
});
test('未知异常会被捕获且不抛出', () async {
final storage = InMemoryConfigStorage();
final httpClient = ThrowingHttpClient(_config());
final manager = ConfigManager(
config: _config(),
refreshInterval: Duration.zero,
storage: storage,
httpClient: httpClient,
);
await manager.init();
await manager.fetchAndCacheConfig(force: true);
expect(manager.currentConfig, isNull);
});
test('Map 非字符串 key remembers 配置结构', () async {
final storage = InMemoryConfigStorage();
final httpClient = FakeHttpClient(_config())
..responseData = <Object?, Object?>{
'data': <Object?, Object?>{
1: 'ignored',
'systemEventTypes': const <Object?>[],
'systemCustonTas': const <Object?>[],
},
};
final manager = ConfigManager(
config: _config(),
refreshInterval: Duration.zero,
storage: storage,
httpClient: httpClient,
);
await manager.init();
await manager.fetchAndCacheConfig(force: true);
expect(manager.currentConfig, isNotNull);
});
test('forceRefresh 与 dispose 可调用', () async {
final storage = InMemoryConfigStorage();
final httpClient = FakeHttpClient(_config())
..responseData = <String, Object?>{
'systemEventTypes': const <Object?>[],
'systemCustonTas': const <Object?>[],
};
final manager = ConfigManager(
config: _config(),
refreshInterval: Duration.zero,
storage: storage,
httpClient: httpClient,
);
await manager.init();
await manager.forceRefresh();
await manager.dispose();
expect(httpClient.getCallCount, greaterThanOrEqualTo(1));
});
});
}