1use nostr_sdk::prelude::*;
11
12use super::derive::channel_pseudonym;
13use super::envelope::{open_message_multi, seal_message_with_ephemeral, seal_with_signed_inner, OpenedMessage};
14#[cfg(test)]
15use super::envelope::open_message;
16use super::transport::{Evidence, Query, Transport};
17use super::{Channel, Community};
18use crate::stored_event::event_kind;
19
20pub async fn publish_message<T: Transport + ?Sized>(
28 transport: &T,
29 community: &Community,
30 channel: &Channel,
31 author: &Keys,
32 content: &str,
33 ms: u64,
34) -> Result<(Event, Keys), String> {
35 let ephemeral = Keys::generate();
36 let outer = seal_message_with_ephemeral(
37 &ephemeral, author, &channel.key, &channel.id, channel.epoch, content, ms,
38 )
39 .map_err(|e| e.to_string())?;
40 transport.publish(&outer, &community.relays).await?;
41 Ok((outer, ephemeral))
42}
43
44pub async fn publish_signed_message<T: Transport + ?Sized>(
52 transport: &T,
53 community: &Community,
54 channel: &Channel,
55 inner: &Event,
56 durable: bool,
57) -> Result<(Event, Keys), String> {
58 let ephemeral = Keys::generate();
59 let outer = seal_with_signed_inner(&ephemeral, inner, &channel.key, &channel.id, channel.epoch)
60 .map_err(|e| e.to_string())?;
61 if durable {
62 transport.publish_durable(&outer, &community.relays).await?;
63 } else {
64 transport.publish(&outer, &community.relays).await?;
65 }
66 Ok((outer, ephemeral))
67}
68
69pub async fn delete_own_message<T: Transport + ?Sized>(
73 transport: &T,
74 relays: &[String],
75 ephemeral: &Keys,
76 outer_event_id: EventId,
77) -> Result<(), String> {
78 let deletion = EventBuilder::delete(EventDeletionRequest::new().ids([outer_event_id]))
79 .sign_with_keys(ephemeral)
80 .map_err(|e| e.to_string())?;
81 transport.publish_durable(&deletion, relays).await
82}
83
84fn channel_read_pseudonyms(channel: &Channel) -> Vec<String> {
88 channel
89 .read_epoch_keys()
90 .iter()
91 .map(|(epoch, key)| channel_pseudonym(key, &channel.id, *epoch).to_hex())
92 .collect()
93}
94
95pub async fn fetch_channel_messages<T: Transport + ?Sized>(
98 transport: &T,
99 community: &Community,
100 channel: &Channel,
101) -> Result<Vec<OpenedMessage>, String> {
102 let query = Query {
103 kinds: vec![event_kind::COMMUNITY_MESSAGE],
104 z_tags: channel_read_pseudonyms(channel),
105 since: None,
106 evidence: Evidence::Fast,
109 ..Default::default()
110 };
111 let events = transport.fetch(&query, &community.relays).await?;
112 let epoch_keys = channel.read_epoch_keys();
117 let mut opened: Vec<OpenedMessage> = Vec::new();
118 let mut dropped = 0usize;
119 for ev in &events {
120 match open_message_multi(ev, &channel.id, &epoch_keys) {
121 Ok(msg) => opened.push(msg),
122 Err(e) => {
123 dropped += 1;
124 crate::log_debug!("[community] dropped event {}: {}", ev.id.to_hex(), e);
125 }
126 }
127 }
128 if dropped > 0 {
129 crate::log_debug!(
130 "[community] channel {} fetch: {} opened, {} dropped",
131 channel.id.to_hex(),
132 opened.len(),
133 dropped
134 );
135 }
136 {
141 let mut seen = std::collections::HashSet::new();
142 opened.retain(|m| seen.insert(m.message_id));
143 }
144 opened.sort_by(|a, b| {
146 a.ms.unwrap_or(0)
147 .cmp(&b.ms.unwrap_or(0))
148 .then_with(|| a.message_id.to_hex().cmp(&b.message_id.to_hex()))
149 });
150 Ok(opened)
151}
152
153pub async fn fetch_channel_events<T: Transport + ?Sized>(
159 transport: &T,
160 community: &Community,
161 channel: &Channel,
162) -> Result<Vec<Event>, String> {
163 let query = Query {
164 kinds: vec![
165 event_kind::COMMUNITY_MESSAGE,
166 event_kind::COMMUNITY_REACTION,
167 event_kind::COMMUNITY_EDIT,
168 event_kind::COMMUNITY_DELETE,
169 event_kind::COMMUNITY_PRESENCE,
170 event_kind::COMMUNITY_KICK,
171 event_kind::COMMUNITY_WEBXDC,
172 ],
173 z_tags: channel_read_pseudonyms(channel),
174 since: None,
175 evidence: Evidence::Fast,
177 ..Default::default()
178 };
179 transport.fetch(&query, &community.relays).await
180}
181
182pub async fn fetch_channel_page<T: Transport + ?Sized>(
189 transport: &T,
190 community: &Community,
191 channel: &Channel,
192 until: Option<u64>,
193 since: Option<u64>,
194 limit: usize,
195) -> Result<Vec<Event>, String> {
196 let query = Query {
197 kinds: vec![
198 event_kind::COMMUNITY_MESSAGE,
199 event_kind::COMMUNITY_REACTION,
200 event_kind::COMMUNITY_EDIT,
201 event_kind::COMMUNITY_DELETE,
202 event_kind::COMMUNITY_PRESENCE,
203 event_kind::COMMUNITY_KICK,
204 event_kind::COMMUNITY_WEBXDC,
205 ],
206 z_tags: channel_read_pseudonyms(channel),
210 until,
211 since,
216 limit: Some(limit),
217 evidence: Evidence::Fast,
221 ..Default::default()
222 };
223 transport.fetch(&query, &community.relays).await
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::community::transport::memory::MemoryRelay;
230 use crate::community::{Channel, ChannelKey, Epoch};
231
232 fn community() -> Community {
234 Community::create("HQ", "general", vec!["r1".into(), "r2".into(), "r3".into()])
235 }
236
237 fn member_view(of: &Community) -> Community {
240 Community {
241 id: of.id,
242 server_root_key: of.server_root_key.clone(),
243 server_root_epoch: of.server_root_epoch,
244 name: of.name.clone(),
245 description: of.description.clone(),
246 icon: of.icon.clone(),
247 banner: of.banner.clone(),
248 relays: of.relays.clone(),
249 channels: of.channels.clone(),
250 owner_attestation: of.owner_attestation.clone(),
251 dissolved: of.dissolved,
252 }
253 }
254
255 #[tokio::test]
256 async fn two_clients_exchange_via_relay() {
257 let relay = MemoryRelay::new();
258 let community = community();
259 let channel = community.channels[0].clone();
260
261 let alice = Keys::generate();
262 publish_message(&relay, &community, &channel, &alice, "gm from alice", 100)
263 .await
264 .unwrap();
265
266 let bob_view = member_view(&community);
268 let msgs = fetch_channel_messages(&relay, &bob_view, &bob_view.channels[0])
269 .await
270 .unwrap();
271 assert_eq!(msgs.len(), 1);
272 assert_eq!(msgs[0].content, "gm from alice");
273 assert_eq!(msgs[0].author, alice.public_key());
274 }
275
276 fn epoch_view(base: &Channel, key: ChannelKey, epoch: u64) -> Channel {
278 Channel {
279 id: base.id, key, epoch: Epoch(epoch), name: base.name.clone(),
280 banned: Vec::new(), protected: Vec::new(), roster: Default::default(), epoch_keys: Vec::new(),
281 dissolved: false,
282 }
283 }
284
285 #[tokio::test]
286 async fn fetch_spans_held_epochs_after_rekeys() {
287 let relay = MemoryRelay::new();
290 let community = community();
291 let base = community.channels[0].clone();
292 let alice = Keys::generate();
293 let k0 = base.key.clone();
294 let k1 = ChannelKey([0x11u8; 32]);
295 let k2 = ChannelKey([0x22u8; 32]);
296
297 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "epoch0", 100).await.unwrap();
298 publish_message(&relay, &community, &epoch_view(&base, k1.clone(), 1), &alice, "epoch1", 200).await.unwrap();
299 publish_message(&relay, &community, &epoch_view(&base, k2.clone(), 2), &alice, "epoch2", 300).await.unwrap();
300
301 let mut reader = member_view(&community);
303 reader.channels[0] = Channel {
304 id: base.id, key: k2.clone(), epoch: Epoch(2), name: base.name.clone(),
305 banned: Vec::new(), protected: Vec::new(), roster: Default::default(),
306 epoch_keys: vec![(Epoch(0), k0), (Epoch(1), k1), (Epoch(2), k2.clone())],
307 dissolved: false,
308 };
309 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
310 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
311 assert_eq!(contents, vec!["epoch0", "epoch1", "epoch2"], "every held epoch's messages returned, none stranded");
312
313 let mut head_only = member_view(&community);
316 head_only.channels[0] = epoch_view(&base, k2, 2);
317 let only = fetch_channel_messages(&relay, &head_only, &head_only.channels[0]).await.unwrap();
318 assert_eq!(only.iter().map(|m| m.content.as_str()).collect::<Vec<_>>(), vec!["epoch2"],
319 "head-only reader sees only the current epoch (single-epoch fallback)");
320 }
321
322 #[tokio::test]
323 async fn page_spans_the_epoch_boundary() {
324 let relay = MemoryRelay::new();
328 let community = community();
329 let base = community.channels[0].clone();
330 let alice = Keys::generate();
331 let k0 = base.key.clone();
332 let k1 = ChannelKey([0x33u8; 32]);
333 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "old-a", 100).await.unwrap();
334 publish_message(&relay, &community, &epoch_view(&base, k0.clone(), 0), &alice, "old-b", 200).await.unwrap();
335 publish_message(&relay, &community, &epoch_view(&base, k1.clone(), 1), &alice, "new-c", 300).await.unwrap();
336
337 let mut reader = member_view(&community);
338 reader.channels[0] = Channel {
339 id: base.id, key: k1.clone(), epoch: Epoch(1), name: base.name.clone(),
340 banned: Vec::new(), protected: Vec::new(), roster: Default::default(),
341 epoch_keys: vec![(Epoch(0), k0), (Epoch(1), k1)],
342 dissolved: false,
343 };
344 let page = fetch_channel_page(&relay, &reader, &reader.channels[0], None, None, 20).await.unwrap();
345 let opened: Vec<String> = page.iter()
346 .filter_map(|e| open_message_multi(e, &reader.channels[0].id, &reader.channels[0].read_epoch_keys()).ok())
347 .map(|m| m.content)
348 .collect();
349 assert!(opened.contains(&"new-c".to_string()), "head-epoch message in the page");
350 assert!(opened.contains(&"old-a".to_string()) && opened.contains(&"old-b".to_string()),
351 "older-epoch messages in the SAME page (across the epoch boundary)");
352 }
353
354 #[tokio::test]
355 async fn three_clients_one_broadcast() {
356 let relay = MemoryRelay::new();
358 let community = community();
359 let channel = community.channels[0].clone();
360 let alice = Keys::generate();
361 publish_message(&relay, &community, &channel, &alice, "hello all", 1)
362 .await
363 .unwrap();
364
365 for reader in [member_view(&community), member_view(&community)] {
366 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0])
367 .await
368 .unwrap();
369 assert_eq!(msgs.len(), 1);
370 assert_eq!(msgs[0].content, "hello all");
371 }
372 }
373
374 #[tokio::test]
375 async fn non_member_with_wrong_key_cannot_read() {
376 let relay = MemoryRelay::new();
377 let community = community();
378 let channel = community.channels[0].clone();
379 let alice = Keys::generate();
380 publish_message(&relay, &community, &channel, &alice, "secret", 1)
381 .await
382 .unwrap();
383
384 let mut outsider = member_view(&community);
387 outsider.channels[0].key = ChannelKey([0xeeu8; 32]);
388 let msgs = fetch_channel_messages(&relay, &outsider, &outsider.channels[0])
389 .await
390 .unwrap();
391 assert!(msgs.is_empty(), "wrong key derives a different pseudonym → no hits");
392
393 let raw = relay
395 .fetch(
396 &Query { kinds: vec![event_kind::COMMUNITY_MESSAGE], ..Default::default() },
397 &community.relays,
398 )
399 .await
400 .unwrap();
401 assert_eq!(raw.len(), 1);
402 assert!(open_message(&raw[0], &outsider.channels[0].key, &channel.id, channel.epoch).is_err());
403 }
404
405 #[tokio::test]
406 async fn messages_return_in_ms_order() {
407 let relay = MemoryRelay::new();
408 let community = community();
409 let channel = community.channels[0].clone();
410 let alice = Keys::generate();
411 publish_message(&relay, &community, &channel, &alice, "third", 300).await.unwrap();
413 publish_message(&relay, &community, &channel, &alice, "first", 100).await.unwrap();
414 publish_message(&relay, &community, &channel, &alice, "second", 200).await.unwrap();
415
416 let reader = member_view(&community);
417 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
418 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
419 assert_eq!(contents, vec!["first", "second", "third"]);
420 }
421
422 #[tokio::test]
423 async fn other_channel_traffic_is_not_returned() {
424 let relay = MemoryRelay::new();
427 let community = community();
428 let chan_a = community.channels[0].clone();
429 let chan_b = Channel {
430 id: super::super::ChannelId([0x77u8; 32]),
431 key: ChannelKey([0x88u8; 32]),
432 epoch: Epoch(0),
433 name: "other".into(),
434 banned: Vec::new(),
435 protected: Vec::new(), roster: Default::default(),
436 epoch_keys: Vec::new(),
437 dissolved: false,
438 };
439 let alice = Keys::generate();
440 publish_message(&relay, &community, &chan_a, &alice, "in A", 1).await.unwrap();
441 publish_message(&relay, &community, &chan_b, &alice, "in B", 1).await.unwrap();
442
443 let reader = member_view(&community);
444 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
445 assert_eq!(msgs.len(), 1);
446 assert_eq!(msgs[0].content, "in A");
447 }
448
449 #[tokio::test]
450 async fn two_distinct_messages_are_not_collapsed() {
451 let relay = MemoryRelay::new();
455 let community = community();
456 let channel = community.channels[0].clone();
457 let alice = Keys::generate();
458 publish_message(&relay, &community, &channel, &alice, "one", 1).await.unwrap();
459 publish_message(&relay, &community, &channel, &alice, "two", 2).await.unwrap();
460
461 let reader = member_view(&community);
462 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
463 assert_eq!(msgs.len(), 2);
464 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
465 assert!(contents.contains(&"one") && contents.contains(&"two"));
466 }
467
468 #[tokio::test]
469 async fn backfill_fetches_and_applies_messages_then_reactions() {
470 use super::super::inbound::{process_channel_batch, IncomingEvent};
475 let relay = MemoryRelay::new();
476 let community = community();
477 let channel = community.channels[0].clone();
478 let alice = Keys::generate();
479 let bob = Keys::generate();
480
481 let (m1_outer, _) = publish_message(&relay, &community, &channel, &alice, "hello", 1).await.unwrap();
482 publish_message(&relay, &community, &channel, &alice, "world", 2).await.unwrap();
483 let m1_inner = open_message(&m1_outer, &channel.key, &channel.id, channel.epoch).unwrap().message_id.to_hex();
485 let react_inner = super::super::envelope::build_inner_typed(
486 bob.public_key(), &channel.id, channel.epoch,
487 event_kind::COMMUNITY_REACTION, "🔥", 3, Some(&m1_inner), &[],
488 ).sign_with_keys(&bob).unwrap();
489 let react_outer = seal_with_signed_inner(&Keys::generate(), &react_inner, &channel.key, &channel.id, channel.epoch).unwrap();
490 relay.publish(&react_outer, &community.relays).await.unwrap();
491
492 let events = fetch_channel_events(&relay, &community, &channel).await.unwrap();
493 assert_eq!(events.len(), 3, "two messages + one reaction fetched");
494 let mut state = crate::state::ChatState::new();
495 let applied = process_channel_batch(&mut state, &events, &channel, &bob.public_key());
496
497 let new_msgs = applied.iter().filter(|e| matches!(e, IncomingEvent::NewMessage(_))).count();
498 let updates: Vec<&String> = applied.iter().filter_map(|e| match e {
499 IncomingEvent::Updated { target_id, .. } => Some(target_id),
500 _ => None,
501 }).collect();
502 assert_eq!(new_msgs, 2, "both messages backfilled");
503 assert_eq!(updates.len(), 1, "reaction applied during backfill");
504 assert_eq!(updates[0], &m1_inner, "reaction landed on its target message");
505 }
506
507 #[tokio::test]
508 async fn same_inner_message_in_two_wrappers_collapses() {
509 let relay = MemoryRelay::new();
513 let community = community();
514 let channel = community.channels[0].clone();
515 let alice = Keys::generate();
516
517 let inner = super::super::envelope::build_inner_event(alice.public_key(), &channel.id, channel.epoch, "dup me", 1, None)
519 .sign_with_keys(&alice)
520 .unwrap();
521 let outer_a = seal_with_signed_inner(&Keys::generate(), &inner, &channel.key, &channel.id, channel.epoch).unwrap();
523 let outer_b = seal_with_signed_inner(&Keys::generate(), &inner, &channel.key, &channel.id, channel.epoch).unwrap();
524 assert_ne!(outer_a.id, outer_b.id, "distinct outer wrappers");
525 relay.publish(&outer_a, &community.relays).await.unwrap();
526 relay.publish(&outer_b, &community.relays).await.unwrap();
527
528 let reader = member_view(&community);
529 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
530 assert_eq!(msgs.len(), 1, "same inner id collapses across wrappers");
531 assert_eq!(msgs[0].content, "dup me");
532 }
533
534 #[tokio::test]
535 async fn bad_event_dropped_good_event_kept_in_same_batch() {
536 let relay = MemoryRelay::new();
539 let community = community();
540 let channel = community.channels[0].clone();
541 let alice = Keys::generate();
542 publish_message(&relay, &community, &channel, &alice, "valid", 1).await.unwrap();
544 let pseudonym =
546 super::super::derive::channel_pseudonym(&channel.key, &channel.id, channel.epoch);
547 let garbage = EventBuilder::new(Kind::Custom(event_kind::COMMUNITY_MESSAGE), "not-base64-or-cipher!!")
548 .tags([
549 Tag::custom(
550 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
551 [pseudonym.to_hex()],
552 ),
553 Tag::custom(TagKind::Custom("v".into()), ["1".to_string()]),
554 ])
555 .sign_with_keys(&Keys::generate())
556 .unwrap();
557 relay.publish(&garbage, &community.relays).await.unwrap();
558
559 let reader = member_view(&community);
560 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
561 assert_eq!(msgs.len(), 1);
562 assert_eq!(msgs[0].content, "valid");
563 }
564
565 #[tokio::test]
566 async fn publish_retains_key_and_owner_can_delete() {
567 let relay = MemoryRelay::new();
568 let community = community();
569 let channel = community.channels[0].clone();
570 let alice = Keys::generate();
571
572 let (outer, ephemeral) =
573 publish_message(&relay, &community, &channel, &alice, "deletable", 1).await.unwrap();
574 assert_eq!(ephemeral.public_key(), outer.pubkey);
576
577 let before = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
578 assert_eq!(before.len(), 1);
579
580 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
582 let after = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
583 assert!(after.is_empty(), "owner's deletion should remove the message");
584 }
585
586 #[tokio::test]
587 async fn deletion_by_a_different_key_is_ignored() {
588 let relay = MemoryRelay::new();
590 let community = community();
591 let channel = community.channels[0].clone();
592 let alice = Keys::generate();
593 let (outer, ephemeral) =
594 publish_message(&relay, &community, &channel, &alice, "mine", 1).await.unwrap();
595
596 let attacker = Keys::generate();
597 delete_own_message(&relay, &community.relays, &attacker, outer.id).await.unwrap();
598 let after = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
599 assert_eq!(after.len(), 1, "a foreign key must not delete someone else's message");
600
601 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
604 let gone = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
605 assert!(gone.is_empty(), "the original signer's key must delete it");
606 }
607
608 #[tokio::test]
609 async fn deletion_must_reach_all_relays_to_take_effect() {
610 let relay = MemoryRelay::new();
614 let community = community(); let channel = community.channels[0].clone();
616 let alice = Keys::generate();
617 let (outer, ephemeral) =
618 publish_message(&relay, &community, &channel, &alice, "sticky", 1).await.unwrap();
619
620 delete_own_message(&relay, &["r1".to_string()], &ephemeral, outer.id).await.unwrap();
622 let still = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
624 assert_eq!(still.len(), 1, "deletion on a subset must not remove it everywhere");
625
626 delete_own_message(&relay, &community.relays, &ephemeral, outer.id).await.unwrap();
628 let gone = fetch_channel_messages(&relay, &community, &channel).await.unwrap();
629 assert!(gone.is_empty());
630 }
631
632 #[tokio::test]
638 #[ignore]
639 async fn live_relay_roundtrip_and_cleanup() {
640 use super::super::transport::LiveTransport;
641 use crate::community::derive::channel_pseudonym;
642
643 let _ = rustls::crypto::ring::default_provider().install_default();
644
645 let relays = vec!["wss://jskitty.com/nostr".to_string()];
646 let community = Community::create("LiveTest", "general", relays.clone());
647 let channel = community.channels[0].clone();
648 let alice = Keys::generate();
649 let transport = LiveTransport::with_timeout(std::time::Duration::from_secs(12));
650
651 let pseudonym = channel_pseudonym(&channel.key, &channel.id, channel.epoch).to_hex();
652 eprintln!("[live] channel pseudonym (z tag) = {pseudonym}");
653
654 let now_ms = std::time::SystemTime::now()
658 .duration_since(std::time::UNIX_EPOCH)
659 .unwrap()
660 .as_millis() as u64;
661 let mut published: Vec<(Keys, Event)> = Vec::new();
662 let mut publish_errs: Vec<String> = Vec::new();
663 for (i, body) in ["live hello one", "live hello two"].iter().enumerate() {
664 let ephemeral = Keys::generate();
665 let outer = seal_message_with_ephemeral(
666 &ephemeral, &alice, &channel.key, &channel.id, channel.epoch, body, now_ms + i as u64,
667 )
668 .expect("seal");
669 match transport.publish(&outer, &relays).await {
670 Ok(()) => {
671 eprintln!("[live] published event {}", outer.id.to_hex());
672 published.push((ephemeral, outer));
673 }
674 Err(e) => publish_errs.push(e),
675 }
676 }
677
678 let fetched = fetch_channel_messages(&transport, &community, &channel).await;
680
681 let mut cleanup_ok = true;
684 for (ephemeral, outer) in &published {
685 let del = EventBuilder::delete(EventDeletionRequest::new().ids([outer.id]))
686 .sign_with_keys(ephemeral)
687 .expect("build deletion");
688 if let Err(e) = transport.publish(&del, &relays).await {
689 cleanup_ok = false;
690 eprintln!("[live] CLEANUP FAILED for {}: {e} — manual delete may be needed", outer.id.to_hex());
691 } else {
692 eprintln!("[live] deleted event {}", outer.id.to_hex());
693 }
694 }
695
696 assert!(publish_errs.is_empty(), "publish errors: {publish_errs:?}");
698 let msgs = fetched.expect("fetch failed");
699 let contents: Vec<&str> = msgs.iter().map(|m| m.content.as_str()).collect();
700 assert!(contents.contains(&"live hello one"), "got {contents:?}");
701 assert!(contents.contains(&"live hello two"), "got {contents:?}");
702 for m in &msgs {
703 assert_eq!(m.author, alice.public_key(), "author recovered over the wire");
704 }
705 assert!(cleanup_ok, "cleanup deletion failed — check relay for leftover events");
706 eprintln!("[live] OK: {} messages round-tripped and cleaned up", msgs.len());
707 }
708
709 #[tokio::test]
710 async fn redundancy_a_dropped_relay_still_delivers() {
711 let relay = MemoryRelay::new();
714 let community = community();
715 let channel = community.channels[0].clone();
716 let alice = Keys::generate();
717
718 let outer = seal_message_with_ephemeral(
719 &Keys::generate(), &alice, &channel.key, &channel.id, channel.epoch, "survives", 1,
720 )
721 .unwrap();
722 relay.inject(&outer, &["r2".to_string()]); let reader = member_view(&community);
725 let msgs = fetch_channel_messages(&relay, &reader, &reader.channels[0]).await.unwrap();
726 assert_eq!(msgs.len(), 1);
727 assert_eq!(msgs[0].content, "survives");
728 }
729}