feat(block2): trust UI — vouch for a person in chat (web of trust)

- TrustCubit: reads how many vouch for the peer + whether YOU do; toggleVouch
  certifies/revokes over TrustTransport; never vouches for self; offline-safe.
  Kept simple — the full known-member rule (threshold+distance) and its
  bootstrap set are still-open policy (network-trust §2); a live count + your
  vouch is the honest first step.
- Chat screen now opens ONE session carrying both messaging AND trust (the
  shared-connection shape), and shows a slim vouch banner: 'Vouched for by N' +
  an 'I know this person' / 'You vouch for them' toggle.
- i18n en/es/pt (human words: 'I know this person', not 'certify').

Tests: 4 trust cubit + messaging + chat, 10 green; analyzer clean.
This commit is contained in:
vjrj 2026-07-10 11:04:07 +02:00
parent 4326e79419
commit 7cba4f7fcf
10 changed files with 367 additions and 17 deletions

View file

@ -0,0 +1,82 @@
import 'package:commons_core/commons_core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/state/trust_cubit.dart';
/// In-memory [TrustTransport]: certify/revoke act as this device's identity.
class FakeTrustTransport implements TrustTransport {
FakeTrustTransport(this.selfId);
final String selfId;
final Map<String, Set<String>> _certs = {};
@override
Future<Set<String>> certifiersOf(String subjectPubkey) async =>
_certs[subjectPubkey] ?? <String>{};
@override
Future<void> certify({
required String subjectPubkey,
Duration validity = const Duration(days: 365),
String note = '',
}) async =>
(_certs[subjectPubkey] ??= {}).add(selfId);
@override
Future<void> revoke({required String subjectPubkey}) async =>
_certs[subjectPubkey]?.remove(selfId);
@override
Future<List<Certification>> allCertifications() async => const [];
@override
Future<void> close() async {}
}
void main() {
const me = 'me';
const peer = 'peer';
test('loads certifier count and whether I vouch', () async {
final transport = FakeTrustTransport(me);
await transport.certify(subjectPubkey: peer); // I already vouch
final cubit = TrustCubit(transport, peerPubkey: peer, selfPubkey: me);
await cubit.load();
expect(cubit.state.certifierCount, 1);
expect(cubit.state.iVouch, isTrue);
expect(cubit.state.loading, isFalse);
await cubit.close();
});
test('toggleVouch certifies then revokes', () async {
final cubit =
TrustCubit(FakeTrustTransport(me), peerPubkey: peer, selfPubkey: me);
await cubit.load();
expect(cubit.state.iVouch, isFalse);
await cubit.toggleVouch();
expect(cubit.state.iVouch, isTrue);
expect(cubit.state.certifierCount, 1);
await cubit.toggleVouch();
expect(cubit.state.iVouch, isFalse);
expect(cubit.state.certifierCount, 0);
await cubit.close();
});
test('never vouches for self', () async {
final cubit =
TrustCubit(FakeTrustTransport(me), peerPubkey: me, selfPubkey: me);
await cubit.load();
await cubit.toggleVouch();
expect(cubit.state.iVouch, isFalse);
await cubit.close();
});
test('offline (no transport) never throws', () async {
final cubit = TrustCubit(null, peerPubkey: peer, selfPubkey: me);
expect(cubit.isOnline, isFalse);
await cubit.load();
await cubit.toggleVouch();
expect(cubit.state.loading, isFalse);
await cubit.close();
});
}