100 lines
3.8 KiB
Dart
100 lines
3.8 KiB
Dart
// Store-level behavior against the real native store: master lifecycle, read
|
|
// marks, pins and the settings the app owns in Hive.
|
|
|
|
import "dart:io";
|
|
import "dart:math";
|
|
import "dart:typed_data";
|
|
|
|
import "package:flutter_test/flutter_test.dart";
|
|
import "package:hive_flutter/hive_flutter.dart";
|
|
|
|
import "package:smol_mail/smol/address.dart";
|
|
import "package:smol_mail/smol/store.dart";
|
|
|
|
Uint8List randomBytes(int n) =>
|
|
Uint8List.fromList(List.generate(n, (_) => Random.secure().nextInt(256)));
|
|
|
|
Future<SmolStore> freshStore(String tag) => SmolStore.open(
|
|
dbPath: "${Directory.systemTemp.createTempSync("smol-store-$tag").path}/store.db",
|
|
stateBox: "$tag-state",
|
|
readBox: "$tag-read");
|
|
|
|
void main() {
|
|
setUpAll(() async {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
final dir = await Directory.systemTemp.createTemp("smol-store-test");
|
|
Hive.init(dir.path);
|
|
});
|
|
|
|
test("master set, restore and rotations", () async {
|
|
final store = await freshStore("master");
|
|
expect(store.master(), isNull);
|
|
expect(store.identity(), isNull);
|
|
|
|
final master = randomBytes(32);
|
|
store.setMaster(master);
|
|
expect(store.master(), master);
|
|
// A fresh identity's public key is the native-derived one.
|
|
expect(store.identity()!.publicKey.length, 52);
|
|
expect(store.rotations(), 0);
|
|
|
|
final other = randomBytes(32);
|
|
store.restoreMaster(other, 3);
|
|
expect(store.master(), other);
|
|
// The account handle was rebuilt at the restored rotation index.
|
|
expect(store.identity()!.publicKey.length, 52);
|
|
});
|
|
|
|
test("leave-on-server is a setting, not protocol state", () async {
|
|
final store = await freshStore("leave");
|
|
expect(store.leaveOnServer(), isFalse);
|
|
await store.setLeaveOnServer(true);
|
|
expect(store.leaveOnServer(), isTrue);
|
|
await store.setLeaveOnServer(false);
|
|
expect(store.leaveOnServer(), isFalse);
|
|
});
|
|
|
|
test("read marks drive the unread counts", () async {
|
|
final store = await freshStore("read");
|
|
expect(store.unreadCount(), 0);
|
|
// Nothing is stored yet, so marking an id is harmless bookkeeping.
|
|
store.markRead("ab" * 32);
|
|
expect(store.isRead("ab" * 32), isTrue);
|
|
});
|
|
|
|
test("pins save, list and unpin", () async {
|
|
final store = await freshStore("pins");
|
|
expect(store.allPins(), isEmpty);
|
|
// A syntactically valid key: the system server's, a real 52-char form.
|
|
const key = "lm2gqd7e5q67xq3isc5hx6jfj2q7a7xvq4l7trctxipudujovmgq";
|
|
store.pinServer("example.org", key, defaultPort);
|
|
expect(store.serverPin("example.org", defaultPort), key);
|
|
expect(store.allPins().length, 1);
|
|
await store.unpinServer("example.org", defaultPort);
|
|
expect(store.serverPin("example.org", defaultPort), isNull);
|
|
});
|
|
|
|
test("wipe clears every secret and mark, overwriting the master", () async {
|
|
final store = await freshStore("wipe");
|
|
final master = randomBytes(32);
|
|
store.setMaster(master);
|
|
// The store's own copy, read back before the wipe.
|
|
final stored = store.master()!;
|
|
store.pinServer("example.org",
|
|
"lm2gqd7e5q67xq3isc5hx6jfj2q7a7xvq4l7trctxipudujovmgq", defaultPort);
|
|
store.markRead("cd" * 32);
|
|
|
|
await store.wipe();
|
|
// The bytes the store owned are overwritten, not just dereferenced —
|
|
// the reference read back through the store sees the zeros. The
|
|
// caller's own buffer is the caller's to zeroize: the store holds a
|
|
// copy precisely so nobody else's zeroization can reach it, and vice
|
|
// versa a wipe never reaches a buffer the store handed out.
|
|
expect(stored.every((b) => b == 0), isTrue);
|
|
expect(master.every((b) => b == 0), isFalse);
|
|
expect(store.master(), isNull);
|
|
expect(store.identity(), isNull);
|
|
expect(store.serverPin("example.org", defaultPort), isNull);
|
|
expect(store.isRead("cd" * 32), isFalse);
|
|
});
|
|
}
|