syncthing_rs/
client.rs

1use crate::{
2    error::{Error, Result},
3    types::{
4        cluster::{PendingDevices, PendingFolders},
5        config::{
6            Configuration, DeviceConfiguration, FolderConfiguration, NewDeviceConfiguration,
7            NewFolderConfiguration,
8        },
9        db::Completion,
10        events::Event,
11        system::Connections,
12    },
13};
14use reqwest::{StatusCode, header};
15use tokio::sync::broadcast::Sender;
16
17const ADDR: &str = "http://localhost:8384/rest";
18
19/// A `ClientBuilder` can be used to create a `Client` with custom configuration.
20#[must_use]
21pub struct ClientBuilder {
22    base_url: Option<String>,
23    api_key: String,
24}
25
26impl ClientBuilder {
27    /// Constructs a new `ClientBuilder`.
28    /// This is the same as `Client::builder()`.
29    ///
30    /// The API can either be generated in the GUI of Syncthing or set
31    /// in the configuration file under `configuration/gui/apikey`.
32    pub fn new(api_key: impl Into<String>) -> Self {
33        Self {
34            base_url: None,
35            api_key: api_key.into(),
36        }
37    }
38
39    /// Set the syncthing URL to something different than `http://localhost:8384/rest`.
40    pub fn base_url(mut self, url: impl Into<String>) -> Self {
41        self.base_url = Some(url.into());
42        self
43    }
44
45    /// Returns a `Client` that uses this `ClientBuilder` configuration.
46    ///
47    /// # Errors
48    ///
49    /// This method fails if the header cannot be created or the HTTP client
50    /// cannot be initialized.
51    pub fn build(self) -> Result<Client> {
52        let base_url = self.base_url.unwrap_or_else(|| ADDR.to_string());
53
54        let mut headers = header::HeaderMap::new();
55        let mut api_key_header = header::HeaderValue::from_str(&self.api_key)?;
56        api_key_header.set_sensitive(true);
57        headers.insert("X-API-KEY", api_key_header);
58
59        let client = reqwest::Client::builder()
60            .default_headers(headers)
61            .build()?;
62
63        Ok(Client { client, base_url })
64    }
65}
66
67/// Abstraction to interact with the Syncthing API.
68///
69/// The Client has various configuration values to tweak, such as the
70/// URL which is set to `localhost:8384/rest` by default. To configure a `Client`,
71/// use `Client::builder()`.
72#[derive(Clone, Debug)]
73pub struct Client {
74    client: reqwest::Client,
75    base_url: String,
76}
77
78impl Client {
79    /// Creates a new HTTP client, with which the syncthing API can be used.
80    ///
81    /// # Panics
82    ///
83    /// This method panics if the client cannot be initialized.
84    ///
85    /// Use `Client::builder()` if you wish to handle the failure as an `Error`
86    /// instead of panicking.
87    #[must_use]
88    pub fn new(api_key: &str) -> Self {
89        ClientBuilder::new(api_key).build().expect("Client::new()")
90    }
91
92    /// Creates a `ClientBuilder` to configure a `Client`.
93    /// This is the same as `ClientBuilder::new()`
94    ///
95    /// The API can either be generated in the GUI of Syncthing or set
96    /// in the configuration file under `configuration/gui/apikey`.
97    pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
98        ClientBuilder::new(api_key)
99    }
100
101    /// Gets all the connections
102    pub async fn get_connections(&self) -> Result<Connections> {
103        log::debug!("GET /system/connections");
104        Ok(self
105            .client
106            .get(format!("{}/system/connections", self.base_url))
107            .send()
108            .await?
109            .error_for_status()?
110            .json()
111            .await?)
112    }
113
114    /// Returns `()` if the syncthing API can be reached.
115    ///
116    /// Use [`health`](crate::client::Client::health) to do the same
117    /// without the need of a valid `api_key`.
118    pub async fn ping(&self) -> Result<()> {
119        log::debug!("GET /system/ping");
120        self.client
121            .get(format!("{}/system/ping", self.base_url))
122            .send()
123            .await?
124            .error_for_status()?;
125
126        Ok(())
127    }
128
129    /// Returns `()` if the syncthing API can be reached.
130    ///
131    /// Use [`ping`](crate::client::Client::ping) to do the same
132    /// but with the requirement of a valid `api_key`.
133    pub async fn health(&self) -> Result<()> {
134        log::debug!("GET /noauth/health");
135        self.client
136            .get(format!("{}/noauth/health", self.base_url))
137            .send()
138            .await?
139            .error_for_status()?;
140
141        Ok(())
142    }
143
144    /// Returns the ID of the current device. This endpoint
145    /// does not require a valid `api_key`.
146    pub async fn get_id(&self) -> Result<String> {
147        log::debug!("GET /noauth/health");
148        Ok(self
149            .client
150            .get(format!("{}/noauth/health", self.base_url))
151            .send()
152            .await?
153            .error_for_status()?
154            .headers()
155            .get("X-Syncthing-Id")
156            .ok_or(Error::HeaderDeviceIDError)?
157            .to_str()
158            .map_err(|_| Error::HeaderParseError)?
159            .to_string())
160    }
161
162    /// Only returns if an error is encountered.
163    /// Transmits every new [event](crate::types::events::Event) over `tx`.
164    /// If `skip_old`, all events before the call to this function do not
165    /// result in a transmission.
166    pub async fn get_events(&self, tx: Sender<Event>, mut skip_old: bool) -> Result<()> {
167        let mut current_id = 0;
168        loop {
169            log::debug!("GET /events");
170            let events: Vec<Event> = self
171                .client
172                .get(format!("{}/events?since={}", self.base_url, current_id))
173                .send()
174                .await?
175                .error_for_status()?
176                .json()
177                .await?;
178
179            log::debug!("received {} new events", events.len());
180            for event in events {
181                current_id = event.id;
182                if !skip_old {
183                    tx.send(event)?;
184                }
185            }
186            log::debug!("current event id is {current_id}");
187            skip_old = false;
188        }
189    }
190
191    /// Returns the entire [`Configuration`]
192    ///
193    /// # Errors
194    ///
195    /// This method fails if the API cannot be reached, the server
196    /// answers with an error code or the JSON cannot be parsed.
197    pub async fn get_configuration(&self) -> Result<Configuration> {
198        log::debug!("GET /config");
199        Ok(self
200            .client
201            .get(format!("{}/config", self.base_url))
202            .send()
203            .await?
204            .error_for_status()?
205            .json()
206            .await?)
207    }
208
209    /// Posts a folder. If the folder already exists, it is replaced,
210    /// otherwise a new one is added.
211    ///
212    /// Use [`add_folder`](crate::client::Client::add_folder) if the operation
213    /// should fail if a folder with the same ID already exists.
214    pub async fn post_folder(&self, folder: impl Into<NewFolderConfiguration>) -> Result<()> {
215        let folder = folder.into();
216        log::debug!("POST /config/folders {:?}", folder);
217        self.client
218            .post(format!("{}/config/folders", self.base_url))
219            .json(&folder)
220            .send()
221            .await?
222            .error_for_status()?;
223
224        Ok(())
225    }
226
227    /// Adds a new folder. If the folder already exists, a
228    /// [`DuplicateFolderError`](crate::error::Error::DuplicateFolderError) is returned.
229    /// This requires an additional check against the API.
230    ///
231    /// Use [`post_folder`](crate::client::Client::post_folder) if the operation
232    /// should blindly set the folder.
233    pub async fn add_folder(&self, folder: impl Into<NewFolderConfiguration>) -> Result<()> {
234        let folder = folder.into();
235        match self.get_folder(folder.get_id()).await {
236            Ok(_) => return Err(Error::DuplicateFolderError),
237            Err(Error::UnknownFolderError) => (),
238            Err(e) => return Err(e),
239        }
240        self.post_folder(folder).await
241    }
242
243    /// Gets the configuration for the folder with the ID `folder_id`. Explicitly
244    /// returns a [`UnknownFolderError`](crate::error::Error::UnknownFolderError)
245    /// if no folder with `folder_id` exists.
246    pub async fn get_folder(&self, folder_id: &str) -> Result<FolderConfiguration> {
247        log::debug!("GET /config/folders/{}", folder_id);
248        let response = self
249            .client
250            .get(format!("{}/config/folders/{}", self.base_url, folder_id))
251            .send()
252            .await?;
253
254        if response.status() == StatusCode::NOT_FOUND {
255            // TODO check that really the folder ID is causing that
256            Err(Error::UnknownFolderError)
257        } else {
258            Ok(response.error_for_status()?.json().await?)
259        }
260    }
261
262    /// Deletes the folder with the ID `folder_id`.
263    pub async fn delete_folder(&self, folder_id: &str) -> Result<()> {
264        log::debug!("DELETE /config/folders/{}", folder_id);
265        self.client
266            .delete(format!("{}/config/folders/{}", self.base_url, folder_id))
267            .send()
268            .await?
269            .error_for_status()?;
270        Ok(())
271    }
272
273    /// Posts a device. If the device already exists, it is replaced,
274    /// otherwise a new one is added.
275    ///
276    /// Use [`add_device`](crate::client::Client::add_device) if the operation
277    /// should fail if a device with the same ID already exists.
278    pub async fn post_device(&self, device: impl Into<NewDeviceConfiguration>) -> Result<()> {
279        let device = device.into();
280        log::debug!("POST /config/devices {:?}", device);
281        self.client
282            .post(format!("{}/config/devices", self.base_url))
283            .json(&device)
284            .send()
285            .await?
286            .error_for_status()?;
287
288        Ok(())
289    }
290
291    /// Adds a new device. If the device already exists, a
292    /// [`DuplicateDeviceError`](crate::error::Error::DuplicateDeviceError) is returned.
293    /// This requires an additional check against the API.
294    ///
295    /// Use [`post_device`](crate::client::Client::post_device) if the operation
296    /// should blindly set the device.
297    pub async fn add_device(&self, device: impl Into<NewDeviceConfiguration>) -> Result<()> {
298        let device = device.into();
299        match self.get_device(device.get_device_id()).await {
300            Ok(_) => return Err(Error::DuplicateDeviceError),
301            Err(Error::UnknownDeviceError) => (),
302            Err(e) => return Err(e),
303        }
304        self.post_device(device).await
305    }
306
307    /// Gets the configuration for the device with the ID `device_id`.
308    pub async fn get_device(&self, device_id: &str) -> Result<DeviceConfiguration> {
309        log::debug!("GET /config/devices/{}", device_id);
310        let response = self
311            .client
312            .get(format!("{}/config/devices/{}", self.base_url, device_id))
313            .send()
314            .await?;
315
316        if response.status() == StatusCode::NOT_FOUND {
317            // TODO check that really the device ID is causing that
318            Err(Error::UnknownDeviceError)
319        } else {
320            Ok(response.error_for_status()?.json().await?)
321        }
322    }
323
324    /// Deletes the device with the ID `device_id`.
325    pub async fn delete_device(&self, device_id: &str) -> Result<()> {
326        log::debug!("DELETE /config/devices/{}", device_id);
327        self.client
328            .delete(format!("{}/config/devices/{}", self.base_url, device_id))
329            .send()
330            .await?
331            .error_for_status()?;
332        Ok(())
333    }
334
335    /// Gets a list of all pending remote devices which have tried to connect but
336    /// are not in our configuration yet.
337    pub async fn get_pending_devices(&self) -> Result<PendingDevices> {
338        log::debug!("GET /cluster/pending/devices");
339        Ok(self
340            .client
341            .get(format!("{}/cluster/pending/devices", self.base_url))
342            .send()
343            .await?
344            .error_for_status()?
345            .json()
346            .await?)
347    }
348
349    /// Gets all folders which remote devices have offered to us, but are not yet shared
350    /// from our instance to them or are not present on our instance.
351    pub async fn get_pending_folders(&self) -> Result<PendingFolders> {
352        log::debug!("GET /cluster/pending/folders");
353        Ok(self
354            .client
355            .get(format!("{}/cluster/pending/folders", self.base_url))
356            .send()
357            .await?
358            .error_for_status()?
359            .json()
360            .await?)
361    }
362
363    /// Remove record about pending remote device with ID `device_id` which tried to connect.
364    ///
365    /// This is not permanent, use `ignore_device` instead.
366    pub async fn dismiss_pending_device(&self, device_id: &str) -> Result<()> {
367        log::debug!("DELETE /cluster/pending/devices?device={device_id}");
368        self.client
369            .delete(format!(
370                "{}/cluster/pending/devices?device={}",
371                self.base_url, device_id
372            ))
373            .send()
374            .await?
375            .error_for_status()?;
376
377        Ok(())
378    }
379
380    /// Remove record about pending remote folder with ID `folder_id`. An optional `device_id`
381    /// can be passed as argument to only remove the pending remote from that device, otherwise
382    /// the folder will be removed as pending for all devices.
383    ///
384    /// This is not permanent, use `ignore_folder` instead.
385    pub async fn dismiss_pending_folder(
386        &self,
387        folder_id: &str,
388        device_id: Option<&str>,
389    ) -> Result<()> {
390        let device_str = match device_id {
391            Some(device_id) => format!("&device={}", device_id),
392            None => String::new(),
393        };
394        log::debug!("DELETE /cluster/pending/folders?folder={folder_id}{device_str}");
395        self.client
396            .delete(format!(
397                "{}/cluster/pending/folders?folder={}{}",
398                self.base_url, folder_id, device_str
399            ))
400            .send()
401            .await?
402            .error_for_status()?;
403
404        Ok(())
405    }
406
407    /// Returns a template device configuration with all default values,
408    /// which only requires a unique device ID to be instantiated.
409    pub async fn get_default_device(&self) -> Result<DeviceConfiguration> {
410        log::debug!("GET /config/defaults/device");
411        Ok(self
412            .client
413            .get(format!("{}/config/defaults/device", self.base_url))
414            .send()
415            .await?
416            .error_for_status()?
417            .json()
418            .await?)
419    }
420
421    /// Returns a template folder configuration with all default values,
422    /// which only requires a unique folder ID to be instantiated.
423    pub async fn get_default_folder(&self) -> Result<FolderConfiguration> {
424        log::debug!("GET /config/defaults/folder");
425        Ok(self
426            .client
427            .get(format!("{}/config/defaults/folder", self.base_url))
428            .send()
429            .await?
430            .error_for_status()?
431            .json()
432            .await?)
433    }
434
435    /// Calculates the data synchronization completion percentage and counts.
436    ///
437    /// Returns the completion percentage (0 to 100), total bytes, and total items.
438    ///
439    /// # Arguments
440    ///
441    /// * `folder_id` - The ID of the folder to calculate completion for.
442    ///   `None` calculates the aggregate completion across all folders.
443    /// * `device_id` - The ID of the device to calculate completion for.
444    ///   `None` calculates the aggregate completion across all devices.
445    ///   If `device_id` is specified but `folder_id` is `None`,
446    ///   completion is calculated for all folders *shared with that device*.
447    pub async fn get_completion(
448        &self,
449        folder_id: Option<&str>,
450        device_id: Option<&str>,
451    ) -> Result<Completion> {
452        let folder_str = match folder_id {
453            Some(folder_id) => format!("folder={}", folder_id),
454            None => String::new(),
455        };
456        let device_str = match device_id {
457            Some(device_id) => format!("device={}", device_id),
458            None => String::new(),
459        };
460        let questionmark = if folder_id.is_some() || device_id.is_some() {
461            "?"
462        } else {
463            ""
464        };
465        let and = if folder_id.is_some() && device_id.is_some() {
466            "&"
467        } else {
468            ""
469        };
470        let query = format!("{}{}{}{}", questionmark, folder_str, and, device_str);
471        log::debug!("GET /db/completion{}", query);
472
473        Ok(self
474            .client
475            .get(format!("{}/db/completion{}", self.base_url, query))
476            .send()
477            .await?
478            .error_for_status()?
479            .json()
480            .await?)
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use crate::types::{config::FolderDeviceConfiguration, events::EventType};
487
488    use super::*;
489
490    use httpmock::prelude::*;
491    use testcontainers::{
492        ContainerAsync, GenericImage, ImageExt,
493        core::{ContainerPort::Tcp, WaitFor},
494        runners::AsyncRunner,
495    };
496    use tokio::sync::broadcast;
497
498    use rstest::*;
499
500    // Example device id from the docs
501    const DEVICE_ID: &str = "MFZWI3D-BONSGYC-YLTMRWG-C43ENR5-QXGZDMM-FZWI3DP-BONSGYY-LTMRWAD";
502
503    #[fixture]
504    async fn syncthing_setup() -> (ContainerAsync<GenericImage>, Client) {
505        let api_key = "foobar";
506        let container = GenericImage::new("syncthing/syncthing", "latest")
507            .with_exposed_port(Tcp(8384))
508            .with_wait_for(WaitFor::message_on_stdout("GUI and API listening on "))
509            .with_env_var("STGUIAPIKEY", api_key)
510            .start()
511            .await
512            .expect("failed to start syncthing container");
513
514        let host = container
515            .get_host()
516            .await
517            .expect("could not get syncthing host");
518        let port = container
519            .get_host_port_ipv4(8384)
520            .await
521            .expect("could not get syncthing port");
522
523        let url = format!("http://{host}:{port}/rest");
524
525        let client = ClientBuilder::new(api_key).base_url(url).build().unwrap();
526
527        (container, client)
528    }
529
530    #[test]
531    fn test_new() {
532        let client = Client::new("foo");
533
534        assert_eq!(client.base_url, "http://localhost:8384/rest");
535    }
536
537    /// Simple ping to a running server should just return Ok(())
538    #[tokio::test]
539    async fn test_ping() {
540        let server = MockServer::start();
541
542        let ping_mock = server.mock(|when, then| {
543            when.method(GET).path("/system/ping");
544            then.status(200)
545                .header("content-type", "application/json")
546                .body(r#"{"ping": "pong"}"#);
547        });
548
549        let client = ClientBuilder::new("")
550            .base_url(server.base_url())
551            .build()
552            .unwrap();
553
554        let result = client.ping().await;
555        ping_mock.assert();
556
557        assert!(result.is_ok());
558    }
559
560    /// Simple test ensuring that a single event in the past is correctly
561    /// transmitted.
562    #[tokio::test]
563    async fn test_single_event() {
564        let server = MockServer::start();
565
566        let event_mock = server.mock(|when, then| {
567            when.method(GET).path("/events");
568            then.status(200)
569                .header("content-type", "application/json")
570                .body(
571                    r#"
572[
573  {
574    "id": 1,
575    "globalID": 1,
576    "time": "2025-05-07T17:05:44.514050967+02:00",
577    "type": "Starting",
578    "data": {
579      "home": "/home/user/.config/syncthing",
580      "myID": "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX"
581    }
582  }
583]
584"#,
585                );
586        });
587
588        let client = ClientBuilder::new("")
589            .base_url(server.base_url())
590            .build()
591            .unwrap();
592
593        let (tx, mut rx) = broadcast::channel(1);
594
595        // Start transmitting events on a separate thread
596        tokio::spawn(async move {
597            let result = client.get_events(tx, false).await;
598            unreachable!("get_events should not have returned: {:?}", result);
599        });
600
601        let event = rx.recv().await;
602        assert!(event_mock.hits() > 0);
603        assert!(event.is_ok());
604        assert!(matches!(event.unwrap().ty, EventType::Starting { home: _ }));
605    }
606
607    #[tokio::test]
608    async fn container_test_health() {
609        // Create container by hand, so we don't know the API key. This is okay
610        // as the health endpoint should work anyway
611        let container = GenericImage::new("syncthing/syncthing", "latest")
612            .with_exposed_port(Tcp(8384))
613            .with_wait_for(WaitFor::message_on_stdout("GUI and API listening on "))
614            .start()
615            .await
616            .expect("failed to start syncthing container");
617
618        let host = container
619            .get_host()
620            .await
621            .expect("could not get syncthing host");
622        let port = container
623            .get_host_port_ipv4(8384)
624            .await
625            .expect("could not get syncthing port");
626
627        let url = format!("http://{host}:{port}/rest");
628
629        let client = ClientBuilder::new("idk").base_url(url).build().unwrap();
630
631        client.health().await.unwrap();
632    }
633
634    #[tokio::test]
635    async fn container_test_id() {
636        // Create container by hand, so we don't know the API key. This is okay
637        // as the id endpoint should work anyway
638        let container = GenericImage::new("syncthing/syncthing", "latest")
639            .with_exposed_port(Tcp(8384))
640            .with_wait_for(WaitFor::message_on_stdout("GUI and API listening on "))
641            .start()
642            .await
643            .expect("failed to start syncthing container");
644
645        let host = container
646            .get_host()
647            .await
648            .expect("could not get syncthing host");
649        let port = container
650            .get_host_port_ipv4(8384)
651            .await
652            .expect("could not get syncthing port");
653
654        let url = format!("http://{host}:{port}/rest");
655
656        let client = ClientBuilder::new("idk").base_url(url).build().unwrap();
657
658        client.get_id().await.unwrap();
659    }
660
661    #[rstest]
662    #[tokio::test]
663    async fn container_test_ping(
664        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
665    ) {
666        let (_container, client) = syncthing_setup.await;
667
668        client.ping().await.unwrap();
669    }
670
671    #[rstest]
672    #[tokio::test]
673    async fn container_test_get_config(
674        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
675    ) {
676        let (_container, client) = syncthing_setup.await;
677
678        client
679            .get_configuration()
680            .await
681            .expect("could not get config");
682    }
683
684    #[rstest]
685    #[tokio::test]
686    async fn container_test_post_folder(
687        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
688    ) {
689        let (_container, client) = syncthing_setup.await;
690        let folder_id = "this-is-a-new-folder";
691        let path = "/tmp";
692
693        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
694
695        client
696            .post_folder(folder)
697            .await
698            .expect("could not post folder");
699
700        let api_folder = client
701            .get_folder(folder_id)
702            .await
703            .expect("could not get folder");
704
705        assert_eq!(&api_folder.id, folder_id);
706        assert_eq!(&api_folder.path, path);
707    }
708
709    #[rstest]
710    #[tokio::test]
711    async fn container_test_add_folder(
712        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
713    ) {
714        let (_container, client) = syncthing_setup.await;
715        let folder_id = "this-is-a-new-folder";
716        let path = "/tmp";
717
718        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
719
720        client
721            .add_folder(folder)
722            .await
723            .expect("could not add folder");
724
725        let api_folder = client
726            .get_folder(folder_id)
727            .await
728            .expect("could not get folder");
729
730        assert_eq!(&api_folder.id, folder_id);
731        assert_eq!(&api_folder.path, path);
732    }
733
734    #[rstest]
735    #[tokio::test]
736    #[should_panic(expected = "DuplicateFolderError")]
737    async fn container_test_add_folder_twice_panic(
738        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
739    ) {
740        let (_container, client) = syncthing_setup.await;
741        let folder_id = "this-is-a-new-folder";
742        let path = "/tmp";
743
744        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
745
746        client
747            .add_folder(folder)
748            .await
749            .expect("could not add folder");
750
751        // "Accidentally" overwrite our folder
752        let duplicate_path = "/usr";
753        let duplicate_folder =
754            NewFolderConfiguration::new(folder_id.to_string(), duplicate_path.to_string());
755
756        client
757            .add_folder(duplicate_folder)
758            .await
759            .expect("could not add folder")
760    }
761
762    #[rstest]
763    #[tokio::test]
764    async fn container_test_post_folder_twice(
765        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
766    ) {
767        let (_container, client) = syncthing_setup.await;
768        let folder_id = "this-is-a-new-folder";
769        let path = "/tmp";
770
771        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
772
773        client
774            .add_folder(folder)
775            .await
776            .expect("could not add folder");
777
778        // "Accidentally" overwrite our folder
779        let duplicate_path = "/usr";
780        let duplicate_folder =
781            NewFolderConfiguration::new(folder_id.to_string(), duplicate_path.to_string());
782
783        client
784            .post_folder(duplicate_folder)
785            .await
786            .expect("could not post folder");
787
788        let api_folder = client
789            .get_folder(folder_id)
790            .await
791            .expect("could not get folder");
792
793        assert_eq!(&api_folder.id, folder_id);
794        assert_eq!(&api_folder.path, duplicate_path);
795    }
796
797    #[rstest]
798    #[tokio::test]
799    async fn container_test_post_device(
800        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
801    ) {
802        let (_container, client) = syncthing_setup.await;
803
804        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
805
806        client
807            .post_device(device)
808            .await
809            .expect("could not post device");
810
811        let api_device = client
812            .get_device(DEVICE_ID)
813            .await
814            .expect("could not get device");
815
816        assert_eq!(&api_device.device_id, DEVICE_ID);
817    }
818
819    #[rstest]
820    #[tokio::test]
821    async fn container_test_add_device(
822        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
823    ) {
824        let (_container, client) = syncthing_setup.await;
825
826        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
827
828        client
829            .add_device(device)
830            .await
831            .expect("could not add device");
832
833        let api_device = client
834            .get_device(DEVICE_ID)
835            .await
836            .expect("could not get device");
837
838        assert_eq!(&api_device.device_id, DEVICE_ID);
839    }
840
841    #[rstest]
842    #[tokio::test]
843    #[should_panic(expected = "DuplicateDeviceError")]
844    async fn container_test_add_device_twice_panic(
845        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
846    ) {
847        let (_container, client) = syncthing_setup.await;
848
849        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
850
851        client
852            .add_device(device)
853            .await
854            .expect("could not add device");
855
856        // "Accidentally" overwrite our device
857        let duplicate_device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
858
859        client
860            .add_device(duplicate_device)
861            .await
862            .expect("could not add device")
863    }
864
865    #[rstest]
866    #[tokio::test]
867    async fn container_test_post_device_twice(
868        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
869    ) {
870        let (_container, client) = syncthing_setup.await;
871        let name = "original";
872
873        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string()).name(name.to_string());
874
875        client
876            .add_device(device)
877            .await
878            .expect("could not add device");
879
880        // "Accidentally" overwrite our device
881        let duplicate_name = "duplicate";
882        let duplicate_device =
883            NewDeviceConfiguration::new(DEVICE_ID.to_string()).name(duplicate_name.to_string());
884
885        client
886            .post_device(duplicate_device)
887            .await
888            .expect("could not post device");
889
890        let api_device = client
891            .get_device(DEVICE_ID)
892            .await
893            .expect("could not get device");
894
895        assert_eq!(&api_device.device_id, DEVICE_ID);
896        assert_eq!(&api_device.name, duplicate_name);
897    }
898
899    #[rstest]
900    #[tokio::test]
901    async fn container_test_pending_device(
902        #[future]
903        #[from(syncthing_setup)]
904        first: (ContainerAsync<GenericImage>, Client),
905        #[future]
906        #[from(syncthing_setup)]
907        second: (ContainerAsync<GenericImage>, Client),
908    ) {
909        let (_first_container, first_client) = first.await;
910        let (_second_container, second_client) = second.await;
911
912        let first_id = first_client
913            .get_id()
914            .await
915            .expect("could not get id of first container");
916        let second_id = second_client
917            .get_id()
918            .await
919            .expect("could not get id of second container");
920
921        // First starts waiting for the event
922        let (event_tx, mut event_rx) = broadcast::channel(10);
923        let first_client_handle = first_client.clone();
924        tokio::spawn(async move {
925            first_client_handle
926                .get_events(event_tx, true)
927                .await
928                .unwrap();
929        });
930
931        // Add the first device to the second
932        second_client
933            .add_device(NewDeviceConfiguration::new(first_id))
934            .await
935            .expect("could not add device");
936
937        // Now wait until we get an added device event on the first container
938        loop {
939            let event = event_rx.recv().await.unwrap();
940            if let EventType::PendingDevicesChanged {
941                added: Some(added), ..
942            } = event.ty
943            {
944                if !added.is_empty() {
945                    break;
946                }
947            }
948        }
949
950        // Check that this device is the correct one
951        let pending = first_client
952            .get_pending_devices()
953            .await
954            .expect("could not get pending devices");
955        assert!(pending.devices.contains_key(&second_id));
956    }
957
958    #[rstest]
959    #[tokio::test]
960    async fn container_test_delete_pending_device(
961        #[future]
962        #[from(syncthing_setup)]
963        first: (ContainerAsync<GenericImage>, Client),
964        #[future]
965        #[from(syncthing_setup)]
966        second: (ContainerAsync<GenericImage>, Client),
967    ) {
968        let (_first_container, first_client) = first.await;
969        let (_second_container, second_client) = second.await;
970
971        let first_id = first_client
972            .get_id()
973            .await
974            .expect("could not get id of first container");
975        let second_id = second_client
976            .get_id()
977            .await
978            .expect("could not get id of second container");
979
980        // First starts waiting for the event
981        let (event_tx, mut event_rx) = broadcast::channel(10);
982        let first_client_handle = first_client.clone();
983        tokio::spawn(async move {
984            first_client_handle
985                .get_events(event_tx, true)
986                .await
987                .unwrap();
988        });
989
990        // Add the first device to the second
991        second_client
992            .add_device(NewDeviceConfiguration::new(first_id))
993            .await
994            .expect("could not add device");
995
996        // Now wait until we get an added device event on the first container
997        loop {
998            let event = event_rx.recv().await.unwrap();
999            if let EventType::PendingDevicesChanged {
1000                added: Some(added), ..
1001            } = event.ty
1002            {
1003                if !added.is_empty() {
1004                    break;
1005                }
1006            }
1007        }
1008
1009        // Check that this device is the correct one
1010        let pending = first_client
1011            .get_pending_devices()
1012            .await
1013            .expect("could not get pending devices");
1014        assert!(pending.devices.contains_key(&second_id));
1015
1016        first_client
1017            .dismiss_pending_device(&second_id)
1018            .await
1019            .expect("could not delete pending device");
1020
1021        // Now wait until we get a removed device event in the first container
1022        loop {
1023            let event = event_rx.recv().await.unwrap();
1024            if let EventType::PendingDevicesChanged {
1025                removed: Some(removed),
1026                ..
1027            } = event.ty
1028            {
1029                if !removed.is_empty() {
1030                    break;
1031                }
1032            }
1033        }
1034
1035        // Check that the device is no longer there
1036        let pending = first_client
1037            .get_pending_devices()
1038            .await
1039            .expect("could not get pending devices");
1040        assert!(!pending.devices.contains_key(&second_id));
1041        // There shouldn't be any pending device anymore
1042        assert_eq!(pending.devices.len(), 0)
1043    }
1044
1045    #[rstest]
1046    #[tokio::test]
1047    async fn container_test_get_default_device(
1048        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
1049    ) {
1050        let (_container, client) = syncthing_setup.await;
1051
1052        client
1053            .get_default_device()
1054            .await
1055            .expect("could not get default device");
1056    }
1057
1058    #[rstest]
1059    #[tokio::test]
1060    async fn container_test_get_default_folder(
1061        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
1062    ) {
1063        let (_container, client) = syncthing_setup.await;
1064
1065        client
1066            .get_default_folder()
1067            .await
1068            .expect("could not get default folder");
1069    }
1070
1071    #[rstest]
1072    #[tokio::test]
1073    async fn container_test_system_connections(
1074        #[future]
1075        #[from(syncthing_setup)]
1076        first: (ContainerAsync<GenericImage>, Client),
1077        #[future]
1078        #[from(syncthing_setup)]
1079        second: (ContainerAsync<GenericImage>, Client),
1080    ) {
1081        let (_first_container, first_client) = first.await;
1082        let (_second_container, second_client) = second.await;
1083
1084        let first_id = first_client
1085            .get_id()
1086            .await
1087            .expect("could not get id of first container");
1088        let second_id = second_client
1089            .get_id()
1090            .await
1091            .expect("could not get id of second container");
1092
1093        // First starts waiting for the event
1094        let (event_tx, mut event_rx) = broadcast::channel(10);
1095        let first_client_handle = first_client.clone();
1096        tokio::spawn(async move {
1097            first_client_handle
1098                .get_events(event_tx, true)
1099                .await
1100                .unwrap();
1101        });
1102
1103        // Add the first device to the second
1104        second_client
1105            .add_device(NewDeviceConfiguration::new(first_id))
1106            .await
1107            .expect("could not add device");
1108
1109        // Now wait until we get an added device event on the first container
1110        loop {
1111            let event = event_rx.recv().await.unwrap();
1112            if let EventType::PendingDevicesChanged {
1113                added: Some(added), ..
1114            } = event.ty
1115            {
1116                if !added.is_empty() {
1117                    break;
1118                }
1119            }
1120        }
1121
1122        // Check that this device is the correct one
1123        let pending = first_client
1124            .get_pending_devices()
1125            .await
1126            .expect("could not get pending devices");
1127        assert!(pending.devices.contains_key(&second_id));
1128
1129        // First client accepts the device
1130        first_client
1131            .add_device(NewDeviceConfiguration::new(second_id.clone()))
1132            .await
1133            .expect("could not add device");
1134
1135        let first_connections = first_client
1136            .get_connections()
1137            .await
1138            .expect("could not get connections");
1139
1140        assert_eq!(first_connections.connections.len(), 1);
1141        assert!(first_connections.connections.contains_key(&second_id));
1142        assert!(
1143            !first_connections
1144                .connections
1145                .get(&second_id)
1146                .unwrap()
1147                .paused
1148        );
1149    }
1150
1151    #[rstest]
1152    #[tokio::test]
1153    async fn container_test_completion(
1154        #[future]
1155        #[from(syncthing_setup)]
1156        first: (ContainerAsync<GenericImage>, Client),
1157        #[future]
1158        #[from(syncthing_setup)]
1159        second: (ContainerAsync<GenericImage>, Client),
1160    ) {
1161        let (_first_container, first_client) = first.await;
1162        let (_second_container, second_client) = second.await;
1163
1164        let first_id = first_client
1165            .get_id()
1166            .await
1167            .expect("could not get id of first container");
1168        let second_id = second_client
1169            .get_id()
1170            .await
1171            .expect("could not get id of second container");
1172
1173        // First starts waiting for the event
1174        let (event_tx, mut event_rx) = broadcast::channel(10);
1175        let first_client_handle = first_client.clone();
1176        tokio::spawn(async move {
1177            first_client_handle
1178                .get_events(event_tx, true)
1179                .await
1180                .unwrap();
1181        });
1182
1183        // Add the first device to the second
1184        second_client
1185            .add_device(NewDeviceConfiguration::new(first_id.clone()))
1186            .await
1187            .expect("could not add device");
1188
1189        // Now wait until we get an added device event on the first container
1190        loop {
1191            let event = event_rx.recv().await.unwrap();
1192            if let EventType::PendingDevicesChanged {
1193                added: Some(added), ..
1194            } = event.ty
1195            {
1196                if !added.is_empty() {
1197                    break;
1198                }
1199            }
1200        }
1201
1202        // Check that this device is the correct one
1203        let pending = first_client
1204            .get_pending_devices()
1205            .await
1206            .expect("could not get pending devices");
1207        assert!(pending.devices.contains_key(&second_id));
1208
1209        // First client accepts the device
1210        first_client
1211            .add_device(NewDeviceConfiguration::new(second_id.clone()))
1212            .await
1213            .expect("could not add device");
1214
1215        // Share a folder
1216        let folder_id = "this-is-a-new-folder";
1217        let path = "/tmp";
1218
1219        let folder_on_first = NewFolderConfiguration::new(folder_id.to_string(), path.to_string())
1220            .devices(vec![FolderDeviceConfiguration {
1221                device_id: second_id.clone(),
1222                introduced_by: String::new(),
1223                encryption_password: String::new(),
1224            }]);
1225
1226        let folder_on_second = NewFolderConfiguration::new(folder_id.to_string(), path.to_string())
1227            .devices(vec![FolderDeviceConfiguration {
1228                device_id: first_id,
1229                introduced_by: String::new(),
1230                encryption_password: String::new(),
1231            }]);
1232
1233        first_client
1234            .post_folder(folder_on_first)
1235            .await
1236            .expect("could not post folder on first");
1237
1238        second_client
1239            .post_folder(folder_on_second)
1240            .await
1241            .expect("could not post folder on second");
1242
1243        let _total_completion = first_client
1244            .get_completion(None, None)
1245            .await
1246            .expect("could not get completion");
1247
1248        let _device_completion = first_client
1249            .get_completion(None, Some(&second_id))
1250            .await
1251            .expect("could not get completion for device");
1252
1253        let _folder_completion = first_client
1254            .get_completion(Some(folder_id), None)
1255            .await
1256            .expect("could not get completion for folder");
1257    }
1258
1259    #[rstest]
1260    #[tokio::test]
1261    async fn container_test_delete_folder(
1262        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
1263    ) {
1264        let (_container, client) = syncthing_setup.await;
1265        let folder_id = "this-is-a-new-folder";
1266        let path = "/tmp";
1267
1268        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
1269
1270        client
1271            .post_folder(folder)
1272            .await
1273            .expect("could not post folder");
1274
1275        let api_folder = client
1276            .get_folder(folder_id)
1277            .await
1278            .expect("could not get folder");
1279
1280        let num_folders = client
1281            .get_configuration()
1282            .await
1283            .expect("could not get config")
1284            .folders
1285            .len();
1286
1287        assert_eq!(&api_folder.id, folder_id);
1288        assert_eq!(&api_folder.path, path);
1289
1290        client
1291            .delete_folder(folder_id)
1292            .await
1293            .expect("could not delete folder");
1294
1295        let config = client
1296            .get_configuration()
1297            .await
1298            .expect("could not get config");
1299
1300        assert_eq!(config.folders.len(), num_folders - 1);
1301    }
1302
1303    #[rstest]
1304    #[tokio::test]
1305    async fn container_test_delete_device(
1306        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
1307    ) {
1308        let (_container, client) = syncthing_setup.await;
1309
1310        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
1311
1312        client
1313            .post_device(device)
1314            .await
1315            .expect("could not post device");
1316
1317        let api_device = client
1318            .get_device(DEVICE_ID)
1319            .await
1320            .expect("could not get device");
1321
1322        let num_devices = client
1323            .get_configuration()
1324            .await
1325            .expect("could not get config")
1326            .devices
1327            .len();
1328
1329        assert_eq!(&api_device.device_id, DEVICE_ID);
1330
1331        client
1332            .delete_device(DEVICE_ID)
1333            .await
1334            .expect("could not delete folder");
1335
1336        let config = client
1337            .get_configuration()
1338            .await
1339            .expect("could not get config");
1340
1341        assert_eq!(config.devices.len(), num_devices - 1);
1342    }
1343}