Skip to content

Commit 1fb448f

Browse files
committed
add sync model round-trip test
1 parent 637a7c0 commit 1fb448f

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

test/backup/v2_roundtrip_test.dart

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import "dart:convert";
2+
import "dart:io";
3+
4+
import "package:flow/entity/account.dart";
5+
import "package:flow/entity/category.dart";
6+
import "package:flow/entity/transaction.dart";
7+
import "package:flow/objectbox.dart";
8+
import "package:flow/sync/export/export_v2.dart";
9+
import "package:flow/sync/model/model_v2.dart";
10+
import "package:flutter_test/flutter_test.dart";
11+
import "package:path/path.dart" as path;
12+
13+
import "../database_test.dart" show objectboxTestRootDir;
14+
import "../objectbox_erase.dart";
15+
import "v1_populate.dart";
16+
17+
/// Roundtrip the v2 export through `generateBackupJSONContentV2`
18+
/// `SyncModelV2.fromJson` and assert that every account, category, and
19+
/// transaction survives serialization. Catches the common regression where
20+
/// an entity field is added but `JsonSerializable` codegen isn't re-run,
21+
/// or where the export pipeline forgets to include a new field.
22+
void main() async {
23+
group("Sync V2: JSON export/import roundtrip", () {
24+
const int dummyTransactionCount = 50;
25+
26+
setUpAll(() async {
27+
TestWidgetsFlutterBinding.ensureInitialized();
28+
29+
// Pre-clean so a crashed prior run doesn't bias the dataset.
30+
// `populateDummyData` short-circuits when an "Alpha" account already
31+
// exists; without the wipe, re-runs would append on top of stale data
32+
// and the test would still pass for the wrong reason.
33+
final Directory previous = Directory(
34+
path.join(objectboxTestRootDir().path, "sync/v2"),
35+
);
36+
if (previous.existsSync()) {
37+
previous.deleteSync(recursive: true);
38+
}
39+
40+
await ObjectBox.initialize(
41+
customDirectory: objectboxTestRootDir().path,
42+
subdirectory: "sync/v2",
43+
);
44+
45+
await populateDummyData(dummyTransactionCount);
46+
});
47+
48+
test("Generated JSON parses back into SyncModelV2", () async {
49+
final String jsonContent = await generateBackupJSONContentV2();
50+
final Map<String, dynamic> decoded =
51+
jsonDecode(jsonContent) as Map<String, dynamic>;
52+
53+
expect(decoded["versionCode"], 2);
54+
expect(decoded.containsKey("transactions"), isTrue);
55+
expect(decoded.containsKey("accounts"), isTrue);
56+
expect(decoded.containsKey("categories"), isTrue);
57+
58+
// The actual deserialization — this is what import_v2 will do.
59+
final SyncModelV2 parsed = SyncModelV2.fromJson(decoded);
60+
61+
expect(parsed.versionCode, 2);
62+
expect(parsed.accounts, isNotEmpty);
63+
expect(parsed.categories, isNotEmpty);
64+
expect(parsed.transactions, isNotEmpty);
65+
});
66+
67+
test(
68+
"Exported entity counts match what's in the ObjectBox store",
69+
() async {
70+
final int expectedAccounts = ObjectBox().box<Account>().count();
71+
final int expectedCategories = ObjectBox().box<Category>().count();
72+
final int expectedTransactions = ObjectBox().box<Transaction>().count();
73+
74+
final SyncModelV2 parsed = SyncModelV2.fromJson(
75+
jsonDecode(await generateBackupJSONContentV2())
76+
as Map<String, dynamic>,
77+
);
78+
79+
expect(parsed.accounts.length, expectedAccounts);
80+
expect(parsed.categories.length, expectedCategories);
81+
expect(parsed.transactions.length, expectedTransactions);
82+
},
83+
);
84+
85+
test(
86+
"Every account uuid + name + currency survives roundtrip",
87+
() async {
88+
final List<Account> originals = await ObjectBox()
89+
.box<Account>()
90+
.getAllAsync();
91+
final SyncModelV2 parsed = SyncModelV2.fromJson(
92+
jsonDecode(await generateBackupJSONContentV2())
93+
as Map<String, dynamic>,
94+
);
95+
96+
final Map<String, Account> byUuid = {
97+
for (final a in parsed.accounts) a.uuid: a,
98+
};
99+
100+
for (final original in originals) {
101+
final Account? roundtripped = byUuid[original.uuid];
102+
expect(
103+
roundtripped,
104+
isNotNull,
105+
reason: "Account ${original.uuid} (${original.name}) lost",
106+
);
107+
expect(roundtripped!.name, original.name);
108+
expect(roundtripped.currency, original.currency);
109+
}
110+
},
111+
);
112+
113+
test(
114+
"Every category uuid + name survives roundtrip",
115+
() async {
116+
final List<Category> originals = await ObjectBox()
117+
.box<Category>()
118+
.getAllAsync();
119+
final SyncModelV2 parsed = SyncModelV2.fromJson(
120+
jsonDecode(await generateBackupJSONContentV2())
121+
as Map<String, dynamic>,
122+
);
123+
124+
final Map<String, Category> byUuid = {
125+
for (final c in parsed.categories) c.uuid: c,
126+
};
127+
128+
for (final original in originals) {
129+
final Category? roundtripped = byUuid[original.uuid];
130+
expect(
131+
roundtripped,
132+
isNotNull,
133+
reason: "Category ${original.uuid} (${original.name}) lost",
134+
);
135+
expect(roundtripped!.name, original.name);
136+
}
137+
},
138+
);
139+
140+
test(
141+
"Every transaction uuid + amount + currency + date survives roundtrip",
142+
() async {
143+
final List<Transaction> originals = await ObjectBox()
144+
.box<Transaction>()
145+
.getAllAsync();
146+
final SyncModelV2 parsed = SyncModelV2.fromJson(
147+
jsonDecode(await generateBackupJSONContentV2())
148+
as Map<String, dynamic>,
149+
);
150+
151+
final Map<String, Transaction> byUuid = {
152+
for (final t in parsed.transactions) t.uuid: t,
153+
};
154+
155+
for (final original in originals) {
156+
final Transaction? roundtripped = byUuid[original.uuid];
157+
expect(
158+
roundtripped,
159+
isNotNull,
160+
reason: "Transaction ${original.uuid} lost",
161+
);
162+
expect(roundtripped!.amount, original.amount);
163+
expect(roundtripped.currency, original.currency);
164+
// Compare ISO 8601 strings to avoid microsecond drift across JSON
165+
// boundaries.
166+
expect(
167+
roundtripped.transactionDate.toUtc().toIso8601String(),
168+
original.transactionDate.toUtc().toIso8601String(),
169+
);
170+
}
171+
},
172+
);
173+
174+
tearDownAll(() async {
175+
await testCleanupObject(
176+
instance: ObjectBox(),
177+
directory: ObjectBox.appDataDirectory,
178+
cleanUp: true,
179+
);
180+
});
181+
});
182+
}

0 commit comments

Comments
 (0)