import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.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/model/system_dim_info.dart'; import 'package:yx_tracking_flutter/src/storage/db_constants.dart'; import 'package:yx_tracking_flutter/src/storage/sqflite_config_storage.dart'; import 'package:yx_tracking_flutter/src/storage/sqflite_event_storage.dart'; class FakePathProviderPlatform extends PathProviderPlatform { FakePathProviderPlatform(this.documentsPath); final String documentsPath; @override Future getApplicationDocumentsPath() async => documentsPath; } void main() { sqfliteFfiInit(); final ffiFactory = databaseFactoryFfi; group('SqfliteEventStorage (ffi)', () { late Directory tempDir; late PathProviderPlatform originalPathProvider; setUp(() { originalPathProvider = PathProviderPlatform.instance; tempDir = Directory.systemTemp.createTempSync('yx_tracking_event_'); }); tearDown(() { PathProviderPlatform.instance = originalPathProvider; if (tempDir.existsSync()) { tempDir.deleteSync(recursive: true); } }); SqfliteEventStorage makeStorage() { return SqfliteEventStorage( databaseFactory: ffiFactory, documentsDirectoryProvider: () async => tempDir, ); } Event makeEvent(String type, int ts) { return Event( systemCode: 'SYS', eventType: type, userInfo: null, clientType: 3, clientTimestamp: ts, timestamp: '2026-01-01T00:00:00.000Z', deviceInfo: const DeviceInfo( os: 'os', model: 'model', screenResolution: '1x1', ), eventParams: {'i': ts}, customTags: const {'t': 1}, createTime: DateTime.fromMillisecondsSinceEpoch(ts), ); } test('未 init 时会抛 StateError', () { final storage = makeStorage(); expect(storage.count, throwsStateError); }); test('基础 CRUD + limit<=0 分支 + init 幂等', () async { final storage = makeStorage(); await storage.init(); await storage.init(); expect(await storage.fetchBatch(0), isEmpty); expect(await storage.fetchRecent(0), isEmpty); final id1 = await storage.insert(makeEvent('A', 1)); final id2 = await storage.insert(makeEvent('B', 2)); expect(id1, greaterThan(0)); expect(id2, greaterThan(id1)); final batch = await storage.fetchBatch(10); expect(batch.map((e) => e.event.eventType), ['A', 'B']); final recent = await storage.fetchRecent(10); expect(recent.map((e) => e.event.eventType), ['B', 'A']); await storage.deleteByIds(const []); await storage.updateRetryCount(id1, 1); await storage.dispose(); await storage.dispose(); }); test('trimToMaxSize 会删除最旧事件并返回删除数', () async { final storage = makeStorage(); await storage.init(); await storage.insert(makeEvent('A', 1)); await storage.insert(makeEvent('B', 2)); await storage.insert(makeEvent('C', 3)); final trimmed = await storage.trimToMaxSize(2); expect(trimmed, 1); final left = await storage.fetchBatch(10); expect(left.map((e) => e.event.eventType), ['B', 'C']); }); test('deleteExpired 会删除截止时间之前的事件', () async { final storage = makeStorage(); await storage.init(); await storage.insert(makeEvent('OLD', 1)); await storage.insert(makeEvent('NEW', 100)); final removed = await storage.deleteExpired( DateTime.fromMillisecondsSinceEpoch(50), ); expect(removed, 1); final left = await storage.fetchBatch(10); expect(left.map((e) => e.event.eventType), ['NEW']); }); test('updateRetryCount 会同时更新 stored 与 event.retryCount', () async { final storage = makeStorage(); await storage.init(); final id = await storage.insert(makeEvent('R', 10)); final stored = (await storage.fetchBatch(1)).single; expect(stored.retryCount, 0); expect(stored.event.retryCount, 0); await storage.updateRetryCount(id, 2); final updated = (await storage.fetchBatch(1)).single; expect(updated.retryCount, 2); expect(updated.event.retryCount, 2); }); test('坏数据会被清理避免卡死队列', () async { final storage = makeStorage(); await storage.init(); await storage.insert(makeEvent('GOOD', 1)); final db = storage.debugDb!; await db.insert( 'events', { 'payload': 'not-json', 'retry_count': 0, 'create_time': 2, }, ); final before = await storage.count(); final batch = await storage.fetchBatch(10); final after = await storage.count(); expect(before, 2); expect(batch.map((e) => e.event.eventType), ['GOOD']); expect(after, 1); }); test('默认 documents provider 分支可工作(使用 fake 平台)', () async { PathProviderPlatform.instance = FakePathProviderPlatform(tempDir.path); final storage = SqfliteEventStorage(databaseFactory: ffiFactory); await storage.init(); expect(await storage.count(), 0); }); test('onUpgrade(oldVersion<1) 会建表', () async { final dbPath = p.join(tempDir.path, DbConstants.dbName); final db = await ffiFactory.openDatabase( dbPath, options: OpenDatabaseOptions(version: 1), ); await db.execute('PRAGMA user_version = 0;'); await db.close(); final storage = makeStorage(); await storage.init(); expect(await storage.count(), 0); }); test('deleteByIds 多 id 会走占位符分支', () async { final storage = makeStorage(); await storage.init(); final id1 = await storage.insert(makeEvent('D1', 1)); final id2 = await storage.insert(makeEvent('D2', 2)); await storage.deleteByIds([id1, id2]); expect(await storage.count(), 0); }); test('行字段类型异常会被清理(create_time 非 int)', () async { final storage = makeStorage(); await storage.init(); final db = storage.debugDb!; await db.insert( 'events', { 'payload': '{}', 'retry_count': 0, 'create_time': 'bad-int', }, ); final before = await storage.count(); final batch = await storage.fetchBatch(10); final after = await storage.count(); expect(before, 1); expect(batch, isEmpty); expect(after, 0); }); }); group('SqfliteConfigStorage (ffi)', () { late Directory tempDir; late PathProviderPlatform originalPathProvider; setUp(() { originalPathProvider = PathProviderPlatform.instance; tempDir = Directory.systemTemp.createTempSync('yx_tracking_config_'); }); tearDown(() { PathProviderPlatform.instance = originalPathProvider; if (tempDir.existsSync()) { tempDir.deleteSync(recursive: true); } }); SqfliteConfigStorage makeStorage() { return SqfliteConfigStorage( databaseFactory: ffiFactory, documentsDirectoryProvider: () async => tempDir, ); } SystemDimInfo makeInfo(int ts) { return SystemDimInfo( systemInfo: const SystemInfo(raw: {'a': 1}), eventDefinitions: const [ EventDefinition(eventCode: 'E'), ], tagDefinitions: const [ TagDefinition(tagName: 't', tagType: 'string', isRequired: true), ], sdkStrategy: const SdkStrategy( enabled: true, defaultSampleRate: 1, eventSettings: {}, ), lastFetchedAt: DateTime.fromMillisecondsSinceEpoch(ts), version: 'v', ); } test('未 init 时会抛 StateError', () { final storage = makeStorage(); expect(storage.loadSystemDimInfo, throwsStateError); }); test('save/load/clear/lastFetchedAt 正常工作', () async { final storage = makeStorage(); await storage.init(); expect(await storage.loadSystemDimInfo(), isNull); final info = makeInfo(123); await storage.saveSystemDimInfo(info); final loaded = await storage.loadSystemDimInfo(); expect(loaded?.systemInfo?.raw['a'], 1); expect(loaded?.eventDefinitions.single.eventCode, 'E'); final rows = await storage.debugDb!.query('config_cache'); expect( rows.single['last_fetched_at'], info.lastFetchedAt.millisecondsSinceEpoch, ); await storage.clear(); expect(await storage.loadSystemDimInfo(), isNull); await storage.dispose(); await storage.dispose(); }); test('默认 documents provider 分支可工作(使用 fake 平台)', () async { PathProviderPlatform.instance = FakePathProviderPlatform(tempDir.path); final storage = SqfliteConfigStorage(databaseFactory: ffiFactory); await storage.init(); expect(await storage.loadSystemDimInfo(), isNull); }); test('payload 为空字符串会被清理', () async { final storage = makeStorage(); await storage.init(); final db = storage.debugDb!; await db.insert( 'config_cache', { 'key': 'system_dim_info', 'payload': '', 'last_fetched_at': 1, }, ); final loaded = await storage.loadSystemDimInfo(); expect(loaded, isNull); expect(await db.query('config_cache'), isEmpty); }); test('payload 为非 Map JSON 会被清理', () async { final storage = makeStorage(); await storage.init(); final db = storage.debugDb!; await db.insert( 'config_cache', { 'key': 'system_dim_info', 'payload': '[]', 'last_fetched_at': 1, }, ); final loaded = await storage.loadSystemDimInfo(); expect(loaded, isNull); expect(await db.query('config_cache'), isEmpty); }); test('坏配置会被删除并返回 null', () async { final storage = makeStorage(); await storage.init(); final dbPath = p.join(tempDir.path, DbConstants.dbName); final db = storage.debugDb!; await db.insert( 'config_cache', { 'key': 'system_dim_info', 'payload': 'not-json', 'last_fetched_at': 1, }, ); final loaded = await storage.loadSystemDimInfo(); expect(loaded, isNull); final rows = await db.query('config_cache'); expect(rows, isEmpty, reason: '坏数据应被清理: $dbPath'); }); }); }