// The binding layer's proof: the Dart client drives the C ABI end to end — // handles, error codes, async isolates, and the backup round-trip. The // server-backed part runs only when the spike bunshin is up on 19619. import "dart:convert"; import "dart:io"; import "dart:typed_data"; import "package:flutter_test/flutter_test.dart"; import "package:smol_mail/native/client.dart"; import "package:smol_mail/native/ffi.dart"; const spikeServerKey = "g5ttsssqjq6j37vso3h7otn33zrk6o6r7c6jjm7guxbbijoa5nqq"; const spikeServer = "127.0.0.1"; const spikePort = 19619; Future spikeUp() async { try { final socket = await Socket.connect(spikeServer, spikePort, timeout: const Duration(milliseconds: 300)); socket.destroy(); return true; } on SocketException { return false; } } void main() async { test("handles, error codes and async isolates work end to end", () async { final dir = await Directory.systemTemp.createTemp("native-binding"); final client = FumiNative("${dir.path}/a.db"); await client.open(); client.setMaster(Uint8List.fromList(List.filled(32, 9))); // The account public key crosses as base32, driven from a worker isolate. expect((await client.accountPk()).length, 52); expect(await client.accountAddress(), isNull); // A pinned but dead host fails by code, never by prose: Unreachable (7). await client.pinServer("127.0.0.1", spikeServerKey); client.setMaster(Uint8List.fromList(List.filled(32, 9))); try { await client.register("nobody@127.0.0.1:19629"); fail("register against a dead port must fail"); } on NativeSmolException catch (err) { expect(err.code, smolUnreachable); expect(err.details["host"], "127.0.0.1"); } // Empty folders and contact lookups render, not crash. expect(await client.mail("inbox"), isEmpty); expect((await client.contact("ghost@example.org"))["key"], ""); await client.close(); }); test("the backup round-trip crosses the ABI in both directions", () async { final dir = await Directory.systemTemp.createTemp("native-backup"); final master = Uint8List.fromList(List.generate(32, (i) => i + 3)); final mine = FumiNative("${dir.path}/mine.db"); await mine.open(); mine.setMaster(master); await mine.pinServer("example.org", spikeServerKey); await mine.importContact( "smol://alice@example.org/ayb6y3mwr3cfkcmcaqbn3cgfi6xsrsoqkalykw5s7oqmwqcpnmsq"); final file = await mine.exportBackup(); final restored = FumiNative("${dir.path}/restored.db"); await restored.open(); restored.setMaster(master); final summary = jsonDecode(await restored.importBackup(file)) as Map; expect(summary["contactsAdded"], 1); expect(summary["pinsAdded"], 1); expect(await restored.serverPin("example.org"), spikeServerKey); await mine.close(); await restored.close(); }); test("register, send, fetch and describe against the spike bunshin", skip: await spikeUp() ? false : "no bunshin on 127.0.0.1:19619", () async { final dir = await Directory.systemTemp.createTemp("native-e2e"); // Random masters: bunshin refuses a key already bound under another // username, so identities must be fresh per run. final aliceMaster = Uint8List.fromList(List.generate(32, (i) => i * 7 + DateTime.now().microsecondsSinceEpoch % 251)); final bobMaster = Uint8List.fromList(List.generate(32, (i) => i * 13 + 5)); final run = DateTime.now().millisecondsSinceEpoch.toRadixString(36); final alice = FumiNative("${dir.path}/alice.db"); await alice.open(); alice.setMaster(aliceMaster); await alice.pinServer(spikeServer, spikeServerKey); await alice.register("a$run@$spikeServer:$spikePort"); expect(await alice.accountAddress(), "a$run@$spikeServer:$spikePort"); final bob = FumiNative("${dir.path}/bob.db"); await bob.open(); bob.setMaster(bobMaster); await bob.pinServer(spikeServer, spikeServerKey); await bob.register("b$run@$spikeServer:$spikePort"); final sent = await alice.send("b$run@$spikeServer:$spikePort", "binding: hello over the ABI", subject: "binding"); expect(sent["change"], "new"); final summary = await bob.fetch(); expect(summary["stored"], 1); // First contact lands in the requests tier (sec 5.8). final requests = await bob.mail("requests"); expect(requests, hasLength(1)); final described = await bob.describe(requests[0]["id"] as String); expect(described["text"], "binding: hello over the ABI"); expect(described["subject"], "binding"); await alice.close(); await bob.close(); }); }