import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/analytics/analytics_service.dart'; void main() { final uuidV7 = RegExp( r'^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$', ); AnalyticsService buildService({ String Function()? getSessionId, String? anonymousId, }) { var counter = 0; return AnalyticsService( apiBaseUrl: 'http://test', getAccessToken: null, getSessionId: getSessionId ?? () => 'session-${counter++}', anonymousId: anonymousId, ); } group('AnalyticsService', () { test('tracks event with required fields', () async { final service = buildService(getSessionId: () => 'session-a'); await service.trackEvent('auth_login_succeeded', { 'identifierType': 'username', 'durationMs': 123, }); final event = service.pendingEvents.single; expect(event['eventName'], 'auth_login_succeeded'); expect(event['eventVersion'], 1); expect(event['sessionId'], 'session-a'); expect(event['anonymousId'], isNotEmpty); expect(event['clientTs'], isNotEmpty); expect(event['appVersion'], isNotEmpty); expect(event['osVersion'], isNotEmpty); expect(event['props'], {'identifierType': 'username', 'durationMs': 123}); }); test('eventId 为 UUIDv7 且逐事件唯一', () async { final service = buildService(); await service.trackEvent('auth_login_succeeded'); await service.trackEvent('auth_logout'); final ids = service.pendingEvents .map((event) => event['eventId'] as String) .toList(); expect(ids[0], matches(uuidV7)); expect(ids[1], matches(uuidV7)); expect(ids[0], isNot(ids[1])); }); test('同一 tracker 下多事件 sessionId 相同,不再每事件生成', () async { final service = buildService(getSessionId: () => 'tracker-session'); await service.trackEvent('auth_login_succeeded'); await service.trackEvent('page_viewed', {'pageName': 'home'}); await service.trackEvent('auth_logout'); final sessionIds = service.pendingEvents .map((event) => event['sessionId']) .toSet(); expect(sessionIds, {'tracker-session'}); }); test('appVersion 可注入更新(不再硬编码)', () async { final service = buildService(); service.setAppVersion('2.3.4+56'); await service.trackEvent('auth_login_succeeded'); expect(service.pendingEvents.single['appVersion'], '2.3.4+56'); }); test('rejects events with forbidden field patterns', () async { final service = buildService(); // Forbidden field triggers local rejection (no throw, silent drop). await service.trackEvent('auth_login_succeeded', { 'userPassword': 'leak', // Forbidden pattern }); expect(service.pendingEvents, isEmpty); }); test('identify sets userId on subsequent events', () async { final service = buildService(); service.identify('user-123'); await service.trackEvent('auth_login_succeeded'); expect(service.pendingEvents.single['userId'], 'user-123'); }); test('reset clears userId but keeps anonymousId', () async { final service = buildService(anonymousId: 'anon-123'); service.identify('user-123'); service.reset(); await service.trackEvent('auth_logout'); final event = service.pendingEvents.single; expect(event.containsKey('userId'), isFalse); expect(event['anonymousId'], 'anon-123'); }); test('上传失败批次重回队列而非整批丢弃', () async { // apiBaseUrl 指向不可达端口:满 20 条触发 flush 必然失败。 final service = AnalyticsService( apiBaseUrl: 'http://127.0.0.1:1', getAccessToken: null, getSessionId: () => 's', ); for (var i = 0; i < 20; i++) { await service.trackEvent('auth_login_succeeded', {'attemptSeq': i}); } // 第 20 条触发上传,失败后批次应重回队列(M0 行为是整批清空)。 expect(service.pendingEvents.length, 20); }); }); }