import 'dart:convert'; import 'package:swarm/swarm.dart'; import 'package:test/test.dart'; import 'fake_relay_connection.dart'; const _event = '{' '"id":"abc123",' '"pubkey":"pub",' '"created_at":1,' '"kind":1,' '"tags":[],' '"content":"hi",' '"sig":"sig"' '}'; void main() { group('NostrPublishActor', () { tearDown(() async {}); test('sends correct EVENT format', () async { final fake = FakeRelayConnection(); addTearDown(fake.dispose); final actor = NostrPublishActor( relayUrls: ['ws://relay1'], retryConfig: const RetryConfig(maxAttempts: 1), connectionFactory: (_) => fake, ); await actor.handle(const PublishBatch([_event])); final eventMsgs = fake.sentMessages .where((m) => (jsonDecode(m) as List)[0] == 'EVENT') .toList(); expect(eventMsgs, hasLength(1)); final decoded = jsonDecode(eventMsgs[0]) as List; expect(decoded[0], equals('EVENT')); expect(decoded[1], isA()); expect((decoded[1] as Map)['id'], equals('abc123')); }); test('returns RelayConfirmed on success', () async { final fake = FakeRelayConnection(); addTearDown(fake.dispose); final actor = NostrPublishActor( relayUrls: ['ws://relay1'], retryConfig: const RetryConfig(maxAttempts: 1), connectionFactory: (_) => fake, ); final result = await actor.handle(const PublishBatch([_event])); expect(result, isA()); final outcomes = (result as BatchPublished).outcomes; expect(outcomes['ws://relay1'], isA()); }); test('returns RelayRejected with no retry', () async { final fake = FakeRelayConnection()..autoRespond = false; addTearDown(fake.dispose); fake.queuedResponses.add(jsonEncode(['OK', 'abc123', false, 'spam'])); final actor = NostrPublishActor( relayUrls: ['ws://relay1'], retryConfig: const RetryConfig(maxAttempts: 1), connectionFactory: (_) => fake, ); final result = await actor.handle(const PublishBatch([_event])); expect(result, isA()); final outcomes = (result as BatchPublished).outcomes; expect(outcomes['ws://relay1'], isA()); expect((outcomes['ws://relay1'] as RelayRejected).reason, equals('spam')); final eventMsgs = fake.sentMessages .where((m) => (jsonDecode(m) as List)[0] == 'EVENT') .toList(); expect(eventMsgs, hasLength(1)); }); test('returns RelayFailed on timeout and retries', () async { final fake = FakeRelayConnection()..autoRespond = false; addTearDown(fake.dispose); final actor = NostrPublishActor( relayUrls: ['ws://relay1'], retryConfig: const RetryConfig( maxAttempts: 2, baseDelay: Duration(milliseconds: 1), jitter: false, ), batchTimeout: const Duration(milliseconds: 50), connectionFactory: (_) => fake, ); final result = await actor.handle(const PublishBatch([_event])); expect(result, isA()); final outcomes = (result as BatchPublished).outcomes; expect(outcomes['ws://relay1'], isA()); final eventMsgs = fake.sentMessages .where((m) => (jsonDecode(m) as List)[0] == 'EVENT') .toList(); expect(eventMsgs, hasLength(2)); }); test('returns RelayFailed on connection error', () async { final fake = FakeRelayConnection()..throwOnConnect = true; addTearDown(fake.dispose); final actor = NostrPublishActor( relayUrls: ['ws://relay1'], retryConfig: const RetryConfig(maxAttempts: 1), connectionFactory: (_) => fake, ); final result = await actor.handle(const PublishBatch([_event])); expect(result, isA()); final outcomes = (result as BatchPublished).outcomes; expect(outcomes['ws://relay1'], isA()); }); test('fans out to multiple relays', () async { final fake1 = FakeRelayConnection(); final fake2 = FakeRelayConnection(); addTearDown(fake1.dispose); addTearDown(fake2.dispose); final fakes = {'ws://relay1': fake1, 'ws://relay2': fake2}; final actor = NostrPublishActor( relayUrls: ['ws://relay1', 'ws://relay2'], retryConfig: const RetryConfig(maxAttempts: 1), connectionFactory: (url) => fakes[url]!, ); final result = await actor.handle(const PublishBatch([_event])); expect(result, isA()); final outcomes = (result as BatchPublished).outcomes; expect(outcomes.keys, containsAll(['ws://relay1', 'ws://relay2'])); expect(outcomes['ws://relay1'], isA()); expect(outcomes['ws://relay2'], isA()); }); }); }