1//! Live Share: guests open a notebook a host serves through a relay, edit it through the
2//! replica and queue they use on a share, queue while the host is away, conflict as on a
3//! share, pass protected sections through as ciphertext, and are refused with a wrong code.
4#![cfg(feature = "live")]
5
6use notebook::{
7 live::share::{self, Refusal, Sharing},
8 session::{Notebook, SyncState},
9};
10use onestore::Arena;
11use std::sync::Arc;
12
13#[path = "support/live.rs"]
14mod live;
15use live::*;
16
17/// A guest opens a section through the host, its edit lands in the host's file, and the
18/// host's own edit reaches the guest.
19#[test]
20fn a_guest_edits_the_host_s_notebook() {
21 let directory = tempfile::tempdir().unwrap();
22 let folder = notebook(directory.path());
23 let url = relay(Default::default());
24 let sharing = Sharing::new("").unwrap();
25 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
26 let (guest, notebook) = guest("Grace", &code(&host), &url, &directory.path().join("grace"));
27 let paths: Vec<String> = notebook
28 .catalog()
29 .sections
30 .iter()
31 .map(|section| section.path.clone())
32 .collect();
33 assert_eq!(paths, ["Garden.one", "Sealed.one"]);
34 let section = open(&notebook, &guest, "Garden.one", None);
35 let file = folder.join("Garden.one");
36 let id = replace(&section, &std::fs::read(&file).unwrap(), 0..8, "Grace's");
37 published(&section, id);
38 assert_eq!(
39 server::text(&std::fs::read(&file).unwrap()).2,
40 "Grace's text"
41 );
42 assert_eq!(host.guests().len(), 1);
43
44 // The host's own edit, as its app commits one.
45 let image = std::fs::read(&file).unwrap();
46 let (space, text, _) = server::text(&image);
47 std::fs::write(&file, server::typed(&image, space, text, 0..7, "Ada's")).unwrap();
48 host.touched(&["Garden.one".into()]);
49 until("the guest never saw the host's edit", || {
50 section
51 .page(space)
52 .is_ok_and(|page| server::page_texts(&page).contains(&"Ada's text".to_owned()))
53 });
54}
55
56#[test]
57fn an_offline_guest_reopens_its_queue_while_the_host_remains_reachable() {
58 use notebook::{Remote, Replica, live::share::HostedRemote, session::Section};
59 use std::{thread, time::Duration};
60
61 let directory = tempfile::tempdir().unwrap();
62 let folder = notebook(directory.path());
63 let url = relay(Default::default());
64 let sharing = Sharing::new("").unwrap();
65 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
66 let (guest, notebook) = guest("Grace", &code(&host), &url, &directory.path().join("grace"));
67 let file = folder.join("Garden.one");
68 let source = std::fs::read(&file).unwrap();
69 let cache = notebook.replica_path("Garden.one").unwrap();
70 std::fs::create_dir_all(cache.parent().unwrap()).unwrap();
71 let replica =
72 Replica::open_or_create(&cache, None, || notebook.read_section("Garden.one")).unwrap();
73 let section = Section::resume_hosted(
74 "Garden.one".into(),
75 replica,
76 Arc::clone(&guest),
77 true,
78 || {},
79 )
80 .unwrap();
81 let id = replace(&section, &source, 0..0, "Hosted offline reopen marker ");
82 let pending = section.pending().unwrap();
83 assert_eq!(pending.len(), 1);
84 section.close().unwrap();
85 let section = Section::resume_hosted(
86 "Garden.one".into(),
87 Replica::open(&cache).unwrap(),
88 Arc::clone(&guest),
89 true,
90 || {},
91 )
92 .unwrap();
93 let mut probe = HostedRemote::new(&guest, "Garden.one");
94 let stamp = onestore::Stamp::of(&source).unwrap();
95 for _ in 0..10 {
96 assert_eq!(probe.stamp().unwrap(), stamp);
97 assert_eq!(section.pending().unwrap(), pending);
98 assert_eq!(std::fs::read(&file).unwrap(), source);
99 thread::sleep(Duration::from_millis(20));
100 }
101 section.wake();
102 published(&section, id);
103 assert_eq!(
104 server::text(&std::fs::read(&file).unwrap()).2,
105 "Hosted offline reopen marker Original text"
106 );
107 assert!(section.pending().unwrap().is_empty());
108 section.close().unwrap();
109 host.stop();
110}
111
112#[test]
113fn presence_reconnect_reconciles_missed_changes_without_reconnecting_access() {
114 use notebook::{
115 Remote,
116 live::{
117 Caret, Presence, Spot,
118 share::{Guest, HostedRemote},
119 },
120 session::Background,
121 };
122 use onestore::Stamp;
123 use std::sync::atomic::{AtomicUsize, Ordering};
124
125 let directory = tempfile::tempdir().unwrap();
126 let folder = notebook(directory.path());
127 let url = relay(Default::default());
128 let sharing = Sharing::new("").unwrap();
129 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
130 let welcome = share::join(hello("Grace"), &code(&host), "", None, Some(&url)).unwrap();
131 let relay = PresenceRelay::new(&url, &welcome.room);
132 let guest = Guest::start(
133 hello("Grace"),
134 welcome.share,
135 welcome.secret,
136 None,
137 Some(&relay.url),
138 || {},
139 )
140 .unwrap();
141 until("the access host was never met", || guest.host().is_some());
142 let host_id = guest.host().unwrap().peer;
143 until("the presence host was never met", || {
144 guest.peers().iter().any(|peer| peer.hello.peer == host_id)
145 });
146 let mut notebook =
147 Notebook::open_hosted(Arc::clone(&guest), directory.path().join("grace")).unwrap();
148 let section = open(&notebook, &guest, "Garden.one", None);
149 let background = Background::hosted(Arc::clone(&guest), || {}).unwrap();
150 background.watch(notebook.replicas());
151 background.hold("Garden.one", &section);
152 let storage = notebook.into_storage();
153 until("the section never synced", || {
154 section.sync_status().unwrap().synced.is_some()
155 });
156 let touched = Arc::new(AtomicUsize::new(0));
157 let heard = Arc::clone(&touched);
158 let observed = Arc::clone(&guest);
159 background.on_touched(Some(Box::new(move |_| {
160 observed.set_presence(Presence::default());
161 HostedRemote::new(&observed, "Garden.one").stamp().unwrap();
162 heard.fetch_add(1, Ordering::SeqCst);
163 })));
164 let file = folder.join("Garden.one");
165 let image = std::fs::read(&file).unwrap();
166 let (space, text, _) = server::text(&image);
167 let stamp = Stamp::of(&image).unwrap();
168 until("the initial TOUCHED never arrived", || {
169 host.touched(&["Garden.one".into()]);
170 touched.load(Ordering::SeqCst) > 0
171 });
172 let mut remote = HostedRemote::new(&guest, "Garden.one");
173 until("TOUCHED did not cache the current stamp", || {
174 let before = relay.state.lock().unwrap().access_bytes;
175 assert_eq!(
176 HostedRemote::new(&guest, "Garden.one").stamp().unwrap(),
177 stamp
178 );
179 relay.state.lock().unwrap().access_bytes == before
180 });
181 let before = relay.state.lock().unwrap().access_bytes;
182 for at in 1..=8 {
183 let spot = Spot {
184 text: text.into(),
185 offset: at,
186 };
187 let presence = Presence {
188 page: Some(space.into()),
189 caret: Some(Caret {
190 anchor: spot,
191 focus: spot,
192 }),
193 ..Presence::default()
194 };
195 host.set_presence(presence.clone());
196 until("ordinary presence never reached the guest", || {
197 guest
198 .peers()
199 .iter()
200 .any(|peer| peer.hello.peer == host_id && peer.presence.as_ref() == Some(&presence))
201 });
202 assert_eq!(remote.stamp().unwrap(), stamp);
203 }
204 assert_eq!(
205 relay.state.lock().unwrap().access_bytes,
206 before,
207 "typing caused remote stamp or image requests"
208 );
209
210 relay.disconnect_presence();
211 until("the presence host never left", || {
212 !guest.peers().iter().any(|peer| peer.hello.peer == host_id)
213 });
214 assert_eq!(guest.host().unwrap().peer, host_id);
215 let access_connections = relay.state.lock().unwrap().access_connections;
216 assert_eq!(access_connections, 1);
217 std::fs::write(folder.join("reachable.bin"), b"still connected").unwrap();
218 assert_eq!(
219 storage.read_file("reachable.bin", 1024).unwrap(),
220 b"still connected"
221 );
222 let missed = server::typed(&image, space, text, 0..8, "Missed");
223 std::fs::write(&file, &missed).unwrap();
224 let before = touched.load(Ordering::SeqCst);
225 host.touched(&["Garden.one".into()]);
226 std::thread::sleep(std::time::Duration::from_millis(200));
227 assert_eq!(
228 touched.load(Ordering::SeqCst),
229 before,
230 "the isolated guest received TOUCHED"
231 );
232 assert!(
233 server::page_texts(&section.page(space).unwrap()).contains(&"Original text".to_owned())
234 );
235
236 relay.state.lock().unwrap().blocked = false;
237 until("the presence host never returned", || {
238 guest.peers().iter().any(|peer| peer.hello.peer == host_id)
239 });
240 until(
241 "the missed change never reconciled after presence returned",
242 || server::page_texts(&section.page(space).unwrap()).contains(&"Missed text".to_owned()),
243 );
244 assert_eq!(guest.host().unwrap().peer, host_id);
245 assert_eq!(
246 relay.state.lock().unwrap().access_connections,
247 access_connections
248 );
249 assert_eq!(section.replica().snapshot().unwrap(), missed);
250 background.stop();
251}
252
253/// While the host is away a guest's edits wait in its replica, the notebook opens from its
254/// last listing, and once the host is back the edits reach its file.
255#[test]
256fn a_guest_queues_while_the_host_is_away() {
257 let directory = tempfile::tempdir().unwrap();
258 let folder = notebook(directory.path());
259 let url = relay(Default::default());
260 let sharing = Sharing::new("").unwrap();
261 let host_cache = directory.path().join("host");
262 let host = host(&folder, &host_cache, &sharing, &url);
263 let cache = directory.path().join("grace");
264 let (guest, notebook) = guest("Grace", &code(&host), &url, &cache);
265 let section = open(&notebook, &guest, "Garden.one", None);
266 let file = folder.join("Garden.one");
267 let image = std::fs::read(&file).unwrap();
268
269 // Ada's computer goes to sleep.
270 let sharing = host.sharing();
271 drop(host);
272 until("the host never left", || guest.host().is_none());
273 let id = replace(&section, &image, 0..8, "Offline");
274 until("the section never said the host was away", || {
275 section
276 .sync_status()
277 .is_ok_and(|status| status.state() == SyncState::NotConnected && status.queued > 0)
278 });
279 assert_eq!(std::fs::read(&file).unwrap(), image);
280 // The notebook opens from its last listing while the host is away.
281 let reopened = Notebook::open_hosted(Arc::clone(&guest), &cache).unwrap();
282 assert_eq!(reopened.catalog().sections.len(), 2);
283
284 let host = self::host(&folder, &host_cache, &sharing, &url);
285 published(&section, id);
286 assert_eq!(
287 server::text(&std::fs::read(&file).unwrap()).2,
288 "Offline text"
289 );
290 drop(host);
291}
292
293/// Two guests that change the same words while apart: the second to publish gets OneNote's
294/// conflict page, in its replica and in the host's file.
295#[test]
296fn two_guests_on_one_page_conflict_as_on_a_share() {
297 let directory = tempfile::tempdir().unwrap();
298 let folder = notebook(directory.path());
299 let url = relay(Default::default());
300 let sharing = Sharing::new("").unwrap();
301 let host_cache = directory.path().join("host");
302 let host = host(&folder, &host_cache, &sharing, &url);
303 let code = code(&host);
304 let (grace, grace_notebook) = guest("Grace", &code, &url, &directory.path().join("grace"));
305 let (alan, alan_notebook) = guest("Alan", &code, &url, &directory.path().join("alan"));
306 let graces = open(&grace_notebook, &grace, "Garden.one", None);
307 let alans = open(&alan_notebook, &alan, "Garden.one", None);
308 let file = folder.join("Garden.one");
309 let image = std::fs::read(&file).unwrap();
310
311 let sharing = host.sharing();
312 drop(host);
313 until("the host never left", || {
314 grace.host().is_none() && alan.host().is_none()
315 });
316 let first = replace(&graces, &image, 0..8, "Grace's");
317 let second = replace(&alans, &image, 0..8, "Alan's");
318 let host = self::host(&folder, &host_cache, &sharing, &url);
319 published(&graces, first);
320 published(&alans, second);
321 let stored = std::fs::read(&file).unwrap();
322 let conflicts = server::conflicts(&stored);
323 assert_eq!(conflicts.len(), 1, "one page holds a conflict page");
324 let (user, kept) = &conflicts[0].1[0];
325 assert_eq!(user, "Guest");
326 let texts: Vec<String> = server::pages(&stored)
327 .iter()
328 .flat_map(|(_, page)| server::page_texts(page))
329 .chain(kept.iter().cloned())
330 .collect();
331 assert!(
332 texts.contains(&"Grace's text".to_owned()) && texts.contains(&"Alan's text".to_owned()),
333 "{texts:?}"
334 );
335 until("the host's conflict never reached a guest", || {
336 !alans.conflicts().unwrap().is_empty() || !graces.conflicts().unwrap().is_empty()
337 });
338 drop(host);
339}
340
341/// A protected section's guest unlocks it with the password; the host only ever stores
342/// what the guest sealed.
343#[test]
344fn a_protected_section_passes_through_as_ciphertext() {
345 let directory = tempfile::tempdir().unwrap();
346 let folder = notebook(directory.path());
347 let url = relay(Default::default());
348 let sharing = Sharing::new("").unwrap();
349 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
350 let (guest, notebook) = guest("Grace", &code(&host), &url, &directory.path().join("grace"));
351 assert!(notebook.unlock("Sealed.one", "wrong password").is_err());
352 let key = notebook.unlock("Sealed.one", PASSWORD).unwrap();
353 let section = open(&notebook, &guest, "Sealed.one", Some(&key));
354 let file = folder.join("Sealed.one");
355 let arena = Arena::default();
356 let mut unlocked =
357 onestore::Section::unlock(&arena, std::fs::read(&file).unwrap(), &key).unwrap();
358 let (space, ..) = unlocked.pages().unwrap()[0];
359 let page = unlocked.page(space).unwrap();
360 let text = page
361 .objects
362 .iter()
363 .find_map(|object| match object {
364 onestore::page::PageObject::Outline(outline) => outline
365 .paragraphs
366 .iter()
367 .find_map(|p| p.text().map(|t| t.id)),
368 _ => None,
369 })
370 .unwrap();
371 let id = replaced(&section, space, text, 0..6, "Guarded");
372 published(&section, id);
373 let stored = std::fs::read(&file).unwrap();
374 let marker = "Guarded"
375 .encode_utf16()
376 .flat_map(u16::to_le_bytes)
377 .collect::<Vec<u8>>();
378 assert!(
379 !stored.windows(marker.len()).any(|window| window == marker),
380 "the host's file holds the new text in the clear"
381 );
382 assert!(onestore::Section::open(&Arena::default(), stored.clone()).is_err());
383 let arena = Arena::default();
384 let reread = onestore::Section::unlock(&arena, stored, &key).unwrap();
385 let texts = server::page_texts(&reread.page(space).unwrap());
386 assert!(texts.contains(&"Guarded text".to_owned()), "{texts:?}");
387}
388
389/// A wrong code is refused without the guest learning anything; the host's code burns after
390/// too many wrong tries and is replaced by new words; too many wrong codes from one network
391/// lock it out; and a guest can't reach outside the notebook or presence's secret.
392#[test]
393fn wrong_codes_are_refused_and_counted() {
394 let directory = tempfile::tempdir().unwrap();
395 let folder = notebook(directory.path());
396 let url = relay(relay::server::Config {
397 burn_after: 2,
398 failures_per_minute: 3,
399 ..Default::default()
400 });
401 let sharing = Sharing::new("").unwrap();
402 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
403 let code = code(&host);
404 let (number, secret) = notebook::live::code::parse(&code).unwrap();
405 let wrong = notebook::live::code::format(number, &mistaken(&secret)).unwrap();
406 let join = |code: &str| share::join(hello("Mallory"), code, "", None, Some(&url));
407 assert_eq!(join("not a code").unwrap_err(), Refusal::Malformed);
408 // A symbol mistyped fails its check here, spending none of the two tries before a burn.
409 let typo = format!(
410 "{}{}",
411 if code.starts_with('7') { '8' } else { '7' },
412 &code[1..]
413 );
414 assert_eq!(join(&typo).unwrap_err(), Refusal::Malformed);
415 assert_eq!(
416 join(&code.to_lowercase().replace('-', " ")).map(|_| ()),
417 Ok(())
418 );
419 assert_eq!(join(&wrong).unwrap_err(), Refusal::Wrong);
420 assert_eq!(join(&wrong).unwrap_err(), Refusal::Wrong);
421 // The code burned, and the host shares a new secret, under a number of its own.
422 until("the code never changed", || {
423 host.code()
424 .is_some_and(|now| now != code && share::code(&now).is_some())
425 });
426 let fresh = host.code().unwrap();
427 assert_eq!(join(&code).unwrap_err(), Refusal::Expired);
428 let burned = number;
429 let (number, secret) = notebook::live::code::parse(&fresh).unwrap();
430 assert_ne!(number, burned);
431 let wrong = notebook::live::code::format(number, &mistaken(&secret)).unwrap();
432 assert_eq!(join(&wrong).unwrap_err(), Refusal::Wrong);
433 // A third wrong code in a minute locks this network out, even from the right code.
434 assert!(matches!(
435 join(&fresh).unwrap_err(),
436 Refusal::TooMany(Some(_))
437 ));
438
439 // Joined, a guest still can't reach outside the notebook.
440 std::fs::create_dir_all(folder.join(".snowbound")).unwrap();
441 std::fs::write(folder.join(".snowbound/live.json"), b"{}").unwrap();
442 std::fs::write(directory.path().join("secret.txt"), b"secret").unwrap();
443 let url = relay(Default::default());
444 let host = self::host(&folder, &directory.path().join("host"), &sharing, &url);
445 let (_guest, notebook) = guest(
446 "Grace",
447 &self::code(&host),
448 &url,
449 &directory.path().join("g"),
450 );
451 let storage = notebook.into_storage();
452 for path in [
453 "../secret.txt",
454 ".snowbound/live.json",
455 "/etc/hosts",
456 "a//b",
457 ] {
458 let error = storage.read_file(path, 1024).unwrap_err();
459 assert!(
460 error.to_string().contains("Outside the notebook"),
461 "{path}: {error}"
462 );
463 }
464 assert!(storage.rename_root("Elsewhere", &[]).is_err());
465}
466
467/// Files larger than one message go up and come back a chunk at a time, through a relay that
468/// hangs up on a peer with more than its queue waiting.
469#[test]
470fn large_files_travel_in_chunks() {
471 let directory = tempfile::tempdir().unwrap();
472 let folder = notebook(directory.path());
473 let url = relay(relay::server::Config {
474 queue: 400 << 10,
475 ..Default::default()
476 });
477 let sharing = Sharing::new("").unwrap();
478 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
479 let (_guest, notebook) = guest("Grace", &code(&host), &url, &directory.path().join("grace"));
480 let storage = notebook.into_storage();
481 let bytes: Vec<u8> = (0..3_000_000u32).map(|at| (at * 7 % 251) as u8).collect();
482 storage
483 .create("Garden_onefiles/big.bin", &bytes)
484 .unwrap_err();
485 std::fs::create_dir(folder.join("Garden_onefiles")).unwrap();
486 storage.create("Garden_onefiles/big.bin", &bytes).unwrap();
487 assert_eq!(
488 std::fs::read(folder.join("Garden_onefiles/big.bin")).unwrap(),
489 bytes
490 );
491 assert_eq!(
492 storage
493 .read_file("Garden_onefiles/big.bin", 4 << 20)
494 .unwrap(),
495 bytes
496 );
497 assert!(
498 storage
499 .read_file("Garden_onefiles/big.bin", 1 << 20)
500 .is_err()
501 );
502}
503
504#[test]
505fn large_section_appends_keep_the_room_connected_and_guests_converge() {
506 use notebook::{EditStatus, Replica, live::share::HostedRemote};
507 use onestore::op::{Edit, Op, PageOp};
508 use std::time::{Duration, Instant};
509
510 let directory = tempfile::tempdir().unwrap();
511 let folder = notebook(directory.path());
512 let file = folder.join("Garden.one");
513 let mut original = "A".repeat(512 << 10);
514 let image = onestore::create_section("Garden.one", &original, "Fixture").unwrap();
515 std::fs::write(&file, &image).unwrap();
516 let (space, text, _) = server::text(&image);
517 let arena = Arena::default();
518 let mut section = onestore::Section::open(&arena, image).unwrap();
519 section
520 .apply(
521 "Fixture",
522 &Edit {
523 at: 134_000_000_000_000_000,
524 ops: vec![Op::Page {
525 space,
526 op: PageOp::Text {
527 text,
528 range: 0..1,
529 with: "a".into(),
530 },
531 }],
532 },
533 )
534 .unwrap();
535 section.seal().unwrap().unwrap().commit_file(&file).unwrap();
536 original.replace_range(0..1, "a");
537 let url = relay(Default::default());
538 let sharing = Sharing::new("").unwrap();
539 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
540 let code = code(&host);
541 let (alice, alice_notebook) = guest("Alice", &code, &url, &directory.path().join("alice"));
542 let (bob, bob_notebook) = guest("Bob", &code, &url, &directory.path().join("bob"));
543 until("the guests never joined the room", || {
544 host.guests().len() == 2
545 });
546 let image = bob_notebook.read_section("Garden.one").unwrap();
547 assert_eq!(alice_notebook.read_section("Garden.one").unwrap(), image);
548 let (space, text, _) = server::text(&image);
549 let writer = Replica::create(directory.path().join("writer.sqlite"), &image).unwrap();
550 let reader = Replica::create(directory.path().join("reader.sqlite"), &image).unwrap();
551 let append = "B".repeat(192 << 10);
552 let expected = format!("{original}{append}");
553 let id = writer
554 .apply(
555 "Alice",
556 Edit {
557 at: 134_000_000_000_000_000,
558 ops: vec![Op::Page {
559 space,
560 op: PageOp::Text {
561 text,
562 range: original.len() as u32..original.len() as u32,
563 with: append,
564 },
565 }],
566 },
567 )
568 .unwrap();
569 writer
570 .sync_once(&mut HostedRemote::new(&alice, "Garden.one"))
571 .unwrap();
572 let stored = std::fs::read(&file).unwrap();
573 let added = stored.len() - image.len();
574 assert!(added > relay::server::Config::default().max_message);
575 assert!(added + 1024 < stored.len() / 2);
576 assert_eq!(server::text(&stored).2, expected);
577 let Some(EditStatus::Published { revision }) = writer.status(id).unwrap() else {
578 panic!("the append has no durable receipt");
579 };
580 let store = onestore::Store::parse(&stored).unwrap();
581 let index = onestore::RevisionIndex::parse(&store).unwrap();
582 assert!(index.spaces[&space].revisions.contains_key(&revision));
583 assert!(writer.pending().unwrap().is_empty());
584 let mut remote = HostedRemote::new(&bob, "Garden.one");
585 reader.sync_once(&mut remote).unwrap();
586 assert_eq!(reader.snapshot().unwrap(), stored);
587 let deadline = Instant::now() + Duration::from_millis(1200);
588 while Instant::now() < deadline {
589 assert_eq!(host.guests().len(), 2, "the section append broke the room");
590 std::thread::sleep(Duration::from_millis(10));
591 }
592}
593
594/// A guest that floods its host with requests is hung up on once too many wait, having had
595/// answers to few of them, and the host goes on serving the others.
596#[test]
597fn a_flooding_guest_is_hung_up_on() {
598 use notebook::live::{
599 Event, Live, Room,
600 wire::{Bye, Request, kind},
601 };
602 use std::sync::{
603 Mutex,
604 atomic::{AtomicUsize, Ordering},
605 };
606 let directory = tempfile::tempdir().unwrap();
607 let folder = notebook(directory.path());
608 let url = relay(Default::default());
609 let sharing = Sharing::new("").unwrap();
610 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
611 let code = code(&host);
612 let (grace, notebook) = guest("Grace", &code, &url, &directory.path().join("grace"));
613 let welcome = share::join(hello("Mallory"), &code, "", None, Some(&url)).unwrap();
614 let replies = Arc::new(AtomicUsize::new(0));
615 let bye = Arc::new(Mutex::new(None));
616 let (counted, said) = (Arc::clone(&replies), Arc::clone(&bye));
617 let mallory = Live::start(
618 hello("Mallory"),
619 &Room::Notebook(welcome.secret),
620 None,
621 Some(&url),
622 move |event| match event {
623 Event::Frame {
624 kind: kind::REPLY, ..
625 } => {
626 counted.fetch_add(1, Ordering::Relaxed);
627 }
628 Event::Frame {
629 kind: kind::BYE,
630 body,
631 ..
632 } => *said.lock().unwrap() = minicbor::decode::<Bye>(body).ok(),
633 _ => {}
634 },
635 )
636 .unwrap();
637 // The line to the host, while Mallory's stream to it is open.
638 let served = |live: &Live| {
639 let host = live
640 .peers()
641 .into_iter()
642 .find(|peer| peer.hello.serves == Some(welcome.share))?;
643 live.line(&host.hello.peer)
644 };
645 until("Mallory never met the host", || served(&mallory).is_some());
646 let line = served(&mallory).unwrap();
647 const SENT: u64 = 5000;
648 for id in 0..SENT {
649 let request = Request {
650 id,
651 path: "Garden.one".into(),
652 ..Request::default()
653 };
654 if line.send(kind::STAMP, &request).is_err() {
655 break;
656 }
657 }
658 until("the host never hung up on Mallory", || {
659 bye.lock().unwrap().is_some() && served(&mallory).is_none()
660 });
661 assert_eq!(bye.lock().unwrap().as_ref().unwrap().reason, "flooded");
662 let answered = replies.load(Ordering::Relaxed);
663 // At most the burst a guest may start at once, what waits for the workers, and what the
664 // rate refills while the flood arrives.
665 assert!(answered < 300, "Mallory had {answered} answers of {SENT}");
666 // Grace, asking at her own pace, is served as before.
667 assert!(grace.host().is_some());
668 assert_eq!(
669 notebook.read_section("Garden.one").unwrap(),
670 std::fs::read(folder.join("Garden.one")).unwrap()
671 );
672}
673
674/// Stopping a share lets every guest go, saying so, and leaves its code and secret for no one:
675/// the code no longer opens anything, and sharing again makes new ones.
676#[test]
677fn stopping_lets_every_guest_go_and_retires_the_code() {
678 let directory = tempfile::tempdir().unwrap();
679 let folder = notebook(directory.path());
680 let url = relay(Default::default());
681 let sharing = Sharing::new("").unwrap();
682 let host = host(&folder, &directory.path().join("host"), &sharing, &url);
683 let code = code(&host);
684 let (guest, _notebook) = guest("Grace", &code, &url, &directory.path().join("grace"));
685 host.stop();
686 until("the guest never heard the host stop", || {
687 guest.ended() == Some(share::Ended::Stopped) && guest.host().is_none()
688 });
689 assert!(host.code().is_none() && host.guests().is_empty());
690 assert!(matches!(
691 share::join(hello("Alan"), &code, "", None, Some(&url)).unwrap_err(),
692 Refusal::NoOne | Refusal::TimedOut
693 ));
694 let again = Sharing::new("").unwrap();
695 assert!(again.secret != sharing.secret && again.share != sharing.share);
696 assert_ne!(again.code, sharing.code);
697}