mirror of
https://github.com/block/buzz.git
synced 2026-11-12 04:12:21 +00:00
findJoinedChannel asked for kinds 39000 and 39002 under one limit of 2. A channel with lingering pre-backfill metadata revisions could fill that limit with metadata and drop the roster, so reopening an older DM would fail after the relay had already opened it. Query each kind with its own limit of 1, in parallel. Signed-off-by: Tom Brow <tomb@block.xyz>
185 lines
5.3 KiB
Dart
185 lines
5.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
|
|
import 'package:buzz/features/channels/channel_management_provider.dart';
|
|
import 'package:buzz/features/channels/channels_provider.dart';
|
|
import 'package:buzz/shared/relay/relay.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:nostr/nostr.dart' as nostr;
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
test(
|
|
'opening a DM finishes while the full channel list is stalled',
|
|
() async {
|
|
final keys = nostr.Keys.generate();
|
|
final session = _DmSession(keys.public);
|
|
final container = ProviderContainer(
|
|
retry: (_, _) => null,
|
|
overrides: [
|
|
relaySessionProvider.overrideWith(() => session),
|
|
myPubkeyProvider.overrideWithValue(keys.public),
|
|
],
|
|
);
|
|
addTearDown(container.dispose);
|
|
container.listen(channelsProvider, (_, _) {});
|
|
await session.listStarted.future;
|
|
var valid = true;
|
|
final actions = container.read(
|
|
Provider(
|
|
(ref) => ChannelActions(
|
|
ref: ref,
|
|
session: session,
|
|
signedEventRelay: SignedEventRelay(
|
|
session: session,
|
|
nsec: keys.nsec,
|
|
),
|
|
currentPubkey: keys.public,
|
|
isCommunityValid: () => valid,
|
|
),
|
|
),
|
|
);
|
|
|
|
final opened = await actions
|
|
.openDm(pubkeys: [_peer], waitForChannelList: false)
|
|
.timeout(const Duration(seconds: 2));
|
|
expect(opened.id, _dmId);
|
|
expect(opened.isDm, isTrue);
|
|
expect(opened.isMember, isTrue);
|
|
expect(opened.participantPubkeys, [keys.public, _peer]);
|
|
expect(session.published.single.kind, 41010);
|
|
expect(session.published.single.tags, [
|
|
['p', _peer],
|
|
]);
|
|
|
|
// The sidebar still reconciles, in the background, once the stalled
|
|
// initial load settles.
|
|
session.stalledList.complete(const []);
|
|
await pumpEventQueue();
|
|
expect(session.listLoads, 2);
|
|
session.stalledList = Completer();
|
|
|
|
// A DM the relay confirms but the caller cannot read fails visibly.
|
|
session.member = false;
|
|
await expectLater(
|
|
actions.openDm(pubkeys: [_peer], waitForChannelList: false),
|
|
throwsStateError,
|
|
);
|
|
|
|
// Hidden-DM resurfacing keeps waiting for the list that clears its
|
|
// hidden state, so it cannot outrun its own retry coalescing.
|
|
session.member = true;
|
|
var resurfaced = false;
|
|
unawaited(
|
|
actions.openDm(pubkeys: [_peer]).then((_) => resurfaced = true),
|
|
);
|
|
await pumpEventQueue();
|
|
expect(resurfaced, isFalse);
|
|
|
|
// A community switch during the targeted read cancels the open.
|
|
session.onRead = () => valid = false;
|
|
await expectLater(
|
|
actions.openDm(pubkeys: [_peer], waitForChannelList: false),
|
|
throwsStateError,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
const _dmId = 'dm-id';
|
|
const _peer =
|
|
'1111111111111111111111111111111111111111111111111111111111111111';
|
|
|
|
class _DmSession extends RelaySessionNotifier {
|
|
_DmSession(this.self);
|
|
final String self;
|
|
bool member = true;
|
|
void Function()? onRead;
|
|
final published = <NostrEvent>[];
|
|
Completer<List<NostrEvent>> stalledList = Completer();
|
|
final listStarted = Completer<void>();
|
|
int listLoads = 0;
|
|
|
|
@override
|
|
SessionState build() => const SessionState(status: SessionStatus.connected);
|
|
|
|
@override
|
|
Future<List<NostrEvent>> queryRelay(
|
|
List<NostrFilter> filters, {
|
|
Duration timeout = const Duration(seconds: 8),
|
|
}) async {
|
|
// Every full channel-list load starts with this membership query.
|
|
expect(filters.single.tags, {
|
|
'#p': [self],
|
|
});
|
|
listLoads++;
|
|
if (!listStarted.isCompleted) listStarted.complete();
|
|
return stalledList.future;
|
|
}
|
|
|
|
@override
|
|
Future<NostrEvent> publish(
|
|
NostrEvent event, {
|
|
Duration timeout = const Duration(seconds: 8),
|
|
}) async {
|
|
published.add(event);
|
|
return NostrEvent(
|
|
id: event.id,
|
|
pubkey: event.pubkey,
|
|
createdAt: event.createdAt,
|
|
kind: event.kind,
|
|
tags: event.tags,
|
|
content: 'response:${jsonEncode({'channel_id': _dmId})}',
|
|
sig: event.sig,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<List<NostrEvent>> fetchHistory(
|
|
NostrFilter filter, {
|
|
Duration timeout = const Duration(seconds: 8),
|
|
}) async {
|
|
expect(filter.tags, {
|
|
'#d': [_dmId],
|
|
});
|
|
onRead?.call();
|
|
// Serve like a relay: newest first, one limit across every kind. A stale
|
|
// metadata revision from before the d_tag backfill still lingers.
|
|
final events = [
|
|
_event(39000, 3, [
|
|
['name', 'DM'],
|
|
['t', 'dm'],
|
|
['private'],
|
|
['hidden'],
|
|
['p', self],
|
|
['p', _peer],
|
|
]),
|
|
_event(39000, 2, [
|
|
['name', 'DM'],
|
|
['t', 'dm'],
|
|
['private'],
|
|
]),
|
|
_event(39002, 1, [
|
|
if (member) ['p', self, '', 'member'],
|
|
['p', _peer, '', 'member'],
|
|
]),
|
|
].where((e) => filter.kinds.contains(e.kind)).toList();
|
|
return events.take(filter.limit).toList();
|
|
}
|
|
}
|
|
|
|
NostrEvent _event(int kind, int createdAt, List<List<String>> tags) =>
|
|
NostrEvent(
|
|
id: '$kind-$createdAt',
|
|
pubkey: 'relay',
|
|
createdAt: createdAt,
|
|
kind: kind,
|
|
tags: [
|
|
['d', _dmId],
|
|
...tags,
|
|
],
|
|
content: '',
|
|
sig: '',
|
|
);
|