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 init() async {} @override Future saveSystemDimInfo(SystemDimInfo info) async { _stored = info; } @override Future loadSystemDimInfo() async => _stored; @override Future clear() async { _stored = null; } @override Future dispose() async {} } class FakeHttpClient extends HttpClient { FakeHttpClient(super.config); int getCallCount = 0; dynamic responseData; Headers responseHeaders = Headers(); DioException? exceptionToThrow; @override Future> get( String path, { Map? queryParameters, Map? headers, CancelToken? cancelToken, }) async { getCallCount += 1; final exception = exceptionToThrow; if (exception != null) { throw exception; } return Response( data: responseData as T?, statusCode: 200, headers: responseHeaders, requestOptions: RequestOptions( path: path, queryParameters: queryParameters, ), ); } } class ThrowingHttpClient extends HttpClient { ThrowingHttpClient(super.config); @override Future> get( String path, { Map? queryParameters, Map? 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: {}), eventDefinitions: const [], tagDefinitions: const [], sdkStrategy: null, lastFetchedAt: fetchedAt, ); } void main() { group('ConfigManager', () { test('fetchAndCacheConfig 会解析配置并缓存', () async { final storage = InMemoryConfigStorage(); final httpClient = FakeHttpClient(_config()) ..responseData = { 'data': { 'systemEventTypes': >[ {'eventCode': 'EVENT_A', 'eventName': 'A'}, ], 'systemCustonTas': >[ { 'tagName': 'tenantId', 'tagType': 'string', 'isRequired': true, }, ], 'sdkStrategy': { 'enabled': true, 'defaultSampleRate': 1, 'eventSettings': { 'EVENT_A': { 'enabled': true, 'sampleRate': 0.25, }, }, }, }, } ..responseHeaders = Headers.fromMap(>{ 'x-config-version': ['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 = { 'data': { 1: 'ignored', 'systemEventTypes': const [], 'systemCustonTas': const [], }, }; 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 = { 'systemEventTypes': const [], 'systemCustonTas': const [], }; 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)); }); }); }