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_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 and announces changes, /// so the inbox list refreshes 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'; late MessageStore store; late InboxService inbox; setUp(() async { store = MessageStore(InMemorySecretStore()); inbox = InboxService( social: await SocialService.fromRootSeedHex(seedHex), settings: SocialSettings(InMemorySecretStore()), store: store, ); }); tearDown(() => inbox.stop()); PrivateMessage msg(String from, String text, int atMs) => PrivateMessage( fromPubkey: from, text: text, at: DateTime.fromMillisecondsSinceEpoch(atMs)); 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; late String myPubkey; setUp(() async { final social = await SocialService.fromRootSeedHex(seedHex); myPubkey = social.publicKeyHex; unread = UnreadService(store, InMemorySecretStore()); notifications = _RecordingNotifications(); inbox = InboxService( social: social, settings: SocialSettings(InMemorySecretStore()), 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); }); }); }