import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nostr/nostr.dart' as nostr; import 'package:buzz/features/channels/send_message_provider.dart'; import 'package:buzz/shared/relay/relay.dart'; void main() { test( 'adds the signed message locally before relay acknowledgement', () async { final session = _PendingPublishRelaySession(); final localMessages = []; final removedIds = []; final completedIds = []; final send = SendMessage( signedEventRelay: SignedEventRelay( session: session, nsec: nostr.Keys.generate().nsec, ), fetchMembers: (_) async => const [], readUserCache: () => const {}, addLocalMessage: (_, event) => localMessages.add(event), completeLocalMessage: (_, eventId) => completedIds.add(eventId), removeLocalMessage: (_, eventId) => removedIds.add(eventId), ); final result = send(channelId: _channelId, content: 'hello'); await session.published; expect(localMessages, hasLength(1)); expect(localMessages.single.id, session.event.id); expect(localMessages.single.content, 'hello'); expect(localMessages.single.channelId, _channelId); expect(removedIds, isEmpty); session.accept(); await result; expect(completedIds, [localMessages.single.id]); expect(removedIds, isEmpty); }, ); test('rolls back the signed local message when publish fails', () async { final session = _PendingPublishRelaySession(); final localMessages = []; final completedIds = []; final removedIds = []; final send = SendMessage( signedEventRelay: SignedEventRelay( session: session, nsec: nostr.Keys.generate().nsec, ), fetchMembers: (_) async => const [], readUserCache: () => const {}, addLocalMessage: (_, event) => localMessages.add(event), completeLocalMessage: (_, eventId) => completedIds.add(eventId), removeLocalMessage: (_, eventId) => removedIds.add(eventId), ); final result = send(channelId: _channelId, content: 'hello'); await session.published; session.reject(); await expectLater(result, throwsException); expect(completedIds, isEmpty); expect(removedIds, [localMessages.single.id]); }); test('cancels delivery after the active community changes', () async { final container = ProviderContainer(); addTearDown(container.dispose); container .read(relayConfigProvider.notifier) .update(baseUrl: 'https://first.example'); final send = container.read(sendMessageProvider); container .read(relayConfigProvider.notifier) .update(baseUrl: 'https://second.example'); await expectLater( send(channelId: _channelId, content: 'old community draft'), throwsA( isA().having( (error) => error.message, 'message', contains('active community changed'), ), ), ); }); } const _channelId = '11111111-1111-4111-8111-111111111111'; class _PendingPublishRelaySession extends RelaySessionNotifier { final Completer _result = Completer(); final Completer _published = Completer(); late NostrEvent event; Future get published => _published.future; @override SessionState build() => const SessionState(status: SessionStatus.connected); @override Future publish( NostrEvent event, { Duration timeout = const Duration(seconds: 8), }) { this.event = event; _published.complete(); return _result.future; } void accept() => _result.complete(event); void reject() => _result.completeError(Exception('relay rejected event')); }