fix(app): no address in debug logs, and deleting goes on whatever reading the account met

The reading of the full reading's fixes found that SignInWaysState kept
Equatable's default text, which debug builds log with the address, and
that an unreadable sign_in.json logged its FormatException, whose message
holds the file's text; and that the account section's Delete caught only
refusals, so a keyring that couldn't be read escaped and the tap did
nothing.

The state says only whether an address is kept; the file's error logs its
type alone. Reading what keeps the account counts the keys as not here
where the keyring can't be read, and Delete goes on whatever the reading
met, failing as the session bloc says if it must.

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 16:38:36 +02:00
commit 28fb6c0309
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
6 changed files with 77 additions and 4 deletions

View file

@ -49,4 +49,11 @@ final class const SignInWaysState({
}) with Equatable {
@override
List<Object?> get props => [loaded, ways, emailAddress];
/// Never the address: blocs' states are logged (the reading of the
/// fixes).
@override
String toString() =>
'SignInWaysState(loaded: $loaded, ways: $ways, '
'address: ${emailAddress == null ? 'none' : 'kept'})';
}

View file

@ -30,7 +30,8 @@ class FileSignInAddresses(final Directory _people) implements SignInAddresses {
} on IOException {
return null;
} on FormatException catch (error) {
log('Unreadable sign_in.json', name: 'Auth', error: error);
// Its type only: its message holds the file's text, the address.
log('Unreadable sign_in.json: ${error.runtimeType}', name: 'Auth');
return null;
}
}

View file

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
import 'package:halkyon_app/src/app/app_scope.dart';
@ -129,8 +131,13 @@ class const AccountTiles({
final AccountSecurity? security;
try {
security = identified ? await passwords.security() : null;
} on PasswordFailed {
// Unreachable: deleting would fail too, as the session bloc says.
} on Object catch (error) {
// Unreachable, or anything else (the reading of the fixes: a failure
// that isn't a refusal escaped, and the tap did nothing): deleting
// goes on, and fails as the session bloc says if it must.
if (error is! PasswordFailed) {
log('Reading the account failed: ${error.runtimeType}', name: 'Auth');
}
session.add(const SessionEventDeleteRequested());
return;
}

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:developer';
import 'dart:typed_data';
import 'package:clock/clock.dart';
@ -427,10 +428,21 @@ class PasswordRepository(
authenticators: ways.password
? await _gateway.authenticators()
: const <AccountAuthenticator>[],
keysHere: await _keys.read(person) != null,
keysHere: await _keysKept(person),
);
}
/// Whether [person]'s keys are in the keyring; not where it can't be read
/// (locked, its service gone), which says nothing else of the account.
Future<bool> _keysKept(String person) async {
try {
return await _keys.read(person) != null;
} on Object catch (error) {
log('The keyring could not be read: ${error.runtimeType}', name: 'Keys');
return false;
}
}
/// Dismisses the warning that a session of the account was ended for its
/// wrong proofs: the signed-in person's, or [pending]'s before it is put
/// in place. The session alone does: it hides a warning, nothing more.

View file

@ -0,0 +1,15 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
void main() {
test("what the sign-in ways' state logs never holds the address (the "
'reading of the fixes)', () {
const state = SignInWaysState(
loaded: true,
emailAddress: 'anna@example.org',
);
expect('$state', isNot(contains('anna')));
expect('$state', contains('loaded'));
});
}

View file

@ -1,8 +1,22 @@
import 'package:clock/clock.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_app/testing.dart';
/// A keyring the system won't read: locked, or its service gone.
final class _UnreadableKeys() implements AccountKeyStore {
@override
Future<void> keep(String person, UnlockedAccountKeys keys) async {}
@override
Future<UnlockedAccountKeys?> read(String person) =>
Future.error(PlatformException(code: 'locked'));
@override
Future<void> forgetAllBut(String? person) async {}
}
Matcher failsWith(PasswordProblem problem) => throwsA(
isA<PasswordFailed>().having((failed) => failed.problem, 'problem', problem),
);
@ -457,6 +471,23 @@ void main() {
expect(security.finished, isTrue);
expect(security.keysHere, isTrue);
});
test("says what keeps the account where the keyring can't be read, "
'the keys counted as not here (the reading of the fixes)', () async {
final unreadable = PasswordRepository(
gateway,
crypto,
_UnreadableKeys(),
auth,
addresses,
);
addTearDown(unreadable.dispose);
final security = await unreadable.security();
expect(security.hasPassword, isTrue);
expect(security.keysHere, isFalse);
});
});
});