feat: adopt smolmail protocol 1.1, adaptive nav shell, and server mail retention
This commit is contained in:
parent
eaaa3f2ede
commit
c693e6fcb9
27 changed files with 1190 additions and 592 deletions
|
|
@ -1,4 +1,4 @@
|
|||
// Regression tests for the onboarding recall flows: restoring a seed and
|
||||
// Regression tests for the onboarding recall flows: restoring a master and
|
||||
// recalling the registered address must land the user in the inbox. The
|
||||
// client's network operations are stubbed; what is under test is the UI and
|
||||
// router flow itself.
|
||||
|
|
@ -14,14 +14,28 @@ import "package:smol_mail/data/providers/providers.dart";
|
|||
import "package:smol_mail/presentation/app_widget.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";
|
||||
|
||||
/// A [SmolClient] whose recall completes instantly, so the test exercises
|
||||
/// the flow rather than the network.
|
||||
/// A [SmolClient] whose network operations complete instantly, so the test
|
||||
/// exercises the flow rather than the network. [restoreAndRecall] still
|
||||
/// enforces the pin gate (SPEC.md §4), since that gate is exactly what the
|
||||
/// restore flow's UX is regression-tested against.
|
||||
class StubClient extends SmolClient {
|
||||
StubClient(super.store);
|
||||
|
||||
@override
|
||||
Future<SmolAddress> restoreAndRecall(String masterHex, String addressText) async {
|
||||
final addr = parseAddress(addressText);
|
||||
if (store.serverPin(addr.host) == null) {
|
||||
throw SmolError("no pinned key for ${addr.host}");
|
||||
}
|
||||
store.restoreMaster(unhex(masterHex.trim()), 0);
|
||||
store.setAccount(addr);
|
||||
return addr;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SmolAddress> recallAccount(String addressText) async {
|
||||
final addr = parseAddress(addressText);
|
||||
|
|
@ -58,7 +72,7 @@ void main() {
|
|||
"restore with an address, but no pin yet, asks for the server key "
|
||||
"and then recalls into the inbox", (tester) async {
|
||||
final store = storeA;
|
||||
final seed = randomBytes(32);
|
||||
final master = randomBytes(32);
|
||||
await tester.pumpWidget(app(store));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text("Create Identity"), findsOneWidget);
|
||||
|
|
@ -68,7 +82,7 @@ void main() {
|
|||
|
||||
var fields = find.byType(TextField);
|
||||
expect(fields, findsNWidgets(2));
|
||||
await tester.enterText(fields.at(0), hex(seed));
|
||||
await tester.enterText(fields.at(0), hex(master));
|
||||
await tester.enterText(fields.at(1), "randogoth@smol.place");
|
||||
await tester.tap(find.text("Restore"));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
@ -80,6 +94,9 @@ void main() {
|
|||
expect(find.text("Pin this server's public key to finish restoring your address."),
|
||||
findsOneWidget);
|
||||
expect(find.text("Invite token (optional)"), findsNothing);
|
||||
// Restoring has no well-defined "register a new address instead" escape
|
||||
// hatch until the rotation index is known (§2).
|
||||
expect(find.text("Register a new address instead"), findsNothing);
|
||||
fields = find.byType(TextField);
|
||||
expect(fields, findsNWidgets(2)); // address (carried over), server key
|
||||
await tester.enterText(fields.at(1), b32encode(randomBytes(32)));
|
||||
|
|
@ -87,84 +104,71 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Inbox"), findsOneWidget);
|
||||
expect(store.seed(), seed);
|
||||
expect(store.master(), master);
|
||||
expect(store.account()!.user, "randogoth");
|
||||
});
|
||||
|
||||
testWidgets("restore without an address recalls from the register step",
|
||||
testWidgets("restore requires an address before it will submit",
|
||||
(tester) async {
|
||||
final store = storeB;
|
||||
final seed = randomBytes(32);
|
||||
await tester.pumpWidget(app(store));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text("Restore From Seed"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
var fields = find.byType(TextField);
|
||||
await tester.enterText(fields.at(0), hex(seed));
|
||||
final fields = find.byType(TextField);
|
||||
await tester.enterText(fields.at(0), hex(randomBytes(32)));
|
||||
await tester.tap(find.text("Restore"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// The register step: address, server key, optional invite token.
|
||||
fields = find.byType(TextField);
|
||||
expect(fields, findsNWidgets(3));
|
||||
await tester.enterText(fields.at(0), "randogoth@smol.place");
|
||||
await tester.enterText(fields.at(1), b32encode(randomBytes(32)));
|
||||
await tester.tap(find.text("Already registered? Recall"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Inbox"), findsOneWidget);
|
||||
expect(store.account()!.user, "randogoth");
|
||||
// Nothing was submitted, so nothing was persisted; still on this step.
|
||||
expect(find.text("Restore"), findsOneWidget);
|
||||
expect(find.text("Inbox"), findsNothing);
|
||||
expect(store.master(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
"restoring again after an incomplete attempt replaces the identity "
|
||||
"restoring after an abandoned Create Identity attempt replaces it "
|
||||
"instead of refusing it", (tester) async {
|
||||
final store = storeC;
|
||||
final abandonedSeed = randomBytes(32);
|
||||
final realSeed = randomBytes(32);
|
||||
final realMaster = randomBytes(32);
|
||||
await tester.pumpWidget(app(store));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// First attempt: restore a seed but never finish registering — lands on
|
||||
// the register step, identity set, no account bound.
|
||||
await tester.tap(find.text("Restore From Seed"));
|
||||
// First attempt: create a fresh identity but never finish registering —
|
||||
// lands on the backup step, master set, no account bound.
|
||||
await tester.tap(find.text("Create Identity"));
|
||||
await tester.pumpAndSettle();
|
||||
var fields = find.byType(TextField);
|
||||
await tester.enterText(fields.at(0), hex(abandonedSeed));
|
||||
await tester.tap(find.text("Restore"));
|
||||
await tester.pumpAndSettle();
|
||||
expect(store.seed(), abandonedSeed);
|
||||
expect(store.master(), isNotNull);
|
||||
expect(store.account(), isNull);
|
||||
|
||||
// Simulate returning to onboarding later (e.g. a cold restart). Pumping
|
||||
// app(store) directly would just rebuild the existing OnboardingScreen
|
||||
// state in place (still parked on the register step) rather than really
|
||||
// restarting, so tear the tree down first to force a fresh app state —
|
||||
// HomeGuard then sends an identity-without-account back to welcome.
|
||||
// state in place rather than really restarting, so tear the tree down
|
||||
// first to force a fresh app state — HomeGuard then sends an
|
||||
// identity-without-account back to welcome.
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pumpWidget(app(store));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text("Create Identity"), findsOneWidget);
|
||||
|
||||
// Restoring a different seed must not throw "identity already exists".
|
||||
// Restoring a different master must not throw "identity already exists".
|
||||
await tester.tap(find.text("Restore From Seed"));
|
||||
await tester.pumpAndSettle();
|
||||
fields = find.byType(TextField);
|
||||
await tester.enterText(fields.at(0), hex(realSeed));
|
||||
final fields = find.byType(TextField);
|
||||
await tester.enterText(fields.at(0), hex(realMaster));
|
||||
await tester.enterText(fields.at(1), "randogoth@smol.place");
|
||||
await tester.tap(find.text("Restore"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Lands on the recall-framed register step (no pin yet); pin it and finish.
|
||||
fields = find.byType(TextField);
|
||||
expect(fields, findsNWidgets(2));
|
||||
await tester.enterText(fields.at(1), b32encode(randomBytes(32)));
|
||||
expect(find.byType(TextField), findsNWidgets(2));
|
||||
await tester.enterText(find.byType(TextField).at(1), b32encode(randomBytes(32)));
|
||||
await tester.tap(find.text("Pin and Recall"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Inbox"), findsOneWidget);
|
||||
expect(store.seed(), realSeed);
|
||||
expect(store.master(), realMaster);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue