yx_tracking_flutter/test/sqflite_storage_test.dart

373 lines
11 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String?> 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: <String, Object?>{'i': ts},
customTags: const <String, Object?>{'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), <String>['A', 'B']);
final recent = await storage.fetchRecent(10);
expect(recent.map((e) => e.event.eventType), <String>['B', 'A']);
await storage.deleteByIds(const <int>[]);
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), <String>['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), <String>['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',
<String, Object?>{
'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), <String>['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(<int>[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',
<String, Object?>{
'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: <String, Object?>{'a': 1}),
eventDefinitions: const <EventDefinition>[
EventDefinition(eventCode: 'E'),
],
tagDefinitions: const <TagDefinition>[
TagDefinition(tagName: 't', tagType: 'string', isRequired: true),
],
sdkStrategy: const SdkStrategy(
enabled: true,
defaultSampleRate: 1,
eventSettings: <String, EventStrategy>{},
),
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',
<String, Object?>{
'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',
<String, Object?>{
'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',
<String, Object?>{
'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');
});
});
}