| ... | @@ -1738,3 +1738,586 @@ fn detached_editor_keeps_selection_and_undo_when_reattachment_fails() { | ... | @@ -1738,3 +1738,586 @@ fn detached_editor_keeps_selection_and_undo_when_reattachment_fails() { |
| 1738 | assert!(sb_view_undo(&mut view, true)); | 1738 | assert!(sb_view_undo(&mut view, true)); |
| 1739 | assert_eq!(view.canvas.active().shown_text(), text); | 1739 | assert_eq!(view.canvas.active().shown_text(), text); |
| 1740 | } | 1740 | } |
| | 1741 | |
| | 1742 | struct LiveRelay(std::process::Child); |
| | 1743 | |
| | 1744 | impl LiveRelay { |
| | 1745 | fn start(test: &str, root: &Path) -> (Self, String) { |
| | 1746 | let receipt = root.join("relay-address"); |
| | 1747 | let child = std::process::Command::new(std::env::current_exe().unwrap()) |
| | 1748 | .args(["--exact", test, "--nocapture"]) |
| | 1749 | .env("SNOWBOUND_MOBILE_TEST_RELAY", &receipt) |
| | 1750 | .stdout(std::process::Stdio::null()) |
| | 1751 | .spawn() |
| | 1752 | .unwrap(); |
| | 1753 | let mut relay = Self(child); |
| | 1754 | assert!(eventually(|| { |
| | 1755 | assert!(relay.0.try_wait().unwrap().is_none()); |
| | 1756 | receipt.exists() |
| | 1757 | })); |
| | 1758 | (relay, std::fs::read_to_string(receipt).unwrap()) |
| | 1759 | } |
| | 1760 | } |
| | 1761 | |
| | 1762 | impl Drop for LiveRelay { |
| | 1763 | fn drop(&mut self) { |
| | 1764 | let _ = self.0.kill(); |
| | 1765 | let _ = self.0.wait(); |
| | 1766 | } |
| | 1767 | } |
| | 1768 | |
| | 1769 | fn live_relay_child() { |
| | 1770 | if let Some(receipt) = std::env::var_os("SNOWBOUND_MOBILE_TEST_RELAY") { |
| | 1771 | std::thread::spawn(|| { |
| | 1772 | std::thread::sleep(Duration::from_secs(180)); |
| | 1773 | std::process::exit(124); |
| | 1774 | }); |
| | 1775 | let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); |
| | 1776 | let receipt = PathBuf::from(receipt); |
| | 1777 | let partial = receipt.with_extension("partial"); |
| | 1778 | std::fs::write(&partial, format!("ws://{}", listener.local_addr().unwrap())).unwrap(); |
| | 1779 | std::fs::rename(partial, receipt).unwrap(); |
| | 1780 | relay::server::serve(listener, Default::default()).unwrap(); |
| | 1781 | } |
| | 1782 | } |
| | 1783 | |
| | 1784 | fn live_host( |
| | 1785 | root: &Path, |
| | 1786 | cache: &Path, |
| | 1787 | sharing: notebook::live::share::Sharing, |
| | 1788 | relay: &str, |
| | 1789 | ) -> notebook::live::share::Host { |
| | 1790 | let kept = cache.join("sharing.json"); |
| | 1791 | let notebook = notebook::session::Notebook::open(root, cache).unwrap(); |
| | 1792 | notebook::live::share::Host::start( |
| | 1793 | notebook.into_storage(), |
| | 1794 | notebook::live::Hello::new("Host".into(), None).unwrap(), |
| | 1795 | sharing, |
| | 1796 | "Mobile fixture", |
| | 1797 | None, |
| | 1798 | Some(relay), |
| | 1799 | || {}, |
| | 1800 | move |sharing| keep_live_fixture(&kept, sharing), |
| | 1801 | ) |
| | 1802 | .unwrap() |
| | 1803 | } |
| | 1804 | |
| | 1805 | fn keep_live_fixture(path: &Path, value: &impl serde::Serialize) -> std::io::Result<()> { |
| | 1806 | use std::io::Write; |
| | 1807 | let mut options = std::fs::OpenOptions::new(); |
| | 1808 | options.write(true).create(true).truncate(true); |
| | 1809 | #[cfg(unix)] |
| | 1810 | std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600); |
| | 1811 | let partial = path.with_extension("partial"); |
| | 1812 | let mut output = options.open(&partial)?; |
| | 1813 | output.write_all(&serde_json::to_vec(value)?)?; |
| | 1814 | output.sync_all()?; |
| | 1815 | std::fs::rename(partial, path)?; |
| | 1816 | #[cfg(unix)] |
| | 1817 | std::fs::File::open(path.parent().unwrap())?.sync_all()?; |
| | 1818 | Ok(()) |
| | 1819 | } |
| | 1820 | |
| | 1821 | struct JoinControl { |
| | 1822 | deadline: Instant, |
| | 1823 | cancelled: AtomicBool, |
| | 1824 | approving: AtomicBool, |
| | 1825 | } |
| | 1826 | |
| | 1827 | impl JoinControl { |
| | 1828 | fn new() -> Self { |
| | 1829 | Self { |
| | 1830 | deadline: Instant::now() + Duration::from_secs(30), |
| | 1831 | cancelled: AtomicBool::new(false), |
| | 1832 | approving: AtomicBool::new(false), |
| | 1833 | } |
| | 1834 | } |
| | 1835 | } |
| | 1836 | |
| | 1837 | extern "C" fn live_join_wait(token: usize, approving: bool) -> bool { |
| | 1838 | // SAFETY: join_live keeps its borrowed JoinControl alive until sb_live_join returns. |
| | 1839 | let control = unsafe { &*(token as *const JoinControl) }; |
| | 1840 | control.approving.store(approving, Ordering::Release); |
| | 1841 | !control.cancelled.load(Ordering::Acquire) && Instant::now() < control.deadline |
| | 1842 | } |
| | 1843 | |
| | 1844 | fn take_json(raw: *mut c_char) -> serde_json::Value { |
| | 1845 | assert!(!raw.is_null()); |
| | 1846 | let text = unsafe { CString::from_raw(raw) }; |
| | 1847 | serde_json::from_slice(text.as_bytes()).unwrap() |
| | 1848 | } |
| | 1849 | |
| | 1850 | fn join_live(code: &str, password: &str, relay: &str, control: &JoinControl) -> serde_json::Value { |
| | 1851 | let code = CString::new(code).unwrap(); |
| | 1852 | let password = CString::new(password).unwrap(); |
| | 1853 | let relay = CString::new(relay).unwrap(); |
| | 1854 | take_json(unsafe { |
| | 1855 | library::sb_live_join( |
| | 1856 | c"Mobile guest".as_ptr(), |
| | 1857 | c"Owned test device".as_ptr(), |
| | 1858 | code.as_ptr(), |
| | 1859 | password.as_ptr(), |
| | 1860 | relay.as_ptr(), |
| | 1861 | false, |
| | 1862 | live_join_wait, |
| | 1863 | control as *const JoinControl as usize, |
| | 1864 | ) |
| | 1865 | }) |
| | 1866 | } |
| | 1867 | |
| | 1868 | fn hosted_c( |
| | 1869 | credential: &serde_json::Value, |
| | 1870 | relay: &str, |
| | 1871 | offline: bool, |
| | 1872 | cache: &Path, |
| | 1873 | ) -> Arc<Library> { |
| | 1874 | let credential = CString::new(serde_json::to_vec(credential).unwrap()).unwrap(); |
| | 1875 | let relay = CString::new(relay).unwrap(); |
| | 1876 | let cache = CString::new(cache.to_str().unwrap()).unwrap(); |
| | 1877 | let mut error = std::ptr::null_mut(); |
| | 1878 | let raw = unsafe { |
| | 1879 | library::sb_library_hosted( |
| | 1880 | credential.as_ptr(), |
| | 1881 | c"Mobile guest".as_ptr(), |
| | 1882 | c"Owned test device".as_ptr(), |
| | 1883 | relay.as_ptr(), |
| | 1884 | false, |
| | 1885 | offline, |
| | 1886 | cache.as_ptr(), |
| | 1887 | &mut error, |
| | 1888 | ) |
| | 1889 | }; |
| | 1890 | if !error.is_null() { |
| | 1891 | let error = unsafe { CString::from_raw(error) }; |
| | 1892 | panic!("hosted open: {}", error.to_string_lossy()); |
| | 1893 | } |
| | 1894 | assert!(!raw.is_null()); |
| | 1895 | // SAFETY: sb_library_hosted hands the caller one owned Arc reference. |
| | 1896 | unsafe { Arc::from_raw(raw) } |
| | 1897 | } |
| | 1898 | |
| | 1899 | #[test] |
| | 1900 | fn live_join_cancels_approval_and_returns_private_per_device_credentials() { |
| | 1901 | live_relay_child(); |
| | 1902 | let directory = tempfile::tempdir().unwrap(); |
| | 1903 | let (_relay, url) = LiveRelay::start( |
| | 1904 | "tests::live_join_cancels_approval_and_returns_private_per_device_credentials", |
| | 1905 | directory.path(), |
| | 1906 | ); |
| | 1907 | let root = directory.path().join("notebook"); |
| | 1908 | let cache = directory.path().join("host-cache"); |
| | 1909 | let creation = onestore::PageCreation::new(None, Some("Mobile fixture"), "Fixture").unwrap(); |
| | 1910 | drop(notebook::session::Notebook::create(&root, &cache, 0, &creation).unwrap()); |
| | 1911 | let mut sharing = notebook::live::share::Sharing::new("join password").unwrap(); |
| | 1912 | sharing.approve = true; |
| | 1913 | let host = live_host(&root, &cache, sharing, &url); |
| | 1914 | assert!(eventually(|| host.code().is_some())); |
| | 1915 | let code = host.code().unwrap(); |
| | 1916 | assert_eq!( |
| | 1917 | join_live("malformed", "", &url, &JoinControl::new())["refusal"], |
| | 1918 | "malformed" |
| | 1919 | ); |
| | 1920 | assert_eq!( |
| | 1921 | join_live(&code, "wrong password", &url, &JoinControl::new())["refusal"], |
| | 1922 | "wrong" |
| | 1923 | ); |
| | 1924 | assert!(host.devices().is_empty()); |
| | 1925 | for decision in ["cancel", "decline", "allow"] { |
| | 1926 | let control = JoinControl::new(); |
| | 1927 | let reply = std::thread::scope(|scope| { |
| | 1928 | let joined = scope.spawn(|| join_live(&code, "join password", &url, &control)); |
| | 1929 | assert!(eventually( |
| | 1930 | || control.approving.load(Ordering::Acquire) && host.requests().len() == 1 |
| | 1931 | )); |
| | 1932 | match decision { |
| | 1933 | "cancel" => control.cancelled.store(true, Ordering::Release), |
| | 1934 | "decline" => host.decline(&host.requests()[0].peer), |
| | 1935 | "allow" => host.allow(&host.requests()[0].peer).unwrap(), |
| | 1936 | _ => unreachable!(), |
| | 1937 | } |
| | 1938 | joined.join().unwrap() |
| | 1939 | }); |
| | 1940 | match decision { |
| | 1941 | "allow" => { |
| | 1942 | let credential = &reply["credential"]; |
| | 1943 | assert!(credential.is_object()); |
| | 1944 | let persisted: notebook::live::share::Sharing = |
| | 1945 | serde_json::from_slice(&std::fs::read(cache.join("sharing.json")).unwrap()) |
| | 1946 | .unwrap(); |
| | 1947 | assert!(credential["secret"] == serde_json::json!(persisted.members[0].secret)); |
| | 1948 | assert!(credential["secret"] != serde_json::json!(persisted.secret)); |
| | 1949 | assert!(credential.get("room").is_none()); |
| | 1950 | assert!(credential.get("code").is_none()); |
| | 1951 | assert!(credential.get("password").is_none()); |
| | 1952 | assert_eq!(credential["protocol"], notebook::live::wire::VERSION); |
| | 1953 | let kept = directory.path().join("joined.json"); |
| | 1954 | keep_live_fixture(&kept, credential).unwrap(); |
| | 1955 | let credential: serde_json::Value = |
| | 1956 | serde_json::from_slice(&std::fs::read(kept).unwrap()).unwrap(); |
| | 1957 | let library = hosted_c( |
| | 1958 | &credential, |
| | 1959 | &url, |
| | 1960 | false, |
| | 1961 | &directory.path().join("guest-cache"), |
| | 1962 | ); |
| | 1963 | assert!(eventually(|| take_json(library::sb_library_live_status( |
| | 1964 | &library |
| | 1965 | ))["state"] |
| | 1966 | == "connected")); |
| | 1967 | assert_eq!(library.tabs().unwrap().len(), 1); |
| | 1968 | } |
| | 1969 | _ => { |
| | 1970 | assert_eq!( |
| | 1971 | reply["refusal"], |
| | 1972 | if decision == "cancel" { |
| | 1973 | "cancelled" |
| | 1974 | } else { |
| | 1975 | "declined" |
| | 1976 | } |
| | 1977 | ); |
| | 1978 | assert!(reply.get("credential").is_none()); |
| | 1979 | assert!(host.devices().is_empty()); |
| | 1980 | assert!(eventually(|| host.requests().is_empty())); |
| | 1981 | } |
| | 1982 | } |
| | 1983 | } |
| | 1984 | } |
| | 1985 | |
| | 1986 | #[test] |
| | 1987 | fn live_guests_reopen_original_offline_edits_search_and_resume_closed_sections() { |
| | 1988 | live_relay_child(); |
| | 1989 | let directory = tempfile::tempdir().unwrap(); |
| | 1990 | let (_relay, url) = LiveRelay::start( |
| | 1991 | "tests::live_guests_reopen_original_offline_edits_search_and_resume_closed_sections", |
| | 1992 | directory.path(), |
| | 1993 | ); |
| | 1994 | let root = directory.path().join("notebook"); |
| | 1995 | std::fs::create_dir_all(&root).unwrap(); |
| | 1996 | for (file, text) in [ |
| | 1997 | ("Read.one", "Original mobile text"), |
| | 1998 | ("Other.one", "Closed section text"), |
| | 1999 | ] { |
| | 2000 | std::fs::write( |
| | 2001 | root.join(file), |
| | 2002 | onestore::create_section(file, text, "Fixture").unwrap(), |
| | 2003 | ) |
| | 2004 | .unwrap(); |
| | 2005 | } |
| | 2006 | let sealed = onestore::create_section("Sealed.one", "Protected fixture", "Fixture").unwrap(); |
| | 2007 | std::fs::write( |
| | 2008 | root.join("Sealed.one"), |
| | 2009 | onestore::protected::rekey( |
| | 2010 | &sealed, |
| | 2011 | None, |
| | 2012 | Some(&onestore::protected::Key::new("section password").unwrap()), |
| | 2013 | ) |
| | 2014 | .unwrap(), |
| | 2015 | ) |
| | 2016 | .unwrap(); |
| | 2017 | let sealed = std::fs::read(root.join("Sealed.one")).unwrap(); |
| | 2018 | let host_cache = directory.path().join("host-cache"); |
| | 2019 | let host = live_host( |
| | 2020 | &root, |
| | 2021 | &host_cache, |
| | 2022 | notebook::live::share::Sharing::new("").unwrap(), |
| | 2023 | &url, |
| | 2024 | ); |
| | 2025 | assert!(eventually(|| host.code().is_some())); |
| | 2026 | let joined = join_live(&host.code().unwrap(), "", &url, &JoinControl::new()); |
| | 2027 | let kept = directory.path().join("joined.json"); |
| | 2028 | assert!(joined["credential"].is_object()); |
| | 2029 | keep_live_fixture(&kept, &joined["credential"]).unwrap(); |
| | 2030 | let credential: serde_json::Value = |
| | 2031 | serde_json::from_slice(&std::fs::read(&kept).unwrap()).unwrap(); |
| | 2032 | let credential = &credential; |
| | 2033 | let cache = directory.path().join("guest-cache"); |
| | 2034 | let library = hosted_c(credential, &url, false, &cache); |
| | 2035 | let tabs = library.tabs().unwrap(); |
| | 2036 | assert!( |
| | 2037 | tabs.iter() |
| | 2038 | .any(|tab| tab.path == "Sealed.one" && tab.locked && !tab.readable) |
| | 2039 | ); |
| | 2040 | let share: [u8; 16] = serde_json::from_value(credential["share"].clone()).unwrap(); |
| | 2041 | let copies = notebook::location::folder(&cache, &notebook::live::share::location(&share)); |
| | 2042 | let replicas = || { |
| | 2043 | let mut paths: Vec<_> = std::fs::read_dir(&copies) |
| | 2044 | .unwrap() |
| | 2045 | .map(|entry| entry.unwrap().path()) |
| | 2046 | .filter(|path| { |
| | 2047 | path.extension() |
| | 2048 | .is_some_and(|extension| extension == "sqlite") |
| | 2049 | }) |
| | 2050 | .collect(); |
| | 2051 | paths.sort(); |
| | 2052 | paths |
| | 2053 | }; |
| | 2054 | assert!(eventually(|| copies.exists() && replicas().len() == 2)); |
| | 2055 | let original_replicas = replicas(); |
| | 2056 | assert!(eventually(|| library |
| | 2057 | .sync_status() |
| | 2058 | .iter() |
| | 2059 | .filter(|sync| sync.state == SyncState::UpToDate as u8) |
| | 2060 | .count() |
| | 2061 | == 2)); |
| | 2062 | let sharing = host.sharing(); |
| | 2063 | drop(host); |
| | 2064 | assert!(eventually(|| take_json(library::sb_library_live_status( |
| | 2065 | &library |
| | 2066 | ))["state"] |
| | 2067 | != "connected")); |
| | 2068 | sb_library_set_offline(&library, true); |
| | 2069 | let section = open(&library, "Read.one"); |
| | 2070 | let space = section.shared.section.pages().unwrap()[0].0; |
| | 2071 | let page = section.shared.section.page(space).unwrap(); |
| | 2072 | let text = page |
| | 2073 | .objects |
| | 2074 | .iter() |
| | 2075 | .find_map(|object| match object { |
| | 2076 | onestore::page::PageObject::Outline(outline) => { |
| | 2077 | outline.paragraphs.iter().find_map(|paragraph| { |
| | 2078 | paragraph |
| | 2079 | .text() |
| | 2080 | .filter(|text| text.text.text().contains("Original mobile text")) |
| | 2081 | .map(|text| text.id) |
| | 2082 | }) |
| | 2083 | } |
| | 2084 | _ => None, |
| | 2085 | }) |
| | 2086 | .unwrap(); |
| | 2087 | section |
| | 2088 | .shared |
| | 2089 | .apply(Edit { |
| | 2090 | at: library::filetime(), |
| | 2091 | ops: vec![Op::Page { |
| | 2092 | space, |
| | 2093 | op: PageOp::Text { |
| | 2094 | text, |
| | 2095 | range: 0..0, |
| | 2096 | with: "OFFLINE_GUEST_TOKEN ".into(), |
| | 2097 | }, |
| | 2098 | }], |
| | 2099 | }) |
| | 2100 | .unwrap(); |
| | 2101 | section.shared.section.written().unwrap(); |
| | 2102 | let pending = section.shared.section.pending().unwrap(); |
| | 2103 | assert_eq!(pending.len(), 1); |
| | 2104 | let identity = section.shared.section.replica().identity().unwrap(); |
| | 2105 | let shared = Arc::clone(&section.shared); |
| | 2106 | drop((section, library)); |
| | 2107 | Arc::into_inner(shared).unwrap().section.close().unwrap(); |
| | 2108 | |
| | 2109 | let reopened: serde_json::Value = |
| | 2110 | serde_json::from_slice(&std::fs::read(&kept).unwrap()).unwrap(); |
| | 2111 | let library = hosted_c(&reopened, &url, true, &cache); |
| | 2112 | assert!( |
| | 2113 | library |
| | 2114 | .tabs() |
| | 2115 | .unwrap() |
| | 2116 | .iter() |
| | 2117 | .any(|tab| tab.path == "Read.one" && tab.readable) |
| | 2118 | ); |
| | 2119 | assert_eq!(replicas(), original_replicas); |
| | 2120 | let section = open(&library, "Read.one"); |
| | 2121 | assert_eq!( |
| | 2122 | section.shared.section.replica().identity().unwrap(), |
| | 2123 | identity |
| | 2124 | ); |
| | 2125 | assert_eq!(section.shared.section.pending().unwrap(), pending); |
| | 2126 | assert!( |
| | 2127 | canvas::search::page_text(&section.shared.section.page(space).unwrap()) |
| | 2128 | .contains("OFFLINE_GUEST_TOKEN") |
| | 2129 | ); |
| | 2130 | let shared = Arc::clone(&section.shared); |
| | 2131 | drop(section); |
| | 2132 | Arc::into_inner(shared).unwrap().section.close().unwrap(); |
| | 2133 | assert_eq!( |
| | 2134 | library::search(&[&library], None, "OFFLINE_GUEST_TOKEN") |
| | 2135 | .unwrap() |
| | 2136 | .len(), |
| | 2137 | 1 |
| | 2138 | ); |
| | 2139 | assert_eq!(std::fs::read(root.join("Sealed.one")).unwrap(), sealed); |
| | 2140 | assert!(!stored_text(&root.join("Read.one"), space).contains("OFFLINE_GUEST_TOKEN")); |
| | 2141 | let host = live_host(&root, &host_cache, sharing, &url); |
| | 2142 | assert!(eventually(|| take_json(library::sb_library_live_status( |
| | 2143 | &library |
| | 2144 | ))["state"] |
| | 2145 | == "connected")); |
| | 2146 | let original = std::fs::read(root.join("Read.one")).unwrap(); |
| | 2147 | drop(library); |
| | 2148 | let library = hosted_c(&reopened, &url, true, &cache); |
| | 2149 | assert!(eventually(|| take_json(library::sb_library_live_status( |
| | 2150 | &library |
| | 2151 | ))["state"] |
| | 2152 | == "connected")); |
| | 2153 | assert_eq!(replicas(), original_replicas); |
| | 2154 | for _ in 0..10 { |
| | 2155 | let retained: Vec<_> = original_replicas |
| | 2156 | .iter() |
| | 2157 | .flat_map(|path| notebook::Replica::open(path).unwrap().pending().unwrap()) |
| | 2158 | .collect(); |
| | 2159 | assert_eq!(retained, pending); |
| | 2160 | assert_eq!(std::fs::read(root.join("Read.one")).unwrap(), original); |
| | 2161 | std::thread::sleep(Duration::from_millis(20)); |
| | 2162 | } |
| | 2163 | // Closed-section search cannot override Work Offline while the host is reachable. |
| | 2164 | for _ in 0..32 { |
| | 2165 | assert_eq!( |
| | 2166 | library::search(&[&library], None, "OFFLINE_GUEST_TOKEN") |
| | 2167 | .unwrap() |
| | 2168 | .len(), |
| | 2169 | 1 |
| | 2170 | ); |
| | 2171 | assert_eq!(std::fs::read(root.join("Read.one")).unwrap(), original); |
| | 2172 | } |
| | 2173 | let retained: Vec<_> = original_replicas |
| | 2174 | .iter() |
| | 2175 | .flat_map(|path| notebook::Replica::open(path).unwrap().pending().unwrap()) |
| | 2176 | .collect(); |
| | 2177 | assert_eq!(retained, pending); |
| | 2178 | let section = open(&library, "Read.one"); |
| | 2179 | assert_eq!( |
| | 2180 | section.shared.section.replica().identity().unwrap(), |
| | 2181 | identity |
| | 2182 | ); |
| | 2183 | for _ in 0..10 { |
| | 2184 | assert_eq!(section.shared.section.pending().unwrap(), pending); |
| | 2185 | assert_eq!(std::fs::read(root.join("Read.one")).unwrap(), original); |
| | 2186 | std::thread::sleep(Duration::from_millis(20)); |
| | 2187 | } |
| | 2188 | assert!(library.sync_status().iter().any(|sync| sync.queued == 1)); |
| | 2189 | sb_library_sync_now(&library); |
| | 2190 | assert!(eventually(|| library |
| | 2191 | .sync_status() |
| | 2192 | .iter() |
| | 2193 | .all(|sync| sync.queued == 0))); |
| | 2194 | assert_eq!( |
| | 2195 | stored_text(&root.join("Read.one"), space) |
| | 2196 | .matches("OFFLINE_GUEST_TOKEN") |
| | 2197 | .count(), |
| | 2198 | 1 |
| | 2199 | ); |
| | 2200 | assert_eq!(std::fs::read(root.join("Sealed.one")).unwrap(), sealed); |
| | 2201 | assert!(section.shared.section.pending().unwrap().is_empty()); |
| | 2202 | let shared = Arc::clone(&section.shared); |
| | 2203 | drop(section); |
| | 2204 | Arc::into_inner(shared).unwrap().section.close().unwrap(); |
| | 2205 | sb_library_set_offline(&library, false); |
| | 2206 | let other_join = join_live(&host.code().unwrap(), "", &url, &JoinControl::new()); |
| | 2207 | let other_kept = directory.path().join("other-joined.json"); |
| | 2208 | keep_live_fixture(&other_kept, &other_join["credential"]).unwrap(); |
| | 2209 | let other_credential: serde_json::Value = |
| | 2210 | serde_json::from_slice(&std::fs::read(other_kept).unwrap()).unwrap(); |
| | 2211 | let other = hosted_c( |
| | 2212 | &other_credential, |
| | 2213 | &url, |
| | 2214 | false, |
| | 2215 | &directory.path().join("other-guest-cache"), |
| | 2216 | ); |
| | 2217 | let secret: [u8; 16] = serde_json::from_value(credential["secret"].clone()).unwrap(); |
| | 2218 | host.remove(&secret).unwrap(); |
| | 2219 | assert!(eventually(|| take_json(library::sb_library_live_status( |
| | 2220 | &library |
| | 2221 | ))["state"] |
| | 2222 | == "removed")); |
| | 2223 | let unaffected = open(&other, "Read.one"); |
| | 2224 | unaffected |
| | 2225 | .shared |
| | 2226 | .apply(Edit { |
| | 2227 | at: library::filetime(), |
| | 2228 | ops: vec![Op::Page { |
| | 2229 | space, |
| | 2230 | op: PageOp::Text { |
| | 2231 | text, |
| | 2232 | range: 0..0, |
| | 2233 | with: "OTHER_GUEST_TOKEN ".into(), |
| | 2234 | }, |
| | 2235 | }], |
| | 2236 | }) |
| | 2237 | .unwrap(); |
| | 2238 | assert!(unaffected.flush(Duration::from_secs(20))); |
| | 2239 | assert_eq!( |
| | 2240 | stored_text(&root.join("Read.one"), space) |
| | 2241 | .matches("OTHER_GUEST_TOKEN") |
| | 2242 | .count(), |
| | 2243 | 1 |
| | 2244 | ); |
| | 2245 | sb_library_set_offline(&library, true); |
| | 2246 | let section = open(&library, "Read.one"); |
| | 2247 | section |
| | 2248 | .shared |
| | 2249 | .apply(Edit { |
| | 2250 | at: library::filetime(), |
| | 2251 | ops: vec![Op::Page { |
| | 2252 | space, |
| | 2253 | op: PageOp::Text { |
| | 2254 | text, |
| | 2255 | range: 0..0, |
| | 2256 | with: "REMOVED_GUEST_TOKEN ".into(), |
| | 2257 | }, |
| | 2258 | }], |
| | 2259 | }) |
| | 2260 | .unwrap(); |
| | 2261 | section.shared.section.written().unwrap(); |
| | 2262 | let pending = section.shared.section.pending().unwrap(); |
| | 2263 | sb_library_sync_now(&library); |
| | 2264 | assert!(eventually(|| library.sync_status().iter().any(|sync| sync |
| | 2265 | .path |
| | 2266 | == "Read.one" |
| | 2267 | && sync.state == SyncState::NotConnected as u8 |
| | 2268 | && sync.queued == 1))); |
| | 2269 | assert_eq!(section.shared.section.pending().unwrap(), pending); |
| | 2270 | let shared = Arc::clone(&section.shared); |
| | 2271 | drop((section, library)); |
| | 2272 | Arc::into_inner(shared).unwrap().section.close().unwrap(); |
| | 2273 | let reopened: serde_json::Value = |
| | 2274 | serde_json::from_slice(&std::fs::read(&kept).unwrap()).unwrap(); |
| | 2275 | let library = hosted_c(&reopened, &url, true, &cache); |
| | 2276 | let section = open(&library, "Read.one"); |
| | 2277 | assert_eq!(section.shared.section.pending().unwrap(), pending); |
| | 2278 | assert!( |
| | 2279 | canvas::search::page_text(&section.shared.section.page(space).unwrap()) |
| | 2280 | .contains("REMOVED_GUEST_TOKEN") |
| | 2281 | ); |
| | 2282 | assert!(!stored_text(&root.join("Read.one"), space).contains("REMOVED_GUEST_TOKEN")); |
| | 2283 | assert_eq!(std::fs::read(root.join("Sealed.one")).unwrap(), sealed); |
| | 2284 | } |
| | 2285 | |
| | 2286 | #[test] |
| | 2287 | fn hosted_credentials_reject_unknown_versions_without_echoing_secret_input() { |
| | 2288 | let directory = tempfile::tempdir().unwrap(); |
| | 2289 | let cache = directory.path().join("must-not-be-created"); |
| | 2290 | let cache = CString::new(cache.to_str().unwrap()).unwrap(); |
| | 2291 | let wrong_version = serde_json::json!({ |
| | 2292 | "share": ([1u8; 16]), "secret": ([2u8; 16]), |
| | 2293 | "protocol": notebook::live::wire::VERSION + 1, |
| | 2294 | "notebook": "Fixture", "host": "Host", |
| | 2295 | }) |
| | 2296 | .to_string(); |
| | 2297 | for input in ["{SECRET_MUST_NOT_BE_ECHOED", wrong_version.as_str()] { |
| | 2298 | let input = CString::new(input).unwrap(); |
| | 2299 | let mut error = std::ptr::null_mut(); |
| | 2300 | let opened = unsafe { |
| | 2301 | library::sb_library_hosted( |
| | 2302 | input.as_ptr(), |
| | 2303 | c"Guest".as_ptr(), |
| | 2304 | std::ptr::null(), |
| | 2305 | std::ptr::null(), |
| | 2306 | false, |
| | 2307 | true, |
| | 2308 | cache.as_ptr(), |
| | 2309 | &mut error, |
| | 2310 | ) |
| | 2311 | }; |
| | 2312 | assert!(opened.is_null()); |
| | 2313 | assert!(!error.is_null()); |
| | 2314 | let error = unsafe { CString::from_raw(error) }; |
| | 2315 | assert!( |
| | 2316 | !error |
| | 2317 | .to_bytes() |
| | 2318 | .windows(b"SECRET_MUST_NOT_BE_ECHOED".len()) |
| | 2319 | .any(|bytes| bytes == b"SECRET_MUST_NOT_BE_ECHOED") |
| | 2320 | ); |
| | 2321 | } |
| | 2322 | assert!(!Path::new(cache.to_str().unwrap()).exists()); |
| | 2323 | } |