tane/apps/app_seeds/test/state/trust_cubit_test.dart
vjrj 4cf53f259f feat(trust): full Duniter web-of-trust membership (params + referents)
Slice 4 of Block 2. The pure rule was already parameterised; this adds the
policy, cold-start and UI around it.

- commons_core: WotParams (sigQty/stepMax/sigValidity, Duniter defaults) +
  WebOfTrust.membersWith; npubToHex helper (NIP-19 decode) for adding roots.
- WotSettings (keystore): the active parameters, configurable — a young
  network loosens them and tightens as it grows, as Ğ1 did. Defaults Duniter.
- TrustReferents: the bootstrap 'seeds' membership is measured from — a
  bundled asset (empty until real founders are curated, no invented keys)
  unioned with referents the user adds by npub/QR. Honest cold-start.
- TrustCubit: computes the full membership verdict against referents+params
  alongside the personal circle, and exposes a TrustTier (networkMember >
  inYourCircle > vouched > unknown). Certifications issued with the active
  validity (they expire and renew, Duniter rule).
- UI: chat trust badge by tier; a 'Network of trust' screen (manage roots +
  advanced params) reached from the profile. i18n en/es/pt/ast.

Tests: WotParams/membersWith, npubToHex, TrustReferents, WotSettings, and
TrustCubit tiers/membership. Resolves the WoT-parameters decision
(open-decisions §B). Trust net stays empty/undetermined until seeded — by
design; users bootstrap their own roots.
2026-07-10 21:02:08 +02:00

160 lines
5.1 KiB
Dart

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 = {}; // subject -> issuers
/// Test helper: record an arbitrary issuer→subject vouch (to build a graph).
void addCert(String issuer, String subject) =>
(_certs[subject] ??= {}).add(issuer);
@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 =>
addCert(selfId, subjectPubkey);
@override
Future<void> revoke({required String subjectPubkey}) async =>
_certs[subjectPubkey]?.remove(selfId);
@override
Future<List<Certification>> allCertifications() async => [
for (final entry in _certs.entries)
for (final issuer in entry.value)
Certification(
issuer: issuer,
subject: entry.key,
issuedAt: DateTime(2026),
),
];
@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('a friend-of-a-friend is in your circle; a stranger is not', () async {
final transport = FakeTrustTransport(me)
..addCert(me, 'friend') // you vouch for a friend
..addCert('friend', peer) // your friend vouches for the peer
..addCert('stranger', 'other'); // unrelated to you
final inCircle = TrustCubit(transport, peerPubkey: peer, selfPubkey: me);
await inCircle.load();
expect(inCircle.state.knownToYou, isTrue);
await inCircle.close();
final outside = TrustCubit(transport, peerPubkey: 'other', selfPubkey: me);
await outside.load();
expect(outside.state.knownToYou, isFalse);
await outside.close();
});
test('network membership: enough referent vouches → member', () async {
const params =
WotParams(sigQty: 2, stepMax: 5, sigValidity: Duration(days: 365));
final transport = FakeTrustTransport(me)
..addCert('r1', peer)
..addCert('r2', peer);
final cubit = TrustCubit(
transport,
peerPubkey: peer,
selfPubkey: me,
referents: {'r1', 'r2'},
params: params,
);
await cubit.load();
expect(cubit.state.networkBootstrapped, isTrue);
expect(cubit.state.isNetworkMember, isTrue);
expect(cubit.state.tier, TrustTier.networkMember);
await cubit.close();
});
test('with no referents, membership is undetermined (not a member)',
() async {
final transport = FakeTrustTransport(me)
..addCert('r1', peer)
..addCert('r2', peer);
// referents default to {} → the trust net isn't seeded.
final cubit = TrustCubit(transport, peerPubkey: peer, selfPubkey: me);
await cubit.load();
expect(cubit.state.networkBootstrapped, isFalse);
expect(cubit.state.isNetworkMember, isFalse);
await cubit.close();
});
test('tier falls back to vouched, then unknown', () async {
final vouched = TrustCubit(
FakeTrustTransport(me)..addCert('someone', peer),
peerPubkey: peer,
selfPubkey: me);
await vouched.load();
expect(vouched.state.tier, TrustTier.vouched);
await vouched.close();
final unknown = TrustCubit(FakeTrustTransport(me),
peerPubkey: 'nobody', selfPubkey: me);
await unknown.load();
expect(unknown.state.tier, TrustTier.unknown);
await unknown.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();
});
}