| 1 | //! What the Live Share tests share: a relay, a host and guests on a notebook of two sections. |
| 2 | #![allow(dead_code)] |
| 3 | |
| 4 | use notebook::{ |
| 5 | EditStatus, Replica, |
| 6 | live::{ |
| 7 | Hello, |
| 8 | share::{self, Guest, Host, Sharing}, |
| 9 | }, |
| 10 | session::{Notebook, Section}, |
| 11 | }; |
| 12 | use onestore::{ |
| 13 | ExGuid, |
| 14 | op::{Edit, Op, PageOp}, |
| 15 | protected::{Key, rekey}, |
| 16 | }; |
| 17 | use std::{ |
| 18 | net::TcpListener, |
| 19 | path::Path, |
| 20 | sync::Arc, |
| 21 | thread, |
| 22 | time::{Duration, Instant}, |
| 23 | }; |
| 24 | |
| 25 | #[path = "server.rs"] |
| 26 | pub mod server; |
| 27 | |
| 28 | pub const PASSWORD: &str = "fixture password"; |
| 29 | |
| 30 | /// A relay on this computer with `config`'s limits: its URL. |
| 31 | pub fn relay(config: relay::server::Config) -> String { |
| 32 | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); |
| 33 | let url = format!("ws://{}", listener.local_addr().unwrap()); |
| 34 | thread::spawn(move || relay::server::serve(listener, config)); |
| 35 | url |
| 36 | } |
| 37 | |
| 38 | /// Another secret than `secret`, as a guess makes one. |
| 39 | pub fn mistaken(secret: &str) -> String { |
| 40 | let first = if secret.starts_with('A') { 'B' } else { 'A' }; |
| 41 | format!("{first}{}", &secret[1..]) |
| 42 | } |
| 43 | |
| 44 | pub fn hello(name: &str) -> Hello { |
| 45 | Hello::new(name.into(), None).unwrap() |
| 46 | } |
| 47 | |
| 48 | pub fn until(what: &str, done: impl Fn() -> bool) { |
| 49 | let deadline = Instant::now() + Duration::from_secs(30); |
| 50 | while !done() { |
| 51 | assert!(Instant::now() < deadline, "{what}"); |
| 52 | thread::sleep(Duration::from_millis(20)); |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | /// A notebook folder holding `Garden.one`, a page reading "Original text", and a protected |
| 57 | /// `Sealed.one` reading "Sealed text". |
| 58 | pub fn notebook(root: &Path) -> std::path::PathBuf { |
| 59 | let folder = root.join("Garden"); |
| 60 | std::fs::create_dir_all(&folder).unwrap(); |
| 61 | std::fs::write( |
| 62 | folder.join("Garden.one"), |
| 63 | onestore::create_section("Garden.one", "Original text", "Fixture").unwrap(), |
| 64 | ) |
| 65 | .unwrap(); |
| 66 | let plain = onestore::create_section("Sealed.one", "Sealed text", "Fixture").unwrap(); |
| 67 | let key = Key::new(PASSWORD).unwrap(); |
| 68 | std::fs::write( |
| 69 | folder.join("Sealed.one"), |
| 70 | rekey(&plain, None, Some(&key)).unwrap(), |
| 71 | ) |
| 72 | .unwrap(); |
| 73 | folder |
| 74 | } |
| 75 | |
| 76 | pub fn host(folder: &Path, cache: &Path, sharing: &Sharing, url: &str) -> Host { |
| 77 | let storage = Notebook::open(folder, cache).unwrap().into_storage(); |
| 78 | Host::start( |
| 79 | storage, |
| 80 | hello("Ada"), |
| 81 | sharing.clone(), |
| 82 | "Garden", |
| 83 | None, |
| 84 | Some(url), |
| 85 | || {}, |
| 86 | |_| Ok(()), |
| 87 | ) |
| 88 | .unwrap() |
| 89 | } |
| 90 | |
| 91 | /// The host's code once the relay has numbered it. |
| 92 | pub fn code(host: &Host) -> String { |
| 93 | until("the code was never numbered", || { |
| 94 | host.code().is_some_and(|code| share::code(&code).is_some()) |
| 95 | }); |
| 96 | host.code().unwrap() |
| 97 | } |
| 98 | |
| 99 | /// `name` joins with `code` and opens the notebook in `cache`. |
| 100 | pub fn guest(name: &str, code: &str, url: &str, cache: &Path) -> (Arc<Guest>, Notebook) { |
| 101 | let welcome = share::join(hello(name), code, "", None, Some(url)).unwrap(); |
| 102 | assert_eq!( |
| 103 | (welcome.notebook.as_str(), welcome.host.as_str()), |
| 104 | ("Garden", "Ada") |
| 105 | ); |
| 106 | let guest = Guest::start( |
| 107 | hello(name), |
| 108 | welcome.share, |
| 109 | welcome.secret, |
| 110 | None, |
| 111 | Some(url), |
| 112 | || {}, |
| 113 | ) |
| 114 | .unwrap(); |
| 115 | until("the host was never met", || guest.host().is_some()); |
| 116 | let notebook = Notebook::open_hosted(Arc::clone(&guest), cache).unwrap(); |
| 117 | (guest, notebook) |
| 118 | } |
| 119 | |
| 120 | pub fn open(notebook: &Notebook, guest: &Arc<Guest>, path: &str, key: Option<&Key>) -> Section { |
| 121 | let replica = notebook.replica_path(path).unwrap(); |
| 122 | std::fs::create_dir_all(replica.parent().unwrap()).unwrap(); |
| 123 | let replica = Replica::open_or_create(&replica, key, || notebook.read_section(path)).unwrap(); |
| 124 | Section::resume_hosted(path.into(), replica, Arc::clone(guest), || {}).unwrap() |
| 125 | } |
| 126 | |
| 127 | pub fn replace(section: &Section, image: &[u8], range: std::ops::Range<u32>, with: &str) -> u64 { |
| 128 | let (space, text, _) = server::text(image); |
| 129 | replaced(section, space, text, range, with) |
| 130 | } |
| 131 | |
| 132 | pub fn replaced( |
| 133 | section: &Section, |
| 134 | space: ExGuid, |
| 135 | text: ExGuid, |
| 136 | range: std::ops::Range<u32>, |
| 137 | with: &str, |
| 138 | ) -> u64 { |
| 139 | let op = PageOp::Text { |
| 140 | text, |
| 141 | range, |
| 142 | with: with.into(), |
| 143 | }; |
| 144 | let edit = Edit { |
| 145 | at: 134_000_000_000_000_000, |
| 146 | ops: vec![Op::Page { space, op }], |
| 147 | }; |
| 148 | section.replica().apply("Guest", edit).unwrap() |
| 149 | } |
| 150 | |
| 151 | pub fn published(section: &Section, id: u64) { |
| 152 | until("the edit was never published", || { |
| 153 | matches!( |
| 154 | section.status(id).unwrap(), |
| 155 | Some(EditStatus::Published { .. }) |
| 156 | ) |
| 157 | }); |
| 158 | } |