fix: read back sent copies after the sec 5.6 upstream fix, pinned at 2d65d66

This commit is contained in:
randogoth 2026-09-28 23:49:39 +03:00
parent 52947e153d
commit d6f4c434fa
36 changed files with 4223 additions and 3118 deletions

View file

@ -0,0 +1,127 @@
// 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();
});
}