import 'package:commons_core/commons_core.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tane/services/inbox_service.dart'; import 'package:tane/services/message_store.dart'; import 'package:tane/services/notification_service.dart'; import 'package:tane/services/social_connection.dart'; import 'package:tane/services/social_service.dart'; import 'package:tane/services/social_settings.dart'; import 'package:tane/services/unread_service.dart'; import '../support/test_support.dart'; /// Records notification calls instead of touching the OS plugin. class _RecordingNotifications extends NotificationService { _RecordingNotifications() : super(supported: false); final titles = []; final peers = []; @override Future showMessage({ required String peerPubkey, required String title, }) async { peers.add(peerPubkey); titles.add(title); } } /// The app-wide inbox listener persists incoming messages, updates unread and /// notifies — even when the specific chat isn't open. Driven through the /// [InboxService.ingest] seam so no relay/network is involved. void main() { const seedHex = '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'; // A connection that never actually opens — ingest doesn't use it. Future offlineConnection() async => SocialConnection( social: await SocialService.fromRootSeedHex(seedHex), settings: SocialSettings(InMemorySecretStore()), open: (_) async => throw StateError('unused in ingest tests'), online: const Stream.empty(), ); late MessageStore store; late InboxService inbox; setUp(() async { store = MessageStore(newTestChatDatabase()); inbox = InboxService( connection: await offlineConnection(), selfPubkey: 'me', store: store, ); }); tearDown(() => inbox.stop()); PrivateMessage msg(String from, String text, int atMs) => PrivateMessage( fromPubkey: from, text: text, at: DateTime.fromMillisecondsSinceEpoch(atMs)); test('messages from a blocked peer are dropped before storage', () async { final settings = SocialSettings(InMemorySecretStore()); await settings.block('mallory'); inbox = InboxService( connection: await offlineConnection(), selfPubkey: 'me', store: store, settings: settings, ); await inbox.ingest(msg('mallory', 'spam', 1000)); await inbox.ingest(msg('alice', 'hola', 2000)); final convos = await store.conversations(); expect(convos.single.peerPubkey, 'alice'); // mallory never landed }); test('an incoming message is persisted into its conversation', () async { await inbox.ingest(msg('alice', 'got seeds?', 1000)); final convos = await store.conversations(); expect(convos.single.peerPubkey, 'alice'); expect(convos.single.lastText, 'got seeds?'); }); test('a new message announces a change; a duplicate stays silent', () async { final changes = []; final sub = inbox.changes.listen(changes.add); await inbox.ingest(msg('alice', 'hola', 1000)); await inbox.ingest(msg('alice', 'hola', 1000)); // relay re-delivery await Future.delayed(Duration.zero); // let the broadcast flush expect(changes, hasLength(1)); // only the first, new one fired expect(await store.history('alice'), hasLength(1)); await sub.cancel(); }); group('unread + notification hooks', () { late UnreadService unread; late _RecordingNotifications notifications; const myPubkey = 'me'; setUp(() async { unread = UnreadService(store, InMemorySecretStore()); notifications = _RecordingNotifications(); inbox = InboxService( connection: await offlineConnection(), selfPubkey: myPubkey, store: store, unread: unread, notifications: notifications, ); }); tearDown(() => unread.dispose()); test('a new peer message updates unread and notifies', () async { await inbox.ingest(msg('alice', 'hola', 1000)); expect(await unread.unreadCount('alice'), 1); expect(notifications.peers, ['alice']); }); test('a duplicate re-delivery neither counts nor notifies', () async { await inbox.ingest(msg('alice', 'hola', 1000)); await inbox.ingest(msg('alice', 'hola', 1000)); expect(await unread.unreadCount('alice'), 1); expect(notifications.peers, hasLength(1)); }); test('a message authored by me is never notified', () async { // NIP-17 doesn't loop your own gift wrap back; the guard is defensive. await inbox.ingest(msg(myPubkey, 'echo', 1000)); expect(notifications.peers, isEmpty); }); test('the open chat is not notified and stays read', () async { unread.activePeer = 'alice'; await inbox.ingest(msg('alice', 'while open', 1000)); expect(await unread.unreadCount('alice'), 0); expect(notifications.peers, isEmpty); }); }); }