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:
parent
b51445a01a
commit
85be0a9acc
1 changed files with 301 additions and 0 deletions
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue