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#[must_use]
21pub struct ClientBuilder {
22 base_url: Option<String>,
23 api_key: String,
24}
25
26impl ClientBuilder {
27 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 pub fn base_url(mut self, url: impl Into<String>) -> Self {
41 self.base_url = Some(url.into());
42 self
43 }
44
45 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#[derive(Clone, Debug)]
73pub struct Client {
74 client: reqwest::Client,
75 base_url: String,
76}
77
78impl Client {
79 #[must_use]
88 pub fn new(api_key: &str) -> Self {
89 ClientBuilder::new(api_key).build().expect("Client::new()")
90 }
91
92 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
98 ClientBuilder::new(api_key)
99 }
100
101 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 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 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 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 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 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 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 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 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 Err(Error::UnknownFolderError)
257 } else {
258 Ok(response.error_for_status()?.json().await?)
259 }
260 }
261
262 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 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 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 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 Err(Error::UnknownDeviceError)
319 } else {
320 Ok(response.error_for_status()?.json().await?)
321 }
322 }
323
324 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 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 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 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 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 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 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 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 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 #[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 #[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 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 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 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 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 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 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 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 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 second_client
933 .add_device(NewDeviceConfiguration::new(first_id))
934 .await
935 .expect("could not add device");
936
937 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 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 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 second_client
992 .add_device(NewDeviceConfiguration::new(first_id))
993 .await
994 .expect("could not add device");
995
996 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 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 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 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 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 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 second_client
1105 .add_device(NewDeviceConfiguration::new(first_id))
1106 .await
1107 .expect("could not add device");
1108
1109 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 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
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 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 second_client
1185 .add_device(NewDeviceConfiguration::new(first_id.clone()))
1186 .await
1187 .expect("could not add device");
1188
1189 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 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
1211 .add_device(NewDeviceConfiguration::new(second_id.clone()))
1212 .await
1213 .expect("could not add device");
1214
1215 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}