fix: onboarding restore no longer traps on an abandoned identity or a missing pin
This commit is contained in:
parent
7d3c8b423c
commit
eaaa3f2ede
3 changed files with 164 additions and 91 deletions
|
|
@ -1,61 +0,0 @@
|
|||
import "dart:io";
|
||||
|
||||
import "package:auto_route/auto_route.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:hive_flutter/hive_flutter.dart";
|
||||
|
||||
import "package:smol_mail/data/providers/providers.dart";
|
||||
import "package:smol_mail/presentation/app_widget.dart";
|
||||
import "package:smol_mail/presentation/routes/app_router.gr.dart";
|
||||
import "package:smol_mail/smol/client.dart";
|
||||
import "package:smol_mail/smol/crypto.dart";
|
||||
import "package:smol_mail/smol/proto.dart";
|
||||
import "package:smol_mail/smol/store.dart";
|
||||
|
||||
class StubClient extends SmolClient {
|
||||
StubClient(super.store);
|
||||
|
||||
@override
|
||||
Future<SmolAddress> recallAccount(String addressText) async {
|
||||
final addr = parseAddress(addressText);
|
||||
store.setAccount(addr);
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late SmolStore store;
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final dir = await Directory.systemTemp.createTemp("dbg4");
|
||||
Hive.init(dir.path);
|
||||
store = await SmolStore.open(stateBox: "dbg-state", mailBox: "dbg-mail");
|
||||
});
|
||||
|
||||
testWidgets("direct replace", (tester) async {
|
||||
final seed = randomBytes(32);
|
||||
await tester.pumpWidget(ProviderScope(
|
||||
overrides: [
|
||||
storeProvider.overrideWithValue(store),
|
||||
clientProvider.overrideWithValue(StubClient(store)),
|
||||
],
|
||||
child: const AppWidget(),
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text("Restore From Seed"));
|
||||
await tester.pumpAndSettle();
|
||||
final fields = find.byType(TextField);
|
||||
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();
|
||||
expect(store.account(), isNotNull, reason: "recall stub ran");
|
||||
final element = tester.element(find.text("Back"));
|
||||
final router = AutoRouter.of(element);
|
||||
await router.replace(InboxRoute());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text("Inbox"), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue