feat(app): the warning that a session was ended for its proofs

A password sign-in whose account carries the warning (a session ended
for ten wrong proofs in a row) shows it before the sign-in is put in
place: when, what it means, and to change the password if it wasn't
the owner. Dismiss clears it on the instance through the held-back
session; Remind me later signs in and keeps it. The account section
shows it above the change of password, with Dismiss, while it stands.
Nothing identifies the session or its device.

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-21 12:10:38 +02:00
commit c95b3d55b3
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 449 additions and 6 deletions

View file

@ -81,6 +81,23 @@ void main() {
expect(cubit.state.failed, isTrue);
expect(cubit.state.security, before);
});
test('carries the warning that a session was ended for its wrong '
'proofs, until it is dismissed', () async {
final ended = DateTime.utc(2026, 10, 19, 8, 30);
anna.sessionEndedForProofsAt = ended;
final cubit = AccountSecurityCubit(passwords);
addTearDown(cubit.close);
await cubit.load();
expect(cubit.state.security?.sessionEndedAt, ended);
await passwords.dismissSessionEndedWarning();
await pumpEventQueue();
expect(gateway.calls, contains('dismissSessionEndedWarning'));
expect(anna.sessionEndedForProofsAt, isNull);
expect(cubit.state.security?.sessionEndedAt, isNull);
});
});
group('a step of the account section', () {

View file

@ -82,6 +82,72 @@ void main() {
);
});
group('warns before signing in that a session was ended for its wrong '
'proofs', () {
late FakePasswordAccount account;
final ended = DateTime.utc(2026, 10, 19, 8, 30);
setUp(() async {
account = anna()..sessionEndedForProofsAt = ended;
await cubit.credentialsEntered('anna@example.org', 'correct horse');
});
test('with when, the sign-in held back until it is read', () async {
expect(await statesOf(cubit, () => cubit.codeEntered(fakeTotpCode)), [
at(PasswordSignInStep.secondFactor, busy: true),
at(PasswordSignInStep.sessionEndedWarning, busy: true),
at(PasswordSignInStep.sessionEndedWarning),
]);
expect(cubit.state.sessionEndedAt, ended);
expect(auth.status, const SignedOut());
});
test('dismissed: signed in, the warning gone', () async {
await cubit.codeEntered(fakeTotpCode);
expect(await statesOf(cubit, cubit.warningDismissed), [
at(PasswordSignInStep.sessionEndedWarning, busy: true),
at(PasswordSignInStep.done, busy: true),
at(PasswordSignInStep.done),
]);
expect(account.sessionEndedForProofsAt, isNull);
expect(
auth.status,
SignedIn(authUserId: account.person, isIdentified: true),
);
});
test('kept for later: signed in, the warning kept', () async {
await cubit.codeEntered(fakeTotpCode);
expect(await statesOf(cubit, cubit.warningKept), [
at(PasswordSignInStep.sessionEndedWarning, busy: true),
at(PasswordSignInStep.done, busy: true),
at(PasswordSignInStep.done),
]);
expect(account.sessionEndedForProofsAt, ended);
expect(
auth.status,
SignedIn(authUserId: account.person, isIdentified: true),
);
});
test('a dismissal the instance refuses stays on the warning, signing '
'nobody in', () async {
await cubit.codeEntered(fakeTotpCode);
gateway.failing = const PasswordFailed(PasswordProblem.failed);
expect(await statesOf(cubit, cubit.warningDismissed), [
at(PasswordSignInStep.sessionEndedWarning, busy: true),
at(
PasswordSignInStep.sessionEndedWarning,
problem: PasswordProblem.failed,
),
]);
expect(auth.status, const SignedOut());
});
});
test('refuses a wrong password and a wrong code alike, on the code, '
'signing nobody in', () async {
anna();