2026-01-23 01:21:10 -03:00
|
|
|
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
2026-01-26 00:34:08 -03:00
|
|
|
import 'package:drift_offline_sync/drift_offline_sync.dart';
|
2026-01-23 01:21:10 -03:00
|
|
|
import 'package:test/test.dart';
|
|
|
|
|
|
|
|
|
|
import '../utils/crdt_context.dart';
|
|
|
|
|
import '../utils/database.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
group('Given CRDT for an empty database', () {
|
|
|
|
|
late OfflineSyncCrdt crdt;
|
|
|
|
|
|
|
|
|
|
setUp(() async {
|
|
|
|
|
(crdt, _, _) = database.crdtContext;
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-25 23:23:47 -03:00
|
|
|
test('when getting a changeset '
|
2026-01-23 01:21:10 -03:00
|
|
|
'then it returns an empty changeset.', () async {
|
|
|
|
|
final changeset = await crdt.getChangeset();
|
|
|
|
|
|
|
|
|
|
expect(changeset, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-25 23:23:47 -03:00
|
|
|
test('when adding data to the database '
|
2026-01-23 01:21:10 -03:00
|
|
|
'then the changeset contains the data.', () async {
|
|
|
|
|
await database.managers.todosTable.create(
|
|
|
|
|
(t) => t(content: 'test'),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final changeset = await crdt.getChangeset();
|
|
|
|
|
expect(changeset, isNotEmpty);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
group('Given CRDT for a database with data', () {
|
|
|
|
|
late OfflineSyncCrdt crdt;
|
|
|
|
|
late CrdtDatabase crdtDb;
|
|
|
|
|
late String nodeId;
|
|
|
|
|
late String rowId;
|
|
|
|
|
late Hlc createdHlc;
|
|
|
|
|
late TodoEntry createdData;
|
|
|
|
|
|
|
|
|
|
setUp(() async {
|
|
|
|
|
final createdRowId = await database.managers.todosTable.create(
|
|
|
|
|
(t) => t(content: 'test', targetDate: Value(DateTime.now())),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
(crdt, crdtDb, nodeId) = database.crdtContext;
|
|
|
|
|
|
|
|
|
|
final allCrdtDataEntries = await crdtDb.managers.crdtDataTable.get();
|
|
|
|
|
createdHlc = allCrdtDataEntries.first.hlcTimestamp;
|
|
|
|
|
rowId = createdRowId.toString();
|
|
|
|
|
createdData = await database.managers.todosTable.getSingle();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
group('when getting a changeset', () {
|
|
|
|
|
group('without any filters', () {
|
|
|
|
|
test(
|
2026-01-25 23:23:47 -03:00
|
|
|
'then it returns a changeset with one entry per column of the created data.',
|
|
|
|
|
() async {
|
|
|
|
|
final changeset = await crdt.getChangeset();
|
|
|
|
|
final changesetColumns = changeset.map((e) => e.columnName).toSet();
|
2026-01-27 15:31:55 -03:00
|
|
|
final expectedColumns = [
|
|
|
|
|
...database.todosTable.$columns.map((c) => c.$name),
|
|
|
|
|
crdtDb.sqlBuilder.isDeletedColumnName,
|
|
|
|
|
];
|
2026-01-23 01:21:10 -03:00
|
|
|
|
2026-01-25 23:23:47 -03:00
|
|
|
expect(changeset.length, expectedColumns.length);
|
|
|
|
|
expect(changesetColumns, equals(expectedColumns));
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-01-23 01:21:10 -03:00
|
|
|
|
|
|
|
|
test('then the raw values are set correctly.', () async {
|
|
|
|
|
final changeset = await crdt.getChangeset();
|
|
|
|
|
final rebuiltData = database.todosTable.map({
|
|
|
|
|
for (final c in database.todosTable.$columns)
|
|
|
|
|
c.$name: changeset
|
|
|
|
|
.firstWhere((e) => e.rowId == rowId && e.columnName == c.$name)
|
|
|
|
|
.rawValue
|
|
|
|
|
?.rawSqlValue,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(rebuiltData, equals(createdData));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-25 23:23:47 -03:00
|
|
|
test('with onlyNodeId set to the nodeId '
|
2026-01-23 01:21:10 -03:00
|
|
|
'then it returns a changeset with the data for the nodeId.', () async {
|
|
|
|
|
final changeset = await crdt.getChangeset(onlyNodeId: nodeId);
|
|
|
|
|
final nodeIds = changeset.map((e) => e.hlcTimestamp.nodeId).toSet();
|
|
|
|
|
|
|
|
|
|
expect(changeset, isNotEmpty);
|
|
|
|
|
expect(nodeIds, equals({nodeId}));
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-25 23:23:47 -03:00
|
|
|
test('with onlyNodeId set to a different nodeId '
|
2026-01-23 01:21:10 -03:00
|
|
|
'then it returns an empty changeset.', () async {
|
|
|
|
|
final changeset = await crdt.getChangeset(onlyNodeId: 'wrong-node');
|
|
|
|
|
|
|
|
|
|
expect(changeset, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-25 23:23:47 -03:00
|
|
|
test('with exceptNodeId set to the nodeId '
|
2026-01-23 01:21:10 -03:00
|
|
|
'then it returns an empty changeset.', () async {
|
|
|
|
|
final changeset = await crdt.getChangeset(exceptNodeId: nodeId);
|
|
|
|
|
|
|
|
|
|
expect(changeset, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-25 23:23:47 -03:00
|
|
|
test('with exceptNodeId set to a different nodeId '
|
2026-01-23 01:21:10 -03:00
|
|
|
'then it returns a changeset with data from other nodes.', () async {
|
|
|
|
|
const otherNodeId = 'other-node';
|
|
|
|
|
final changeset = await crdt.getChangeset(exceptNodeId: otherNodeId);
|
|
|
|
|
final nodeIds = changeset.map((e) => e.hlcTimestamp.nodeId).toSet();
|
|
|
|
|
|
|
|
|
|
expect(changeset, isNotEmpty);
|
|
|
|
|
expect(nodeIds, isNot(contains(otherNodeId)));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test(
|
2026-01-25 23:23:47 -03:00
|
|
|
'with modifiedAfter set to a timestamp before the created data '
|
|
|
|
|
'then it returns a changeset with the entries from the created data.',
|
|
|
|
|
() async {
|
|
|
|
|
final earlierHlc = Hlc.zero(nodeId);
|
|
|
|
|
final changeset = await crdt.getChangeset(modifiedAfter: earlierHlc);
|
|
|
|
|
|
|
|
|
|
expect(changeset, isNotEmpty);
|
|
|
|
|
expect(changeset.map((e) => e.rowId).toSet(), equals({rowId}));
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-01-23 01:21:10 -03:00
|
|
|
|
2026-01-25 23:23:47 -03:00
|
|
|
test('with modifiedAfter set to a timestamp after the created data '
|
2026-01-23 01:21:10 -03:00
|
|
|
'then it returns an empty changeset.', () async {
|
|
|
|
|
final futureHlc = createdHlc.increment();
|
|
|
|
|
final changeset = await crdt.getChangeset(modifiedAfter: futureHlc);
|
|
|
|
|
|
|
|
|
|
expect(changeset, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|