test(app): the password screens' flows that had none

An impostor instance refused; a lost authenticator's reset by the
recovery phrase; the password alone signing in once a reset took
effect, an authenticator set up first; starting an account's reset
without the phrase, and finishing one with new keys and a new phrase;
in the account section, removing an authenticator (a code, never the
last), new recovery codes, and a change of password refused for a wrong
current one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-20 20:17:12 +02:00
commit 85be0a9acc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -270,6 +270,190 @@ void main() {
expect(find.text('App of ${anna.person}'), findsOneWidget);
expect(anna.password, 'remembered');
});
/// Signs in as far as the code: the sign-in sheet, [address] and
/// [password] entered.
Future<void> credentials(
WidgetTester tester, {
String address = 'anna@example.org',
String password = 'correct horse',
}) async {
await tester.tap(find.widgetWithText(FilledButton, 'Sign in'));
await tester.pumpAndSettle();
await tester.enterText(field('Email address'), address);
await tester.enterText(field('Password'), password);
await tapButton(tester, 'Continue');
}
testWidgets("refuses an instance that can't prove it knows the "
'password, signing nobody in', (tester) async {
dependencies.passwordGateway
..addAccount(
'anna@example.org',
password: 'correct horse',
crypto: dependencies.crypto,
)
..provesPassword = false;
await start(tester);
await credentials(tester);
await tester.enterText(
field('Code from your authenticator app'),
fakeTotpCode,
);
await tapButton(tester, 'Sign in');
expect(
find.textContaining("The instance couldn't prove it knows your"),
findsOneWidget,
);
expect(find.textContaining('App of'), findsNothing);
expect(dependencies.keys.kept, isEmpty);
});
testWidgets('a lost authenticator: asks for its reset with the recovery '
'phrase, saying when it takes effect', (tester) async {
dependencies.passwordGateway.addAccount(
'anna@example.org',
password: 'correct horse',
crypto: dependencies.crypto,
);
await start(tester);
await credentials(tester);
await tester.tap(find.text('Lost your authenticator?'));
await tester.pumpAndSettle();
await tester.tap(find.text('Reset with your recovery phrase'));
await tester.pumpAndSettle();
await tester.enterText(field('Recovery phrase'), otherFakePhrase);
await tapButton(tester, 'Reset');
expect(
find.text("The address or the recovery phrase isn't right."),
findsOneWidget,
);
await tester.enterText(field('Recovery phrase'), fakePhrase);
await tapButton(tester, 'Reset');
expect(
find.textContaining('Your authenticators are reset on'),
findsOneWidget,
);
});
testWidgets('once the reset of the authenticators took effect, the '
'password alone signs in, an authenticator set up first', (
tester,
) async {
final anna = dependencies.passwordGateway.addAccount(
'anna@example.org',
password: 'correct horse',
crypto: dependencies.crypto,
)..authenticatorReset = DateTime.utc(2026, 9, 30);
await start(tester);
await credentials(tester);
await tester.tap(find.text('Lost your authenticator?'));
await tester.pumpAndSettle();
await tester.tap(find.text('Sign in without a code'));
await tester.pumpAndSettle();
expect(find.byType(QrCodeView), findsOneWidget);
expect(find.textContaining('App of'), findsNothing);
await tester.enterText(
field('Code from your authenticator app'),
fakeTotpCode,
);
await tapButton(tester, 'Continue');
expect(find.text('AAAA-0001'), findsOneWidget);
await tapButton(tester, "I've kept them");
expect(find.text('App of ${anna.person}'), findsOneWidget);
expect(anna.finished, isTrue);
});
testWidgets("without the phrase: a code by mail starts the account's "
'reset, saying from when', (tester) async {
final anna = dependencies.passwordGateway.addAccount(
'anna@example.org',
password: 'forgotten',
crypto: dependencies.crypto,
);
await start(tester);
await tester.tap(find.widgetWithText(FilledButton, 'Sign in'));
await tester.pumpAndSettle();
await tester.tap(find.text('Forgot your password?'));
await tester.pumpAndSettle();
await tester.tap(find.text('Reset my account'));
await tester.pumpAndSettle();
await tester.enterText(field('Email address'), 'anna@example.org');
await tapButton(tester, 'Continue');
await tester.enterText(field('Code from the email'), 'CODE-9');
await tapButton(tester, 'Reset');
expect(find.text("This code isn't right."), findsOneWidget);
await tester.enterText(
field('Code from the email'),
dependencies.passwordGateway.codeFor('anna@example.org'),
);
await tapButton(tester, 'Reset');
expect(
find.textContaining('Your account can be reset from'),
findsOneWidget,
);
expect(anna.accountReset, isNotNull);
expect(anna.password, 'forgotten');
await tapButton(tester, 'Done');
expect(find.textContaining('App of'), findsNothing);
});
testWidgets('finishes a reset that took effect: new keys, a new phrase '
'written down and checked, an authenticator, then signed in', (
tester,
) async {
final anna = dependencies.passwordGateway.addAccount(
'anna@example.org',
password: 'forgotten',
crypto: dependencies.crypto,
)..accountReset = DateTime.utc(2026, 9, 30);
final keysBefore = anna.passwordKeys;
await start(tester);
await tester.tap(find.widgetWithText(FilledButton, 'Sign in'));
await tester.pumpAndSettle();
await tester.tap(find.text('Forgot your password?'));
await tester.pumpAndSettle();
await tester.tap(find.text('Finish a reset'));
await tester.pumpAndSettle();
await tester.enterText(field('Email address'), 'anna@example.org');
expect(
find.textContaining('your account gets new keys'),
findsOneWidget,
);
await tapButton(tester, 'Continue');
await tester.enterText(field('New password'), 'a new start');
await tester.enterText(field('New password again'), 'a new start');
await tapButton(tester, 'Continue');
expect(find.text('Your recovery phrase'), findsOneWidget);
await tapButton(tester, "I've written it down");
await typeCheckedWords(tester, fakePhrase);
await tapButton(tester, 'Continue');
await tester.enterText(
field('Code from the email'),
dependencies.passwordGateway.codeFor('anna@example.org'),
);
await tapButton(tester, 'Continue');
await tester.enterText(
field('Code from your authenticator app'),
fakeTotpCode,
);
await tapButton(tester, 'Continue');
await tapButton(tester, "I've kept them");
expect(find.text('App of ${anna.person}'), findsOneWidget);
expect(anna.password, 'a new start');
expect(anna.passwordKeys, isNot(same(keysBefore)));
expect(anna.accountReset, isNull);
expect(anna.finished, isTrue);
});
});
group('the account section', () {
@ -400,5 +584,122 @@ void main() {
findsOneWidget,
);
});
/// Anna, signed in with her password and a code.
Future<FakePasswordAccount> annaSignedIn() async {
final anna = dependencies.passwordGateway.addAccount(
'anna@example.org',
password: 'correct horse',
crypto: dependencies.crypto,
);
final passwords = dependencies.passwords;
await passwords.enter(
await passwords.signIn(
await passwords.unlock('anna@example.org', 'correct horse'),
totpCode: fakeTotpCode,
),
);
return anna;
}
testWidgets('removes an authenticator only with a code, and never the '
'last', (tester) async {
final anna = await annaSignedIn();
final passwords = dependencies.passwords;
final laptop = await passwords.enrol(name: 'Laptop', label: 'anna');
await passwords.confirm(
laptop.id,
fakeTotpCode,
existingCode: fakeTotpCode,
);
await show(tester);
await tester.tap(find.byTooltip('Remove').first);
await tester.pumpAndSettle();
expect(find.text('Remove Phone?'), findsOneWidget);
await tester.enterText(
field('Code from your authenticator app'),
'000000',
);
await tapButton(tester, 'Remove');
expect(
find.text("This code isn't right. Enter the one your app shows now."),
findsOneWidget,
);
expect(anna.authenticators, hasLength(2));
await tester.enterText(
field('Code from your authenticator app'),
fakeTotpCode,
);
await tapButton(tester, 'Remove');
expect(anna.authenticators.map((each) => each.name), ['Laptop']);
await tester.tap(find.byTooltip('Remove'));
await tester.pumpAndSettle();
await tester.enterText(
field('Code from your authenticator app'),
fakeTotpCode,
);
await tapButton(tester, 'Remove');
expect(
find.text("Your last authenticator can't go: every sign-in needs one."),
findsOneWidget,
);
expect(anna.authenticators, hasLength(1));
});
testWidgets('makes new recovery codes only with a code, the old ones '
'gone', (tester) async {
final anna = await annaSignedIn();
await show(tester);
await tester.tap(find.text('New recovery codes'));
await tester.pumpAndSettle();
await tester.enterText(
field('Code from your authenticator app'),
'000000',
);
await tapButton(tester, 'Continue');
expect(
find.text("This code isn't right. Enter the one your app shows now."),
findsOneWidget,
);
expect(anna.recoveryCodes.first, 'AAAA-0001');
await tester.enterText(
field('Code from your authenticator app'),
fakeTotpCode,
);
await tapButton(tester, 'Continue');
expect(find.text('BBBB-0001'), findsOneWidget);
expect(anna.recoveryCodes, isNot(contains('AAAA-0001')));
await tapButton(tester, "I've kept them");
expect(find.text('BBBB-0001'), findsNothing);
});
testWidgets('refuses a change of password with a wrong current one, '
'nothing changing', (tester) async {
final anna = await annaSignedIn();
await show(tester);
await tester.tap(find.text('Change password'));
await tester.pumpAndSettle();
await tester.enterText(field('Current password'), 'wrong horse');
await tester.enterText(field('New password'), 'better horse');
await tester.enterText(field('New password again'), 'better horse');
await tester.enterText(
field('Code from your authenticator app'),
fakeTotpCode,
);
await tapButton(tester, 'Change password');
expect(
find.text("The address, the password or the code isn't right."),
findsOneWidget,
);
expect(anna.password, 'correct horse');
});
});
}