feat: replace mail protocol with smolmail and rebuild the UI
This commit is contained in:
parent
cb24b68069
commit
e94e4158cc
93 changed files with 6387 additions and 3479 deletions
140
test/store_test.dart
Normal file
140
test/store_test.dart
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// Store-level behavior: contact key history (§8), import binding, and the
|
||||
// export/import backup file.
|
||||
|
||||
import "dart:convert";
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:hive_flutter/hive_flutter.dart";
|
||||
|
||||
import "package:smol_mail/smol/client.dart";
|
||||
import "package:smol_mail/smol/crypto.dart";
|
||||
import "package:smol_mail/smol/errors.dart";
|
||||
import "package:smol_mail/smol/proto.dart";
|
||||
import "package:smol_mail/smol/store.dart";
|
||||
|
||||
// Each store gets its own boxes; Hive is a per-process singleton, so without
|
||||
// this the "exporting" and "importing" stores would be the same store.
|
||||
Future<SmolStore> freshStore(String tag) =>
|
||||
SmolStore.open(stateBox: "test-$tag-state", mailBox: "test-$tag-mail");
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final dir = await Directory.systemTemp.createTemp("smol-store-test");
|
||||
Hive.init(dir.path);
|
||||
});
|
||||
|
||||
test("contact history keeps displaced keys, not re-saves", () async {
|
||||
final store = await freshStore("history");
|
||||
final a = randomBytes(32), b = randomBytes(32), c = randomBytes(32);
|
||||
|
||||
store.saveContact("alice@example.org", a, true);
|
||||
expect(store.contact("alice@example.org")!.history, isEmpty);
|
||||
|
||||
store.saveContact("alice@example.org", b, false);
|
||||
final rotated = store.contact("alice@example.org")!;
|
||||
expect(rotated.key, b);
|
||||
expect(rotated.history.length, 1);
|
||||
expect(rotated.history.first.key, a);
|
||||
expect(rotated.history.first.until, greaterThan(0));
|
||||
|
||||
// Re-saving the same key is not a rotation and must not add an entry.
|
||||
store.saveContact("alice@example.org", b, true);
|
||||
expect(store.contact("alice@example.org")!.history.length, 1);
|
||||
|
||||
// A second displacement appends, oldest first.
|
||||
store.saveContact("alice@example.org", c, false);
|
||||
final history = store.contact("alice@example.org")!.history;
|
||||
expect(history.length, 2);
|
||||
expect(history[0].key, a);
|
||||
expect(history[1].key, b);
|
||||
expect(store.allContacts().single.$2.history.length, 2);
|
||||
});
|
||||
|
||||
test("importContact binds a smol:// address to the key it carries", () async {
|
||||
final store = await freshStore("import-contact");
|
||||
final client = SmolClient(store);
|
||||
final identity = identityFromSeed(randomBytes(32));
|
||||
client.importContact(
|
||||
"smol://bob@example.org/${b32encode(identity.publicKey)}");
|
||||
final saved = store.contact("bob@example.org")!;
|
||||
expect(saved.verified, isTrue);
|
||||
expect(saved.key, identity.publicKey);
|
||||
});
|
||||
|
||||
test("export never contains the seed and round-trips through import",
|
||||
() async {
|
||||
final a = await freshStore("export");
|
||||
final b = await freshStore("round-trip");
|
||||
a.setIdentity(randomBytes(32));
|
||||
a.pinServer("example.org", randomBytes(32));
|
||||
a.saveContact("alice@example.org", randomBytes(32), true);
|
||||
a.saveContact("alice@example.org", randomBytes(32), false); // history grows
|
||||
await a.storeMessage(
|
||||
"inbox", MailRecord("aa", randomBytes(64), receivedAt: 5));
|
||||
await a.storeMessage("sent",
|
||||
MailRecord("bb", randomBytes(64), recipient: "bob@example.org", sentAt: 6));
|
||||
|
||||
final data = a.exportData();
|
||||
expect(data["gsmolExport"], 1); // gsmol-compatible marker
|
||||
expect(jsonEncode(data).contains(hex(a.seed()!)), isFalse);
|
||||
|
||||
final summary = await b.importData(data);
|
||||
expect(summary.mailAdded, 2);
|
||||
expect(summary.pinsAdded, 1);
|
||||
expect(summary.contactsAdded, 1);
|
||||
expect(b.serverPin("example.org"), a.serverPin("example.org"));
|
||||
expect(b.contact("alice@example.org")!.history.length, 1);
|
||||
expect(b.getMessage("inbox", "aa"), isNotNull);
|
||||
expect(b.getMessage("sent", "bb"), isNotNull);
|
||||
expect(b.seed(), isNull); // never the seed
|
||||
});
|
||||
|
||||
test("import never overwrites a differing trust binding", () async {
|
||||
final store = await freshStore("conflict");
|
||||
final mine = randomBytes(32), other = randomBytes(32);
|
||||
store.pinServer("example.org", mine);
|
||||
store.saveContact("alice@example.org", mine, true);
|
||||
final summary = await store.importData({
|
||||
"gsmolExport": 1,
|
||||
"servers": {"example.org": b32encode(other)},
|
||||
"contacts": {
|
||||
"alice@example.org": {"key": b32encode(other), "verified": true},
|
||||
"bob@example.org": {"key": b32encode(randomBytes(32)), "verified": false},
|
||||
},
|
||||
});
|
||||
expect(summary.pinsConflicted, 1);
|
||||
expect(summary.pinsAdded, 0);
|
||||
expect(summary.contactsConflicted, 1);
|
||||
expect(summary.contactsAdded, 1);
|
||||
expect(store.serverPin("example.org"), mine);
|
||||
expect(store.contact("alice@example.org")!.key, mine);
|
||||
expect(store.contact("bob@example.org"), isNotNull);
|
||||
});
|
||||
|
||||
test("import skips malformed entries and rejects wrong files", () async {
|
||||
final store = await freshStore("malformed");
|
||||
final summary = await store.importData({
|
||||
"gsmolExport": 1,
|
||||
"servers": {"bad": "notbase32!", "short": b32encode(randomBytes(8))},
|
||||
"contacts": {
|
||||
"x": {"key": "nope"},
|
||||
"y": "not a record",
|
||||
},
|
||||
"inbox": [
|
||||
{"id": "ok", "envelope": base64Encode(randomBytes(64)), "receivedAt": 1},
|
||||
{"id": "bad", "envelope": "!!!not base64!!!"},
|
||||
"not a record",
|
||||
],
|
||||
"sent": "not a list",
|
||||
});
|
||||
expect(summary.malformed, 7); // 2 pins, 2 contacts, 2 mail, 1 sent
|
||||
expect(summary.pinsAdded, 0);
|
||||
expect(summary.mailAdded, 1);
|
||||
expect(store.getMessage("inbox", "ok"), isNotNull);
|
||||
expect(store.getMessage("inbox", "bad"), isNull);
|
||||
|
||||
expect(() => store.importData({"nope": 1}), throwsA(isA<SmolError>()));
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue