275 lines
7.7 KiB
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));
|
|
});
|
|
});
|
|
}
|