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