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        events::Event,
10    },
11};
12use reqwest::{StatusCode, header};
13use tokio::sync::broadcast::Sender;
14
15const ADDR: &str = "http://localhost:8384/rest";
16
17/// A `ClientBuilder` can be used to create a `Client` with custom configuration.
18#[must_use]
19pub struct ClientBuilder {
20    base_url: Option<String>,
21    api_key: String,
22}
23
24impl ClientBuilder {
25    /// Constructs a new `ClientBuilder`.
26    /// This is the same as `Client::builder()`.
27    ///
28    /// The API can either be generated in the GUI of Syncthing or set
29    /// in the configuration file under `configuration/gui/apikey`.
30    pub fn new(api_key: impl Into<String>) -> Self {
31        Self {
32            base_url: None,
33            api_key: api_key.into(),
34        }
35    }
36
37    /// Set the syncthing URL to something different than `http://localhost:8384/rest`.
38    pub fn base_url(mut self, url: impl Into<String>) -> Self {
39        self.base_url = Some(url.into());
40        self
41    }
42
43    /// Returns a `Client` that uses this `ClientBuilder` configuration.
44    ///
45    /// # Errors
46    ///
47    /// This method fails if the header cannot be created or the HTTP client
48    /// cannot be initialized.
49    pub fn build(self) -> Result<Client> {
50        let base_url = self.base_url.unwrap_or_else(|| ADDR.to_string());
51
52        let mut headers = header::HeaderMap::new();
53        let mut api_key_header = header::HeaderValue::from_str(&self.api_key)?;
54        api_key_header.set_sensitive(true);
55        headers.insert("X-API-KEY", api_key_header);
56
57        let client = reqwest::Client::builder()
58            .default_headers(headers)
59            .build()?;
60
61        Ok(Client { client, base_url })
62    }
63}
64
65/// Abstraction to interact with the Syncthing API.
66///
67/// The Client has various configuration values to tweak, such as the
68/// URL which is set to `localhost:8384/rest` by default. To configure a `Client`,
69/// use `Client::builder()`.
70#[derive(Clone, Debug)]
71pub struct Client {
72    client: reqwest::Client,
73    base_url: String,
74}
75
76impl Client {
77    /// Creates a new HTTP client, with which the syncthing API can be used.
78    ///
79    /// # Panics
80    ///
81    /// This method panics if the client cannot be initialized.
82    ///
83    /// Use `Client::builder()` if you wish to handle the failure as an `Error`
84    /// instead of panicking.
85    #[must_use]
86    pub fn new(api_key: &str) -> Self {
87        ClientBuilder::new(api_key).build().expect("Client::new()")
88    }
89
90    /// Creates a `ClientBuilder` to configure a `Client`.
91    /// This is the same as `ClientBuilder::new()`
92    ///
93    /// The API can either be generated in the GUI of Syncthing or set
94    /// in the configuration file under `configuration/gui/apikey`.
95    pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
96        ClientBuilder::new(api_key)
97    }
98
99    /// Returns `()` if the syncthing API can be reached.
100    ///
101    /// Use [`health`](crate::client::Client::health) to do the same
102    /// without the need of a valid `api_key`.
103    pub async fn ping(&self) -> Result<()> {
104        log::debug!("GET /system/ping");
105        self.client
106            .get(format!("{}/system/ping", self.base_url))
107            .send()
108            .await?
109            .error_for_status()?;
110
111        Ok(())
112    }
113
114    /// Returns `()` if the syncthing API can be reached.
115    ///
116    /// Use [`ping`](crate::client::Client::ping) to do the same
117    /// but with the requirement of a valid `api_key`.
118    pub async fn health(&self) -> Result<()> {
119        log::debug!("GET /noauth/health");
120        self.client
121            .get(format!("{}/noauth/health", self.base_url))
122            .send()
123            .await?
124            .error_for_status()?;
125
126        Ok(())
127    }
128
129    /// Returns the ID of the current device. This endpoint
130    /// does not require a valid `api_key`.
131    pub async fn get_id(&self) -> Result<String> {
132        log::debug!("GET /noauth/health");
133        Ok(self
134            .client
135            .get(format!("{}/noauth/health", self.base_url))
136            .send()
137            .await?
138            .error_for_status()?
139            .headers()
140            .get("X-Syncthing-Id")
141            .ok_or(Error::HeaderDeviceIDError)?
142            .to_str()
143            .map_err(|_| Error::HeaderParseError)?
144            .to_string())
145    }
146
147    /// Only returns if an error is encountered.
148    /// Transmits every new [event](crate::types::events::Event) over `tx`.
149    /// If `skip_old`, all events before the call to this function do not
150    /// result in a transmission.
151    pub async fn get_events(&self, tx: Sender<Event>, mut skip_old: bool) -> Result<()> {
152        let mut current_id = 0;
153        loop {
154            log::debug!("GET /events");
155            let events: Vec<Event> = self
156                .client
157                .get(format!("{}/events?since={}", self.base_url, current_id))
158                .send()
159                .await?
160                .error_for_status()?
161                .json()
162                .await?;
163
164            log::debug!("received {} new events", events.len());
165            for event in events {
166                current_id = event.id;
167                if !skip_old {
168                    tx.send(event)?;
169                }
170            }
171            log::debug!("current event id is {current_id}");
172            skip_old = false;
173        }
174    }
175
176    /// Returns the entire [`Configuration`]
177    ///
178    /// # Errors
179    ///
180    /// This method fails if the API cannot be reached, the server
181    /// answers with an error code or the JSON cannot be parsed.
182    pub async fn get_configuration(&self) -> Result<Configuration> {
183        log::debug!("GET /config");
184        Ok(self
185            .client
186            .get(format!("{}/config", self.base_url))
187            .send()
188            .await?
189            .error_for_status()?
190            .json()
191            .await?)
192    }
193
194    /// Posts a folder. If the folder already exists, it is replaced,
195    /// otherwise a new one is added.
196    ///
197    /// Use [`add_folder`](crate::client::Client::add_folder) if the operation
198    /// should fail if a folder with the same ID already exists.
199    pub async fn post_folder(&self, folder: impl Into<NewFolderConfiguration>) -> Result<()> {
200        let folder = folder.into();
201        log::debug!("POST /config/folders {:?}", folder);
202        self.client
203            .post(format!("{}/config/folders", self.base_url))
204            .json(&folder)
205            .send()
206            .await?
207            .error_for_status()?;
208
209        Ok(())
210    }
211
212    /// Adds a new folder. If the folder already exists, a
213    /// [`DuplicateFolderError`](crate::error::Error::DuplicateFolderError) is returned.
214    /// This requires an additional check against the API.
215    ///
216    /// Use [`post_folder`](crate::client::Client::post_folder) if the operation
217    /// should blindly set the folder.
218    pub async fn add_folder(&self, folder: impl Into<NewFolderConfiguration>) -> Result<()> {
219        let folder = folder.into();
220        match self.get_folder(folder.get_id()).await {
221            Ok(_) => return Err(Error::DuplicateFolderError),
222            Err(Error::UnknownFolderError) => (),
223            Err(e) => return Err(e),
224        }
225        self.post_folder(folder).await
226    }
227
228    /// Gets the configuration for the folder with the ID `folder_id`. Explicitly
229    /// returns a [`UnknownFolderError`](crate::error::Error::UnknownFolderError)
230    /// if no folder with `folder_id` exists.
231    pub async fn get_folder(&self, folder_id: &str) -> Result<FolderConfiguration> {
232        log::debug!("GET /config/folders/{}", folder_id);
233        let response = self
234            .client
235            .get(format!("{}/config/folders/{}", self.base_url, folder_id))
236            .send()
237            .await?;
238
239        if response.status() == StatusCode::NOT_FOUND {
240            // TODO check that really the folder ID is causing that
241            Err(Error::UnknownFolderError)
242        } else {
243            Ok(response.error_for_status()?.json().await?)
244        }
245    }
246
247    /// Posts a device. If the device already exists, it is replaced,
248    /// otherwise a new one is added.
249    ///
250    /// Use [`add_device`](crate::client::Client::add_device) if the operation
251    /// should fail if a device with the same ID already exists.
252    pub async fn post_device(&self, device: impl Into<NewDeviceConfiguration>) -> Result<()> {
253        let device = device.into();
254        log::debug!("POST /config/devices {:?}", device);
255        self.client
256            .post(format!("{}/config/devices", self.base_url))
257            .json(&device)
258            .send()
259            .await?
260            .error_for_status()?;
261
262        Ok(())
263    }
264
265    /// Adds a new device. If the device already exists, a
266    /// [`DuplicateDeviceError`](crate::error::Error::DuplicateDeviceError) is returned.
267    /// This requires an additional check against the API.
268    ///
269    /// Use [`post_device`](crate::client::Client::post_device) if the operation
270    /// should blindly set the device.
271    pub async fn add_device(&self, device: impl Into<NewDeviceConfiguration>) -> Result<()> {
272        let device = device.into();
273        match self.get_device(device.get_device_id()).await {
274            Ok(_) => return Err(Error::DuplicateDeviceError),
275            Err(Error::UnknownDeviceError) => (),
276            Err(e) => return Err(e),
277        }
278        self.post_device(device).await
279    }
280
281    /// Gets the configuration for the device with the ID `device_id`.
282    pub async fn get_device(&self, device_id: &str) -> Result<DeviceConfiguration> {
283        log::debug!("GET /config/devices/{}", device_id);
284        let response = self
285            .client
286            .get(format!("{}/config/devices/{}", self.base_url, device_id))
287            .send()
288            .await?;
289
290        if response.status() == StatusCode::NOT_FOUND {
291            // TODO check that really the device ID is causing that
292            Err(Error::UnknownDeviceError)
293        } else {
294            Ok(response.error_for_status()?.json().await?)
295        }
296    }
297
298    /// Gets a list of all pending remote devices which have tried to connect but
299    /// are not in our configuration yet.
300    pub async fn get_pending_devices(&self) -> Result<PendingDevices> {
301        log::debug!("GET /cluster/pending/devices");
302        Ok(self
303            .client
304            .get(format!("{}/cluster/pending/devices", self.base_url))
305            .send()
306            .await?
307            .error_for_status()?
308            .json()
309            .await?)
310    }
311
312    /// Gets all folders which remote devices have offered to us, but are not yet shared
313    /// from our instance to them or are not present on our instance.
314    pub async fn get_pending_folders(&self) -> Result<PendingFolders> {
315        log::debug!("GET /cluster/pending/folders");
316        Ok(self
317            .client
318            .get(format!("{}/cluster/pending/folders", self.base_url))
319            .send()
320            .await?
321            .error_for_status()?
322            .json()
323            .await?)
324    }
325
326    /// Remove record about pending remote device with ID `device_id` which tried to connect.
327    ///
328    /// This is not permanent, use `ignore_device` instead.
329    pub async fn delete_pending_device(&self, device_id: &str) -> Result<()> {
330        log::debug!("DELETE /cluster/pending/devices?device={device_id}");
331        self.client
332            .delete(format!(
333                "{}/cluster/pending/devices?device={}",
334                self.base_url, device_id
335            ))
336            .send()
337            .await?
338            .error_for_status()?;
339
340        Ok(())
341    }
342
343    /// Remove record about pending remote folder with ID `folder_id`. An optional `device_id`
344    /// can be passed as argument to only remove the pending remote from that device, otherwise
345    /// the folder will be removed as pending for all devices.
346    ///
347    /// This is not permanent, use `ignore_folder` instead.
348    pub async fn delete_pending_folder(
349        &self,
350        folder_id: &str,
351        device_id: Option<&str>,
352    ) -> Result<()> {
353        let device_str = match device_id {
354            Some(device_id) => format!("?device={}", device_id),
355            None => String::new(),
356        };
357        log::debug!("DELETE /clusterpending/folders?folder={folder_id}{device_str}");
358        self.client
359            .delete(format!(
360                "{}/cluster/pending/folders?folder={}{}",
361                self.base_url, folder_id, device_str
362            ))
363            .send()
364            .await?
365            .error_for_status()?;
366
367        Ok(())
368    }
369
370    /// Returns a template device configuration with all default values,
371    /// which only requires a unique device ID to be instantiated.
372    pub async fn get_default_device(&self) -> Result<DeviceConfiguration> {
373        log::debug!("GET /config/defaults/device");
374        Ok(self
375            .client
376            .get(format!("{}/config/defaults/device", self.base_url))
377            .send()
378            .await?
379            .error_for_status()?
380            .json()
381            .await?)
382    }
383
384    /// Returns a template folder configuration with all default values,
385    /// which only requires a unique folder ID to be instantiated.
386    pub async fn get_default_folder(&self) -> Result<FolderConfiguration> {
387        log::debug!("GET /config/defaults/folder");
388        Ok(self
389            .client
390            .get(format!("{}/config/defaults/folder", self.base_url))
391            .send()
392            .await?
393            .error_for_status()?
394            .json()
395            .await?)
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use crate::types::events::EventType;
402
403    use super::*;
404
405    use httpmock::prelude::*;
406    use testcontainers::{
407        ContainerAsync, GenericImage, ImageExt,
408        core::{ContainerPort::Tcp, WaitFor},
409        runners::AsyncRunner,
410    };
411    use tokio::sync::broadcast;
412
413    use rstest::*;
414
415    // Example device id from the docs
416    const DEVICE_ID: &str = "MFZWI3D-BONSGYC-YLTMRWG-C43ENR5-QXGZDMM-FZWI3DP-BONSGYY-LTMRWAD";
417
418    #[fixture]
419    async fn syncthing_setup() -> (ContainerAsync<GenericImage>, Client) {
420        let api_key = "foobar";
421        let container = GenericImage::new("syncthing/syncthing", "latest")
422            .with_exposed_port(Tcp(8384))
423            .with_wait_for(WaitFor::message_on_stdout("GUI and API listening on "))
424            .with_env_var("STGUIAPIKEY", api_key)
425            .start()
426            .await
427            .expect("failed to start syncthing container");
428
429        let host = container
430            .get_host()
431            .await
432            .expect("could not get syncthing host");
433        let port = container
434            .get_host_port_ipv4(8384)
435            .await
436            .expect("could not get syncthing port");
437
438        let url = format!("http://{host}:{port}/rest");
439
440        let client = ClientBuilder::new(api_key).base_url(url).build().unwrap();
441
442        (container, client)
443    }
444
445    #[test]
446    fn test_new() {
447        let client = Client::new("foo");
448
449        assert_eq!(client.base_url, "http://localhost:8384/rest");
450    }
451
452    /// Simple ping to a running server should just return Ok(())
453    #[tokio::test]
454    async fn test_ping() {
455        let server = MockServer::start();
456
457        let ping_mock = server.mock(|when, then| {
458            when.method(GET).path("/system/ping");
459            then.status(200)
460                .header("content-type", "application/json")
461                .body(r#"{"ping": "pong"}"#);
462        });
463
464        let client = ClientBuilder::new("")
465            .base_url(server.base_url())
466            .build()
467            .unwrap();
468
469        let result = client.ping().await;
470        ping_mock.assert();
471
472        assert!(result.is_ok());
473    }
474
475    /// Simple test ensuring that a single event in the past is correctly
476    /// transmitted.
477    #[tokio::test]
478    async fn test_single_event() {
479        let server = MockServer::start();
480
481        let event_mock = server.mock(|when, then| {
482            when.method(GET).path("/events");
483            then.status(200)
484                .header("content-type", "application/json")
485                .body(
486                    r#"
487[
488  {
489    "id": 1,
490    "globalID": 1,
491    "time": "2025-05-07T17:05:44.514050967+02:00",
492    "type": "Starting",
493    "data": {
494      "home": "/home/user/.config/syncthing",
495      "myID": "XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX"
496    }
497  }
498]
499"#,
500                );
501        });
502
503        let client = ClientBuilder::new("")
504            .base_url(server.base_url())
505            .build()
506            .unwrap();
507
508        let (tx, mut rx) = broadcast::channel(1);
509
510        // Start transmitting events on a separate thread
511        tokio::spawn(async move {
512            let result = client.get_events(tx, false).await;
513            unreachable!("get_events should not have returned: {:?}", result);
514        });
515
516        let event = rx.recv().await;
517        assert!(event_mock.hits() > 0);
518        assert!(event.is_ok());
519        assert_eq!(event.unwrap().ty, EventType::Starting {})
520    }
521
522    #[tokio::test]
523    async fn container_test_health() {
524        // Create container by hand, so we don't know the API key. This is okay
525        // as the health endpoint should work anyway
526        let container = GenericImage::new("syncthing/syncthing", "latest")
527            .with_exposed_port(Tcp(8384))
528            .with_wait_for(WaitFor::message_on_stdout("GUI and API listening on "))
529            .start()
530            .await
531            .expect("failed to start syncthing container");
532
533        let host = container
534            .get_host()
535            .await
536            .expect("could not get syncthing host");
537        let port = container
538            .get_host_port_ipv4(8384)
539            .await
540            .expect("could not get syncthing port");
541
542        let url = format!("http://{host}:{port}/rest");
543
544        let client = ClientBuilder::new("idk").base_url(url).build().unwrap();
545
546        client.health().await.unwrap();
547    }
548
549    #[tokio::test]
550    async fn container_test_id() {
551        // Create container by hand, so we don't know the API key. This is okay
552        // as the id endpoint should work anyway
553        let container = GenericImage::new("syncthing/syncthing", "latest")
554            .with_exposed_port(Tcp(8384))
555            .with_wait_for(WaitFor::message_on_stdout("GUI and API listening on "))
556            .start()
557            .await
558            .expect("failed to start syncthing container");
559
560        let host = container
561            .get_host()
562            .await
563            .expect("could not get syncthing host");
564        let port = container
565            .get_host_port_ipv4(8384)
566            .await
567            .expect("could not get syncthing port");
568
569        let url = format!("http://{host}:{port}/rest");
570
571        let client = ClientBuilder::new("idk").base_url(url).build().unwrap();
572
573        client.get_id().await.unwrap();
574    }
575
576    #[rstest]
577    #[tokio::test]
578    async fn container_test_ping(
579        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
580    ) {
581        let (_container, client) = syncthing_setup.await;
582
583        client.ping().await.unwrap();
584    }
585
586    #[rstest]
587    #[tokio::test]
588    async fn container_test_get_config(
589        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
590    ) {
591        let (_container, client) = syncthing_setup.await;
592
593        client
594            .get_configuration()
595            .await
596            .expect("could not get config");
597    }
598
599    #[rstest]
600    #[tokio::test]
601    async fn container_test_post_folder(
602        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
603    ) {
604        let (_container, client) = syncthing_setup.await;
605        let folder_id = "this-is-a-new-folder";
606        let path = "/tmp";
607
608        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
609
610        client
611            .post_folder(folder)
612            .await
613            .expect("could not post folder");
614
615        let api_folder = client
616            .get_folder(folder_id)
617            .await
618            .expect("could not get folder");
619
620        assert_eq!(&api_folder.id, folder_id);
621        assert_eq!(&api_folder.path, path);
622    }
623
624    #[rstest]
625    #[tokio::test]
626    async fn container_test_add_folder(
627        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
628    ) {
629        let (_container, client) = syncthing_setup.await;
630        let folder_id = "this-is-a-new-folder";
631        let path = "/tmp";
632
633        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
634
635        client
636            .add_folder(folder)
637            .await
638            .expect("could not add folder");
639
640        let api_folder = client
641            .get_folder(folder_id)
642            .await
643            .expect("could not get folder");
644
645        assert_eq!(&api_folder.id, folder_id);
646        assert_eq!(&api_folder.path, path);
647    }
648
649    #[rstest]
650    #[tokio::test]
651    #[should_panic(expected = "DuplicateFolderError")]
652    async fn container_test_add_folder_twice_panic(
653        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
654    ) {
655        let (_container, client) = syncthing_setup.await;
656        let folder_id = "this-is-a-new-folder";
657        let path = "/tmp";
658
659        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
660
661        client
662            .add_folder(folder)
663            .await
664            .expect("could not add folder");
665
666        // "Accidentally" overwrite our folder
667        let duplicate_path = "/usr";
668        let duplicate_folder =
669            NewFolderConfiguration::new(folder_id.to_string(), duplicate_path.to_string());
670
671        client
672            .add_folder(duplicate_folder)
673            .await
674            .expect("could not add folder")
675    }
676
677    #[rstest]
678    #[tokio::test]
679    async fn container_test_post_folder_twice(
680        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
681    ) {
682        let (_container, client) = syncthing_setup.await;
683        let folder_id = "this-is-a-new-folder";
684        let path = "/tmp";
685
686        let folder = NewFolderConfiguration::new(folder_id.to_string(), path.to_string());
687
688        client
689            .add_folder(folder)
690            .await
691            .expect("could not add folder");
692
693        // "Accidentally" overwrite our folder
694        let duplicate_path = "/usr";
695        let duplicate_folder =
696            NewFolderConfiguration::new(folder_id.to_string(), duplicate_path.to_string());
697
698        client
699            .post_folder(duplicate_folder)
700            .await
701            .expect("could not post folder");
702
703        let api_folder = client
704            .get_folder(folder_id)
705            .await
706            .expect("could not get folder");
707
708        assert_eq!(&api_folder.id, folder_id);
709        assert_eq!(&api_folder.path, duplicate_path);
710    }
711
712    #[rstest]
713    #[tokio::test]
714    async fn container_test_post_device(
715        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
716    ) {
717        let (_container, client) = syncthing_setup.await;
718
719        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
720
721        client
722            .post_device(device)
723            .await
724            .expect("could not post device");
725
726        let api_device = client
727            .get_device(DEVICE_ID)
728            .await
729            .expect("could not get device");
730
731        assert_eq!(&api_device.device_id, DEVICE_ID);
732    }
733
734    #[rstest]
735    #[tokio::test]
736    async fn container_test_add_device(
737        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
738    ) {
739        let (_container, client) = syncthing_setup.await;
740
741        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
742
743        client
744            .add_device(device)
745            .await
746            .expect("could not add device");
747
748        let api_device = client
749            .get_device(DEVICE_ID)
750            .await
751            .expect("could not get device");
752
753        assert_eq!(&api_device.device_id, DEVICE_ID);
754    }
755
756    #[rstest]
757    #[tokio::test]
758    #[should_panic(expected = "DuplicateDeviceError")]
759    async fn container_test_add_device_twice_panic(
760        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
761    ) {
762        let (_container, client) = syncthing_setup.await;
763
764        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
765
766        client
767            .add_device(device)
768            .await
769            .expect("could not add device");
770
771        // "Accidentally" overwrite our device
772        let duplicate_device = NewDeviceConfiguration::new(DEVICE_ID.to_string());
773
774        client
775            .add_device(duplicate_device)
776            .await
777            .expect("could not add device")
778    }
779
780    #[rstest]
781    #[tokio::test]
782    async fn container_test_post_device_twice(
783        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
784    ) {
785        let (_container, client) = syncthing_setup.await;
786        let name = "original";
787
788        let device = NewDeviceConfiguration::new(DEVICE_ID.to_string()).name(name.to_string());
789
790        client
791            .add_device(device)
792            .await
793            .expect("could not add device");
794
795        // "Accidentally" overwrite our device
796        let duplicate_name = "duplicate";
797        let duplicate_device =
798            NewDeviceConfiguration::new(DEVICE_ID.to_string()).name(duplicate_name.to_string());
799
800        client
801            .post_device(duplicate_device)
802            .await
803            .expect("could not post device");
804
805        let api_device = client
806            .get_device(DEVICE_ID)
807            .await
808            .expect("could not get device");
809
810        assert_eq!(&api_device.device_id, DEVICE_ID);
811        assert_eq!(&api_device.name, duplicate_name);
812    }
813
814    #[rstest]
815    #[tokio::test]
816    async fn container_test_pending_device(
817        #[future]
818        #[from(syncthing_setup)]
819        first: (ContainerAsync<GenericImage>, Client),
820        #[future]
821        #[from(syncthing_setup)]
822        second: (ContainerAsync<GenericImage>, Client),
823    ) {
824        let (_first_container, first_client) = first.await;
825        let (_second_container, second_client) = second.await;
826
827        let first_id = first_client
828            .get_id()
829            .await
830            .expect("could not get id of first container");
831        let second_id = second_client
832            .get_id()
833            .await
834            .expect("could not get id of second container");
835
836        // First starts waiting for the event
837        let (event_tx, mut event_rx) = broadcast::channel(10);
838        let first_client_handle = first_client.clone();
839        tokio::spawn(async move {
840            first_client_handle
841                .get_events(event_tx, true)
842                .await
843                .unwrap();
844        });
845
846        // Add the first device to the second
847        second_client
848            .add_device(NewDeviceConfiguration::new(first_id))
849            .await
850            .expect("could not add device");
851
852        // Now wait until we get an added device event on the first container
853        loop {
854            let event = event_rx.recv().await.unwrap();
855            match event.ty {
856                EventType::PendingDevicesChanged {
857                    added: Some(added), ..
858                } => {
859                    if !added.is_empty() {
860                        break;
861                    }
862                }
863                // Skip other events
864                _ => {}
865            }
866        }
867
868        // Check that this device is the correct one
869        let pending = first_client
870            .get_pending_devices()
871            .await
872            .expect("could not get pending devices");
873        assert!(pending.devices.contains_key(&second_id));
874    }
875
876    #[rstest]
877    #[tokio::test]
878    async fn container_test_delete_pending_device(
879        #[future]
880        #[from(syncthing_setup)]
881        first: (ContainerAsync<GenericImage>, Client),
882        #[future]
883        #[from(syncthing_setup)]
884        second: (ContainerAsync<GenericImage>, Client),
885    ) {
886        let (_first_container, first_client) = first.await;
887        let (_second_container, second_client) = second.await;
888
889        let first_id = first_client
890            .get_id()
891            .await
892            .expect("could not get id of first container");
893        let second_id = second_client
894            .get_id()
895            .await
896            .expect("could not get id of second container");
897
898        // First starts waiting for the event
899        let (event_tx, mut event_rx) = broadcast::channel(10);
900        let first_client_handle = first_client.clone();
901        tokio::spawn(async move {
902            first_client_handle
903                .get_events(event_tx, true)
904                .await
905                .unwrap();
906        });
907
908        // Add the first device to the second
909        second_client
910            .add_device(NewDeviceConfiguration::new(first_id))
911            .await
912            .expect("could not add device");
913
914        // Now wait until we get an added device event on the first container
915        loop {
916            let event = event_rx.recv().await.unwrap();
917            match event.ty {
918                EventType::PendingDevicesChanged {
919                    added: Some(added), ..
920                } => {
921                    if !added.is_empty() {
922                        break;
923                    }
924                }
925                // Skip other events
926                _ => {}
927            }
928        }
929
930        // Check that this device is the correct one
931        let pending = first_client
932            .get_pending_devices()
933            .await
934            .expect("could not get pending devices");
935        assert!(pending.devices.contains_key(&second_id));
936
937        first_client
938            .delete_pending_device(&second_id)
939            .await
940            .expect("could not delete pending device");
941
942        // Now wait until we get a removed device event in the first container
943        loop {
944            let event = event_rx.recv().await.unwrap();
945            match event.ty {
946                EventType::PendingDevicesChanged {
947                    removed: Some(removed),
948                    ..
949                } => {
950                    if !removed.is_empty() {
951                        break;
952                    }
953                }
954                // Skip other events
955                _ => {}
956            }
957        }
958
959        // Check that the device is no longer there
960        let pending = first_client
961            .get_pending_devices()
962            .await
963            .expect("could not get pending devices");
964        assert!(!pending.devices.contains_key(&second_id));
965        // There shouldn't be any pending device anymore
966        assert_eq!(pending.devices.len(), 0)
967    }
968
969    #[rstest]
970    #[tokio::test]
971    async fn container_test_get_default_device(
972        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
973    ) {
974        let (_container, client) = syncthing_setup.await;
975
976        client
977            .get_default_device()
978            .await
979            .expect("could not get default device");
980    }
981
982    #[rstest]
983    #[tokio::test]
984    async fn container_test_get_default_folder(
985        #[future] syncthing_setup: (ContainerAsync<GenericImage>, Client),
986    ) {
987        let (_container, client) = syncthing_setup.await;
988
989        client
990            .get_default_folder()
991            .await
992            .expect("could not get default folder");
993    }
994}