import 'dart:convert'; import 'package:swarm/swarm.dart'; import 'package:test/test.dart'; import 'fake_relay_connection.dart'; const _filter = '{"kinds":[1],"limit":10}'; Map _makeEvent(String id) => { 'id': id, 'pubkey': 'pub', 'created_at': 1, 'kind': 1, 'tags': [], 'content': 'hello', 'sig': 'sig', }; void main() { group('NostrFetchActor', () { test('returns events before EOSE', () async { final fake = FakeRelayConnection(); addTearDown(fake.dispose); final event1 = _makeEvent('id1'); final event2 = _makeEvent('id2'); fake.onReq = (subId) { fake.push(jsonEncode(['EVENT', subId, event1])); fake.push(jsonEncode(['EVENT', subId, event2])); fake.push(jsonEncode(['EOSE', subId])); }; final actor = NostrFetchActor( relayUrls: ['ws://relay1'], connectionFactory: (_) => fake, ); final result = await actor.handle(const FetchEvents(_filter)); expect(result, isA()); expect((result as EventsFetched).events, hasLength(2)); }); test('deduplicates events across relays', () async { final fake1 = FakeRelayConnection(); final fake2 = FakeRelayConnection(); addTearDown(fake1.dispose); addTearDown(fake2.dispose); final event = _makeEvent('shared-id'); fake1.onReq = (subId) { fake1.push(jsonEncode(['EVENT', subId, event])); fake1.push(jsonEncode(['EOSE', subId])); }; fake2.onReq = (subId) { fake2.push(jsonEncode(['EVENT', subId, event])); fake2.push(jsonEncode(['EOSE', subId])); }; final fakes = {'ws://relay1': fake1, 'ws://relay2': fake2}; final actor = NostrFetchActor( relayUrls: ['ws://relay1', 'ws://relay2'], connectionFactory: (url) => fakes[url]!, ); final result = await actor.handle(const FetchEvents(_filter)); expect(result, isA()); expect((result as EventsFetched).events, hasLength(1)); }); test('returns partial results when deadline expires', () async { final fake = FakeRelayConnection(); addTearDown(fake.dispose); final event = _makeEvent('id1'); fake.onReq = (subId) { fake.push(jsonEncode(['EVENT', subId, event])); // no EOSE — deadline will expire }; final actor = NostrFetchActor( relayUrls: ['ws://relay1'], eoseDeadline: const Duration(milliseconds: 100), connectionFactory: (_) => fake, ); final result = await actor.handle(const FetchEvents(_filter)); expect(result, isA()); expect((result as EventsFetched).events, hasLength(1)); }); test('tolerates relay failure and returns events from healthy relay', () async { final fakeDown = FakeRelayConnection()..throwOnConnect = true; final fakeUp = FakeRelayConnection(); addTearDown(fakeDown.dispose); addTearDown(fakeUp.dispose); final event = _makeEvent('id1'); fakeUp.onReq = (subId) { fakeUp.push(jsonEncode(['EVENT', subId, event])); fakeUp.push(jsonEncode(['EOSE', subId])); }; final fakes = {'ws://down': fakeDown, 'ws://up': fakeUp}; final actor = NostrFetchActor( relayUrls: ['ws://down', 'ws://up'], connectionFactory: (url) => fakes[url]!, ); final result = await actor.handle(const FetchEvents(_filter)); expect(result, isA()); expect((result as EventsFetched).events, hasLength(1)); }); test('bounds a hung connect so the fetch cannot stall forever', () async { final fakeHang = FakeRelayConnection()..hangOnConnect = true; final fakeUp = FakeRelayConnection(); addTearDown(fakeHang.dispose); addTearDown(fakeUp.dispose); final event = _makeEvent('id1'); fakeUp.onReq = (subId) { fakeUp.push(jsonEncode(['EVENT', subId, event])); fakeUp.push(jsonEncode(['EOSE', subId])); }; final fakes = {'ws://hang': fakeHang, 'ws://up': fakeUp}; final actor = NostrFetchActor( relayUrls: ['ws://hang', 'ws://up'], connectDeadline: const Duration(milliseconds: 50), connectionFactory: (url) => fakes[url]!, ); // The outer timeout is a guard: without the connect deadline this hangs. final result = await actor .handle(const FetchEvents(_filter)) .timeout(const Duration(seconds: 2)); expect(result, isA()); expect((result as EventsFetched).events, hasLength(1)); }); test('sends CLOSE after EOSE', () async { final fake = FakeRelayConnection(); addTearDown(fake.dispose); final event = _makeEvent('id1'); fake.onReq = (subId) { fake.push(jsonEncode(['EVENT', subId, event])); fake.push(jsonEncode(['EOSE', subId])); }; final actor = NostrFetchActor( relayUrls: ['ws://relay1'], connectionFactory: (_) => fake, ); await actor.handle(const FetchEvents(_filter)); final reqMsg = fake.sentMessages.firstWhere( (m) => (jsonDecode(m) as List)[0] == 'REQ', ); final subId = (jsonDecode(reqMsg) as List)[1] as String; final closeMsgs = fake.sentMessages .where((m) => (jsonDecode(m) as List)[0] == 'CLOSE') .toList(); expect(closeMsgs, hasLength(1)); expect((jsonDecode(closeMsgs[0]) as List)[1], equals(subId)); }); }); }