fix: onboarding restore no longer traps on an abandoned identity or a missing pin

This commit is contained in:
randogoth 2026-09-27 00:09:38 +03:00
parent 7d3c8b423c
commit eaaa3f2ede
3 changed files with 164 additions and 91 deletions

View file

@ -33,7 +33,7 @@ class StubClient extends SmolClient {
void main() {
// Hive box opening is real file IO and must happen outside testWidgets'
// fake-async zone — including the per-test stores.
late final SmolStore storeA, storeB;
late final SmolStore storeA, storeB, storeC;
setUpAll(() async {
TestWidgetsFlutterBinding.ensureInitialized();
final dir = await Directory.systemTemp.createTemp("smol-recall-flow");
@ -42,6 +42,8 @@ void main() {
stateBox: "recall-a-state", mailBox: "recall-a-mail");
storeB = await SmolStore.open(
stateBox: "recall-b-state", mailBox: "recall-b-mail");
storeC = await SmolStore.open(
stateBox: "recall-c-state", mailBox: "recall-c-mail");
});
Widget app(SmolStore store) => ProviderScope(
@ -52,8 +54,9 @@ void main() {
child: const AppWidget(),
);
testWidgets("restore with an address recalls and lands in the inbox",
(tester) async {
testWidgets(
"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);
await tester.pumpWidget(app(store));
@ -63,13 +66,26 @@ void main() {
await tester.tap(find.text("Restore From Seed"));
await tester.pumpAndSettle();
final fields = find.byType(TextField);
var fields = find.byType(TextField);
expect(fields, findsNWidgets(2));
await tester.enterText(fields.at(0), hex(seed));
await tester.enterText(fields.at(1), "randogoth@smol.place");
await tester.tap(find.text("Restore"));
await tester.pumpAndSettle();
// Recall needs the server pinned first (SPEC.md §4) — that's the normal
// state right after a restore, so this lands on the register step framed
// for recall (no error, no Invite token field) rather than the inbox yet.
expect(find.text("Inbox"), findsNothing);
expect(find.text("Pin this server's public key to finish restoring your address."),
findsOneWidget);
expect(find.text("Invite token (optional)"), findsNothing);
fields = find.byType(TextField);
expect(fields, findsNWidgets(2)); // address (carried over), server key
await tester.enterText(fields.at(1), b32encode(randomBytes(32)));
await tester.tap(find.text("Pin and Recall"));
await tester.pumpAndSettle();
expect(find.text("Inbox"), findsOneWidget);
expect(store.seed(), seed);
expect(store.account()!.user, "randogoth");
@ -101,4 +117,54 @@ void main() {
expect(find.text("Inbox"), findsOneWidget);
expect(store.account()!.user, "randogoth");
});
testWidgets(
"restoring again after an incomplete attempt replaces the identity "
"instead of refusing it", (tester) async {
final store = storeC;
final abandonedSeed = randomBytes(32);
final realSeed = 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"));
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.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.
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".
await tester.tap(find.text("Restore From Seed"));
await tester.pumpAndSettle();
fields = find.byType(TextField);
await tester.enterText(fields.at(0), hex(realSeed));
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)));
await tester.tap(find.text("Pin and Recall"));
await tester.pumpAndSettle();
expect(find.text("Inbox"), findsOneWidget);
expect(store.seed(), realSeed);
});
}