127 lines
4.6 KiB
Dart
127 lines
4.6 KiB
Dart
// 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<bool> 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<String, dynamic>;
|
|
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();
|
|
});
|
|
}
|