152 lines
4.4 KiB
Dart
152 lines
4.4 KiB
Dart
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));
|
|
});
|
|
});
|
|
}
|