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.
160 lines
5.1 KiB
Dart
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();
|
|
});
|
|
}
|