Skip to main content

coven/
test_utils.rs

1//! Installations for application tests: an isolated in-memory keychain and
2//! a network the test drives, with the production store graph.
3
4use super::Coven;
5use crate::*;
6use coven_crypto::custody::{Keychain, KeyringCustody, StoreKeychain};
7use coven_foundation::files::StoreDirectories;
8use coven_foundation::network_watch::{NetworkSource, TestNetwork};
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12#[cfg(test)]
13#[path = "test_utils_tests.rs"]
14mod tests;
15
16impl Coven {
17    /// An installation for a test: its stores under `app_dir`, a keychain
18    /// with no device-only entries and none of the person's entries, isolated
19    /// from the OS and other tests, and a usable network that isn't metered,
20    /// which the test drives. A new one simulates another device; keep one to
21    /// test reopening.
22    pub fn in_memory(app_dir: PathBuf) -> Self {
23        Self::in_memory_reporting_to(app_dir, crate::task::HeardBugs::shared())
24    }
25
26    /// `in_memory`, whose bugs in work nobody waits for go to `bugs`, for a
27    /// test that hears them (E15).
28    pub fn in_memory_reporting_to(app_dir: PathBuf, bugs: crate::task::BugSinkRef) -> Self {
29        Self::testing(app_dir, Keychain::in_memory("coven-test").expect("valid service name"), bugs)
30    }
31
32    /// An installation, as `in_memory` makes, whose keychain's device-only
33    /// entries are the ones `save_device_keychain` saved at `path`: the same
34    /// device again, which opens the stores it had, as a test reopens a kept
35    /// copy of a store in another process. Its person entries start empty.
36    pub fn in_memory_with_saved_keychain(app_dir: PathBuf, path: &Path) -> Result<Self, KeyError> {
37        Ok(Self::testing(
38            app_dir,
39            Keychain::in_memory_with_saved_device_entries("coven-test", path)?,
40            crate::task::HeardBugs::shared(),
41        ))
42    }
43
44    /// An installation on an Android phone: its person entries behave as
45    /// Block Store keeps them, this phone's own, with Block Store's limits
46    /// (§13.1).
47    pub fn with_block_store(app_dir: PathBuf) -> Self {
48        Self::testing(
49            app_dir,
50            Keychain::in_memory_block_store("coven-test").expect("valid service name"),
51            crate::task::HeardBugs::shared(),
52        )
53    }
54
55    /// An installation for a test with the keychain `keychain`, such as one
56    /// failing on cue.
57    #[cfg(test)]
58    pub(crate) fn with_keychain(app_dir: PathBuf, keychain: Arc<Keychain>) -> Self {
59        Self::testing(app_dir, keychain, crate::task::HeardBugs::shared())
60    }
61
62    /// An installation for a test with the keychain `keychain`, whose bugs
63    /// in work nobody waits for go to `bugs` (E15).
64    #[cfg(test)]
65    pub(crate) fn with_keychain_reporting_to(
66        app_dir: PathBuf,
67        keychain: Arc<Keychain>,
68        bugs: crate::task::BugSinkRef,
69    ) -> Self {
70        Self::testing(app_dir, keychain, bugs)
71    }
72
73    /// The app directory's stores, for a test that begins a bootstrap or a
74    /// replacement itself.
75    #[cfg(test)]
76    pub(crate) fn directories(&self) -> &StoreDirectories {
77        &self.directories
78    }
79
80    fn testing(app_dir: PathBuf, keychain: Arc<Keychain>, bugs: crate::task::BugSinkRef) -> Self {
81        Self {
82            directories: StoreDirectories::new(app_dir, bugs),
83            keychain,
84            network: NetworkSource::Test(TestNetwork::new(true, false)),
85        }
86    }
87
88    /// Another device of the same person, its stores under `app_dir`: an
89    /// installation with its own empty device-only keychain. With iCloud
90    /// Keychain it shares this one's person entries, as iCloud Keychain
91    /// carries a restore code and S3 key between the person's devices; with
92    /// Block Store it has its own, empty.
93    pub fn another_device(&self, app_dir: PathBuf) -> Self {
94        Self::testing(app_dir, self.keychain.another_device(), self.directories.bugs().clone())
95    }
96
97    /// A new phone set up from this one by direct transfer, its stores under
98    /// `app_dir`: nothing of this phone's own, and a copy of its Block Store
99    /// entries (§13.1). Requires `with_block_store`.
100    pub fn set_up_new_device(&self, app_dir: PathBuf) -> Self {
101        Self::testing(app_dir, self.keychain.set_up_new_device(), self.directories.bugs().clone())
102    }
103
104    /// A new phone set up from this phone's last cloud backup, its stores
105    /// under `app_dir`: the Block Store entries that backup holds (§13.1).
106    /// Requires `with_block_store`.
107    pub fn restore_new_device_from_cloud(&self, app_dir: PathBuf) -> Self {
108        Self::testing(
109            app_dir,
110            self.keychain.restore_new_device_from_cloud(),
111            self.directories.bugs().clone(),
112        )
113    }
114
115    /// Google's periodic cloud backup of this phone's Block Store: the entries
116    /// stored while end-to-end encryption was available. Requires
117    /// `with_block_store`.
118    pub fn back_up_to_cloud(&self) {
119        self.keychain.back_up_to_cloud();
120    }
121
122    /// Whether this phone's cloud backup would be end-to-end encrypted: it
123    /// has a screen lock. Requires `with_block_store`.
124    pub fn set_end_to_end_encryption(&self, available: bool) {
125        self.keychain.set_end_to_end_encryption(available);
126    }
127
128    /// Create through the production builder, then close for a reopening fixture.
129    pub async fn create_closed_store(
130        &self,
131        name: &str,
132        ids: IdSourceRef,
133    ) -> Result<StoreDir, StoreCreationError> {
134        let handle = self
135            .builder()
136            .id_source(ids)
137            .synced_tables(Vec::new())
138            .migrations(Vec::new())
139            .create_store(name)
140            .await?;
141        let directory = self.store_dir(&handle.store_id());
142        handle.close().await?;
143        Ok(directory)
144    }
145
146    /// Publish a directory fixture without a database or member custody.
147    pub fn create_store_dir(
148        &self,
149        id: StoreId,
150        ids: &dyn IdSource,
151    ) -> Result<StoreDir, crate::DirectoryCreationError> {
152        self.directories.create_store_dir(id, ids)
153    }
154
155    /// Change the network this installation's test drives.
156    fn drive_network(&self, change: impl FnOnce(&TestNetwork)) {
157        match &self.network {
158            NetworkSource::Test(network) => change(network),
159            NetworkSource::Platform | NetworkSource::Refused(_) => {
160                panic!("an installation made for a test drives its network")
161            }
162        }
163    }
164
165    /// The device moves to another usable network: a network report, as
166    /// the platform's would make (api.md E5).
167    pub fn join_network(&self, metered: bool) {
168        self.drive_network(|network| network.join(metered));
169    }
170
171    /// The device loses its network: no report.
172    pub fn lose_network(&self) {
173        self.drive_network(TestNetwork::lose);
174    }
175
176    /// The platform fails to say what the device's network is: coven can't
177    /// tell, and calls it metered, until the test says again.
178    pub fn network_cannot_tell(&self) {
179        self.drive_network(TestNetwork::cannot_tell);
180    }
181
182    /// The device's network turns metered or unmetered: no report.
183    pub fn set_network_metered(&self, metered: bool) {
184        self.drive_network(|network| network.set_metered(metered));
185    }
186
187    /// Seed the simulated OS custody as if this installation had opened keys.
188    pub fn keep_store_keys(
189        &self,
190        directory: &StoreDir,
191        keys: &StoreKeyring,
192    ) -> Result<(), KeyError> {
193        let keychain = Arc::new(StoreKeychain::new(self.keychain.clone(), directory.id()));
194        Custody::<StoreKeyring>::persist(&KeyringCustody::new(keychain), keys)
195    }
196
197    /// Save this installation's device-only keychain entries at `path`,
198    /// replacing what is there: each store's device id, store keys and
199    /// member keys, which `in_memory_with_saved_keychain` loads. The
200    /// person's entries stay out.
201    pub fn save_device_keychain(&self, path: &Path) -> Result<(), KeyError> {
202        self.keychain.save_device_entries(path)
203    }
204
205    /// Delete this installation's storage credentials for `store`, as the
206    /// person deleting the keychain entry by hand does. The recorded location
207    /// stays: a handle opened afterwards reports no storage set up on this
208    /// device, and setup connects that location again.
209    pub fn lose_storage_credentials(&self, store: StoreId) -> Result<(), KeyError> {
210        StoreKeychain::new(self.keychain.clone(), store).delete_storage_credentials()
211    }
212
213    /// This installation's custody for one store, to read what it holds.
214    #[cfg(test)]
215    pub(crate) fn store_keychain(&self, store: StoreId) -> StoreKeychain {
216        StoreKeychain::new(self.keychain.clone(), store)
217    }
218
219    /// The person's entries for one store, as this installation sees them.
220    #[cfg(test)]
221    pub(crate) fn person_entries(&self, store: StoreId) -> coven_crypto::custody::PersonEntries {
222        coven_crypto::custody::PersonEntries::new(self.keychain.clone(), store)
223    }
224
225    /// While `refuse` holds, refuse this installation's calls on person entries only.
226    #[cfg(test)]
227    pub(crate) fn refuse_person_entry_operations(&self, refuse: bool) {
228        self.keychain.refuse_person_entry_operations(refuse);
229    }
230
231    /// The stores listed, every one readable; an unreadable entry fails the test.
232    #[cfg(test)]
233    pub(crate) async fn readable_stores(&self) -> Vec<StoreInfo> {
234        self.stores()
235            .await
236            .unwrap()
237            .into_iter()
238            .map(|listed| match listed {
239                ListedStore::Readable(info) => info,
240                ListedStore::Unreadable { id, reason } => {
241                    panic!("store {id} is listed as unreadable: {reason}")
242                }
243            })
244            .collect()
245    }
246
247    /// Refuse the next keychain operation before changing any stored value.
248    pub fn fail_next_keychain_operation(&self) {
249        self.keychain.fail_next_operation();
250    }
251
252    #[cfg(test)]
253    pub(crate) fn fail_keychain_operations_after(&self, successes: &[usize]) {
254        self.keychain.fail_operations_after(successes);
255    }
256}