swarm/test/nostr_fetch_actor_test.dart

153 lines
4.4 KiB
Dart
Raw Normal View History

2026-06-07 16:45:43 +03:00
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<String, dynamic> _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<EventsFetched>());
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<EventsFetched>());
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<EventsFetched>());
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<EventsFetched>());
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));
});
});
}