Files
packages.dart/experimental/pocketbase_sync/test/pb_sync_manager_test.dart
T
2026-01-15 14:38:46 -08:00

176 lines
4.8 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'package:pocketbase/pocketbase.dart';
import 'package:pocketbase_sync/pocketbase_sync.dart';
import 'package:pocketbase_sync/sync_managers/in_memory.dart';
// Generate Mocks
@GenerateNiceMocks([MockSpec<PocketBase>(), MockSpec<RecordService>()])
import 'pb_sync_manager_test.mocks.dart';
// Test Model
class Note {
final String id;
final String content;
final String category;
Note({required this.id, required this.content, required this.category});
Map<String, dynamic> toJson() => {
'id': id,
'content': content,
'category': category,
};
static Note fromJson(Map<String, dynamic> json) => Note(
id: json['id'],
content: json['content'],
category: json['category'] ?? 'General',
);
}
void main() {
late MockPocketBase mockPb;
late MockRecordService mockCollection;
late InMemoryRepository<Note> repository;
late PocketBaseSyncManager<Note> manager;
setUp(() {
mockPb = MockPocketBase();
mockCollection = MockRecordService();
repository = InMemoryRepository<Note>();
when(mockPb.collection('notes')).thenReturn(mockCollection);
manager = PocketBaseSyncManager<Note>(
pb: mockPb,
collection: 'notes',
toJson: (n) => n.toJson(),
fromJson: (j) => Note.fromJson(j),
repository: repository,
retentionPeriod: const Duration(days: 30),
);
});
group('Sync Logic', () {
test('Push Update: Sends minimal PATCH using HybridDiffer', () async {
// 1. Setup Synced State
final base = Note(id: 'n1', content: 'Base Content', category: 'Work');
await repository.save(
SyncRecord(
id: 'n1',
data: base,
baseData: base,
serverUpdatedAt: DateTime.parse('2023-01-01 10:00:00'),
isDirty: false,
),
);
// 2. Local Update (Only change category)
await manager.update(
'n1',
Note(id: 'n1', content: 'Base Content', category: 'Home'),
);
// 3. Mock Response
when(mockCollection.update(any, body: anyNamed('body'))).thenAnswer(
(_) async => RecordModel.fromJson(
{'id': 'n1', 'updated': '2023-01-01 10:05:00'}),
);
// Mock ID List (Server has n1)
when(mockCollection.getFullList(fields: 'id')).thenAnswer(
(_) async => [
RecordModel.fromJson({'id': 'n1'})
],
);
// 4. Sync
await manager.sync();
// 5. Verify Payload
final captured = verify(
mockCollection.update('n1', body: captureAnyNamed('body')),
).captured;
final patch = captured.first as Map<String, dynamic>;
expect(patch.containsKey('category'), isTrue);
expect(
patch.containsKey('content'),
isFalse,
reason: "Content didn't change",
);
});
test('First Pull: Fetches ALL records (getFullList)', () async {
// 1. Mock Server returning list
final remoteItems = [
RecordModel.fromJson({
'id': 'n1',
'updated': '2023-01-01 10:00:00',
'content': 'A',
}),
];
when(
mockCollection.getFullList(filter: anyNamed('filter')),
).thenAnswer((_) async => remoteItems);
// Mock ID List (Server has n1)
when(mockCollection.getFullList(fields: 'id')).thenAnswer(
(_) async => [
RecordModel.fromJson({'id': 'n1'})
],
);
// 2. Sync
await manager.sync();
// 3. Verify Repository Populated
final all = await repository.getAll();
expect(all.length, equals(1));
expect(all.first.data.content, equals('A'));
});
});
group('Retention Policy', () {
test('Cleanup: Removes expired tombstones', () async {
// Expired Tombstone
await repository.save(
SyncRecord(
id: 'del',
data: Note(id: 'del', content: '', category: ''),
serverUpdatedAt: DateTime.now(),
isDeleted: true,
isDirty: false,
deletedAt: DateTime.now().subtract(const Duration(days: 31)),
),
);
// Active Tombstone
await repository.save(
SyncRecord(
id: 'active',
data: Note(id: 'active', content: '', category: ''),
serverUpdatedAt: DateTime.now(),
isDeleted: true,
isDirty: false,
deletedAt: DateTime.now().subtract(const Duration(days: 1)),
),
);
await manager.sync(); // Triggers cleanup at end
expect(
await repository.get('del'),
isNull,
reason: "Should be hard deleted",
);
expect(
await repository.get('active'),
isNotNull,
reason: "Should be kept",
);
});
});
}