fix(app): every change of how an account is secured asks for the password

The app's side of the owner's proof (OwnerProof, the third reading): adding
an authenticator once one is confirmed, removing a confirmed one and new
recovery codes now ask for the current password beside the code, as
changing the password, a new phrase and deleting already did; one
challenge and one answer per sheet (OwnerAnswer). A wrong password or code
reads as the one refusal the instance gives.

Deleting chooses a code by mail from what the instance says
(secondFactorWorks), no longer from whether an authenticator was ever
confirmed: with the authenticators' reset taken effect but not carried
out, the app offered a code field the instance refused (the fourth
finding). Signed-in calls take no address any more, so a device that
doesn't know it changes the password as any other; deleting by mail asks
for the address only where the device doesn't know it, as the code goes
there (the fifth finding).

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 10:42:55 +02:00
commit a0ae08f975
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_app/testing.dart';
@ -220,6 +221,53 @@ void main() {
);
});
testWidgets("deleting an account whose authenticators' reset took effect "
'asks for a code by mail, as the instance takes it, and for the '
"address where this device doesn't know it", (tester) async {
// The first person the fake password gateway makes.
const person = '00000000-0000-4000-8000-000000000001';
gateway = FakeAuthGateway(
status: const SignedIn(authUserId: person, isIdentified: true),
);
await start(tester);
// Its authenticator still confirmed, the reset taken effect but not
// carried out (the third reading's fourth finding).
dependencies.passwordGateway
.addAccount(
'anna@example.org',
password: 'correct horse',
crypto: dependencies.crypto,
)
.authenticatorReset = clock.now().subtract(
const Duration(hours: 1),
);
Finder field(String label) => find.widgetWithText(TextField, label);
await tester.tap(find.text('Delete account'));
await tester.pumpAndSettle();
await tester.tap(find.text('Delete account').last);
await tester.pumpAndSettle();
expect(find.textContaining('a code by mail confirms it'), findsOneWidget);
await tester.enterText(field('Current password'), 'correct horse');
await tester.enterText(field('Email address'), 'anna@example.org');
await tester.tap(find.text('Email me a code'));
await tester.pumpAndSettle();
expect(
dependencies.passwordGateway.mailed.single.purpose,
MailCodePurpose.deletion,
);
await tester.enterText(
field('Code from the email'),
dependencies.passwordGateway.codeFor('anna@example.org'),
);
await tester.tap(find.text('Delete account').last);
await tester.pumpAndSettle();
expect(gateway.accountDeletions, 1);
});
testWidgets('deleting an account with a password asks for it and a '
'code, which a wrong one fails, deleting nothing', (tester) async {
// The first person the fake password gateway makes.