Merge branch 'spike/block2-derisking'
This commit is contained in:
commit
bd6938c62d
18 changed files with 308 additions and 150 deletions
46
apps/app_seeds/test/services/message_store_test.dart
Normal file
46
apps/app_seeds/test/services/message_store_test.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/services/message_store.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
void main() {
|
||||
late MessageStore store;
|
||||
setUp(() => store = MessageStore(InMemorySecretStore()));
|
||||
|
||||
PrivateMessage msg(String from, String text, int atMs) =>
|
||||
PrivateMessage(
|
||||
fromPubkey: from,
|
||||
text: text,
|
||||
at: DateTime.fromMillisecondsSinceEpoch(atMs));
|
||||
|
||||
test('history is empty for an unknown peer', () async {
|
||||
expect(await store.history('peer'), isEmpty);
|
||||
});
|
||||
|
||||
test('append then history round-trips, oldest first', () async {
|
||||
await store.append('peer', msg('peer', 'hi', 1000));
|
||||
await store.append('peer', msg('me', 'hello', 2000));
|
||||
final history = await store.history('peer');
|
||||
expect(history.map((m) => m.text), ['hi', 'hello']);
|
||||
expect(history.first.fromPubkey, 'peer');
|
||||
expect(history.last.at.millisecondsSinceEpoch, 2000);
|
||||
});
|
||||
|
||||
test('conversations are kept separate per peer', () async {
|
||||
await store.append('a', msg('a', 'toA', 1));
|
||||
await store.append('b', msg('b', 'toB', 1));
|
||||
expect((await store.history('a')).single.text, 'toA');
|
||||
expect((await store.history('b')).single.text, 'toB');
|
||||
});
|
||||
|
||||
test('history is capped to the most recent 200', () async {
|
||||
for (var i = 0; i < 210; i++) {
|
||||
await store.append('peer', msg('me', 'm$i', i));
|
||||
}
|
||||
final history = await store.history('peer');
|
||||
expect(history, hasLength(200));
|
||||
expect(history.first.text, 'm10'); // oldest 10 dropped
|
||||
expect(history.last.text, 'm209');
|
||||
});
|
||||
}
|
||||
|
|
@ -2,8 +2,11 @@ import 'dart:async';
|
|||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/services/message_store.dart';
|
||||
import 'package:tane/state/messages_cubit.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
/// In-memory [MessageTransport]: records sends, lets a test push inbox messages.
|
||||
class FakeMessageTransport implements MessageTransport {
|
||||
final List<({String to, String text})> sent = [];
|
||||
|
|
@ -80,6 +83,33 @@ void main() {
|
|||
await cubit.close();
|
||||
});
|
||||
|
||||
test('loads saved history on start and persists across a new cubit',
|
||||
() async {
|
||||
final store = MessageStore(InMemorySecretStore());
|
||||
await store.append(
|
||||
peer, msg(peer, 'earlier')); // a message from a previous session
|
||||
|
||||
final transport = FakeMessageTransport();
|
||||
final cubit = MessagesCubit(transport,
|
||||
peerPubkey: peer, selfPubkey: me, store: store);
|
||||
await cubit.start();
|
||||
expect(cubit.state.messages.map((m) => m.text), ['earlier']);
|
||||
|
||||
await cubit.send('hi'); // persisted
|
||||
transport.receive(msg(peer, 'reply')); // persisted
|
||||
await pumpEventQueue();
|
||||
expect(cubit.state.messages.map((m) => m.text), ['earlier', 'hi', 'reply']);
|
||||
await cubit.close();
|
||||
|
||||
// A fresh cubit (even offline) sees the saved conversation.
|
||||
final reopened =
|
||||
MessagesCubit(null, peerPubkey: peer, selfPubkey: me, store: store);
|
||||
await reopened.start();
|
||||
expect(reopened.state.messages.map((m) => m.text),
|
||||
['earlier', 'hi', 'reply']);
|
||||
await reopened.close();
|
||||
});
|
||||
|
||||
test('offline (no transport) never throws', () async {
|
||||
final cubit = MessagesCubit(null, peerPubkey: peer, selfPubkey: me)..start();
|
||||
expect(cubit.isOnline, isFalse);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import 'package:tane/state/trust_cubit.dart';
|
|||
class FakeTrustTransport implements TrustTransport {
|
||||
FakeTrustTransport(this.selfId);
|
||||
final String selfId;
|
||||
final Map<String, Set<String>> _certs = {};
|
||||
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 =>
|
||||
|
|
@ -18,14 +22,22 @@ class FakeTrustTransport implements TrustTransport {
|
|||
Duration validity = const Duration(days: 365),
|
||||
String note = '',
|
||||
}) async =>
|
||||
(_certs[subjectPubkey] ??= {}).add(selfId);
|
||||
addCert(selfId, subjectPubkey);
|
||||
|
||||
@override
|
||||
Future<void> revoke({required String subjectPubkey}) async =>
|
||||
_certs[subjectPubkey]?.remove(selfId);
|
||||
|
||||
@override
|
||||
Future<List<Certification>> allCertifications() async => const [];
|
||||
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 {}
|
||||
|
|
@ -62,6 +74,23 @@ void main() {
|
|||
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('never vouches for self', () async {
|
||||
final cubit =
|
||||
TrustCubit(FakeTrustTransport(me), peerPubkey: me, selfPubkey: me);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
|
@ -14,6 +11,12 @@ import 'package:tane/ui/market_screen.dart';
|
|||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
// NOTE: discovery/offer-list rendering is covered by offers_cubit_test.dart
|
||||
// (plain `test` + pumpEventQueue). We deliberately do NOT drive discovery
|
||||
// through `testWidgets` here: the transient "searching" CircularProgressIndicator
|
||||
// never settles under pumpAndSettle and hangs the runner. These widget tests
|
||||
// only cover the offline/setup paths, which have no live stream.
|
||||
|
||||
/// A location provider returning a fixed coarse position.
|
||||
class FakeLocation implements CoarseLocationProvider {
|
||||
FakeLocation(this.lat, this.lon);
|
||||
|
|
@ -24,32 +27,7 @@ class FakeLocation implements CoarseLocationProvider {
|
|||
(lat: lat, lon: lon);
|
||||
}
|
||||
|
||||
/// In-memory offer transport: discover matches by geohash prefix.
|
||||
class FakeOfferTransport implements OfferTransport {
|
||||
final List<Offer> offers;
|
||||
FakeOfferTransport(this.offers);
|
||||
|
||||
@override
|
||||
Future<PublishResult> publish(Offer offer) async =>
|
||||
PublishResult(accepted: true, transportRef: offer.id);
|
||||
|
||||
@override
|
||||
Stream<Offer> discover(DiscoveryQuery query) {
|
||||
final c = StreamController<Offer>();
|
||||
for (final o in offers) {
|
||||
if (o.approxGeohash.startsWith(query.geohashPrefix)) c.add(o);
|
||||
}
|
||||
unawaited(c.close()); // finite: deliver buffered matches, then done
|
||||
return c.stream;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> retract(String offerId) async {}
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
Widget _wrapBody(OffersCubit cubit, {String? selfPubkey}) {
|
||||
Widget _wrapBody(OffersCubit cubit) {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
return TranslationProvider(
|
||||
child: MaterialApp(
|
||||
|
|
@ -63,65 +41,31 @@ Widget _wrapBody(OffersCubit cubit, {String? selfPubkey}) {
|
|||
home: Scaffold(
|
||||
body: BlocProvider.value(
|
||||
value: cubit,
|
||||
child: MarketBody(onConfigure: () {}, selfPubkey: selfPubkey),
|
||||
child: MarketBody(onConfigure: () {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Offer _offer(String id, String summary, OfferType type) => Offer(
|
||||
id: id,
|
||||
authorPubkeyHex: 'ab' * 32,
|
||||
summary: summary,
|
||||
type: type,
|
||||
approxGeohash: 'sp3e9',
|
||||
);
|
||||
Widget _wrapMarket(SocialService social, SocialSettings settings,
|
||||
{CoarseLocationProvider? location}) {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
return TranslationProvider(
|
||||
child: MaterialApp(
|
||||
locale: AppLocale.en.flutterLocale,
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: MarketScreen(social: social, settings: settings, location: location),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('shows a "You" badge on my own listing', (tester) async {
|
||||
const me = 'aa';
|
||||
final cubit = OffersCubit(FakeOfferTransport([
|
||||
Offer(
|
||||
id: '1',
|
||||
authorPubkeyHex: me,
|
||||
summary: 'Mine',
|
||||
type: OfferType.gift,
|
||||
approxGeohash: 'sp3e9',
|
||||
),
|
||||
]));
|
||||
await tester.pumpWidget(_wrapBody(cubit, selfPubkey: me));
|
||||
await cubit.discover('sp3');
|
||||
// Pump a couple of frames (NOT pumpAndSettle — the brief "searching" spinner
|
||||
// never settles). The buffered offers deliver on a microtask.
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('You'), findsOneWidget);
|
||||
// My own listing offers no "Message yourself" button.
|
||||
expect(find.text('Message'), findsNothing);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
testWidgets('renders discovered offers with their type', (tester) async {
|
||||
final cubit = OffersCubit(FakeOfferTransport([
|
||||
_offer('1', 'Tomate rosa de Barbastro', OfferType.gift),
|
||||
_offer('2', 'Judía del ganxet', OfferType.exchange),
|
||||
]));
|
||||
addTearDown(cubit.close);
|
||||
|
||||
await tester.pumpWidget(_wrapBody(cubit));
|
||||
await cubit.discover('sp3');
|
||||
// Pump a couple of frames (NOT pumpAndSettle — the brief "searching" spinner
|
||||
// never settles). The buffered offers deliver on a microtask.
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Tomate rosa de Barbastro'), findsOneWidget);
|
||||
expect(find.text('Judía del ganxet'), findsOneWidget);
|
||||
expect(find.text('Near you'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('offline (no transport) shows the "set up sharing" prompt',
|
||||
(tester) async {
|
||||
final cubit = OffersCubit(null);
|
||||
|
|
@ -139,22 +83,8 @@ void main() {
|
|||
final social = await SocialService.fromRootSeedHex('00' * 32);
|
||||
final settings = SocialSettings(InMemorySecretStore());
|
||||
await settings.setRelayUrls(const []); // offline: don't hit the network
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: MaterialApp(
|
||||
locale: AppLocale.en.flutterLocale,
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: MarketScreen(social: social, settings: settings),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpWidget(_wrapMarket(social, settings));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Seeds near you'), findsOneWidget); // app bar title
|
||||
|
|
@ -166,25 +96,9 @@ void main() {
|
|||
final social = await SocialService.fromRootSeedHex('00' * 32);
|
||||
final settings = SocialSettings(InMemorySecretStore());
|
||||
await settings.setRelayUrls(const []); // offline: don't hit the network
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: MaterialApp(
|
||||
locale: AppLocale.en.flutterLocale,
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: MarketScreen(
|
||||
social: social,
|
||||
settings: settings,
|
||||
location: FakeLocation(41.39, 2.16), // Barcelona-ish
|
||||
),
|
||||
),
|
||||
),
|
||||
_wrapMarket(social, settings, location: FakeLocation(41.39, 2.16)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue