1use crate::services::MultiRelayMessageManager;
2use crate::services::blob_store::MultiRelayBlobService;
3use crate::{FileStorage, RelayClient, SessionManager};
4use async_broadcast;
5use eyeball::SharedObservable;
6use std::collections::BTreeMap;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9use tokio::task::JoinHandle;
10use zoe_client_storage::SqliteMessageStorage;
11use zoe_wire_protocol::KeyId;
12
13#[cfg(not(feature = "frb-api"))]
14mod api;
15#[cfg(feature = "frb-api")]
16pub mod api; mod builder;
18mod info;
19mod secret;
20
21pub use builder::ClientBuilder;
22pub use info::{
23 OverallConnectionStatus, RelayConnectionInfo, RelayConnectionStatus, RelayInfo,
24 RelayStatusUpdate,
25};
26pub use secret::ClientSecret;
27
28pub use api::relay::RelayConnectionHandle;
30
31#[cfg(feature = "frb-api")]
32use flutter_rust_bridge::frb;
33
34pub type ZoeClientStorage = SqliteMessageStorage;
35pub type ZoeClientSessionManager = SessionManager<ZoeClientStorage, ZoeClientMessageManager>;
36pub type ZoeClientMessageManager = MultiRelayMessageManager<ZoeClientStorage>;
37pub type ZoeClientBlobService = MultiRelayBlobService<ZoeClientStorage>;
38pub type ZoeClientFileStorage = FileStorage<ZoeClientBlobService>;
39#[derive(Clone)]
40#[cfg_attr(feature = "frb-api", frb(opaque))]
41pub struct Client {
42 pub(crate) client_secret: Arc<ClientSecret>,
43 pub(crate) fs: Arc<ZoeClientFileStorage>,
44 pub(crate) storage: Arc<ZoeClientStorage>,
46 pub(crate) message_manager: Arc<ZoeClientMessageManager>,
47 pub(crate) blob_service: Arc<ZoeClientBlobService>,
48 pub(crate) relay_connections: Arc<RwLock<BTreeMap<KeyId, RelayClient>>>,
49 pub(crate) relay_info: Arc<RwLock<BTreeMap<KeyId, RelayConnectionInfo>>>,
50 pub(crate) encryption_key: [u8; 32],
51 pub(crate) client_secret_observable: SharedObservable<ClientSecret>,
53 pub(crate) relay_status_sender: Arc<async_broadcast::Sender<RelayStatusUpdate>>,
55 _relay_status_keeper: Arc<async_broadcast::InactiveReceiver<RelayStatusUpdate>>,
58 pub(crate) connection_monitors: Arc<RwLock<BTreeMap<KeyId, JoinHandle<()>>>>,
60 pub(crate) session_manager: Arc<ZoeClientSessionManager>,
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
68 use tempfile::TempDir;
69 use tokio::fs;
70 use zoe_app_primitives::RelayAddress;
71 use zoe_wire_protocol::KeyPair;
72
73 async fn create_test_client_offline() -> (Client, TempDir, TempDir) {
74 let media_temp_dir = TempDir::new().unwrap();
75 let db_temp_dir = TempDir::new().unwrap();
76
77 let mut builder = ClientBuilder::default();
78 builder.media_storage_dir_pathbuf(media_temp_dir.path().to_path_buf());
79 builder.db_storage_dir_pathbuf(db_temp_dir.path().to_path_buf());
80 builder.encryption_key([42u8; 32]);
81 builder.autoconnect(false); let client = builder.build().await.unwrap();
84 (client, media_temp_dir, db_temp_dir)
85 }
86
87 #[tokio::test]
88 async fn test_client_file_storage_offline() {
89 let (client, media_temp_dir, _db_temp_dir) = create_test_client_offline().await;
90
91 let test_file_path = media_temp_dir.path().join("test_file.txt");
93 let test_content = b"Hello, offline world!";
94 fs::write(&test_file_path, test_content).await.unwrap();
95
96 let file_ref = client.store_file(test_file_path.clone()).await.unwrap();
98 assert!(!file_ref.blob_hash.is_empty());
99
100 assert!(client.has_file(&file_ref).await.unwrap());
102
103 let retrieved_content = client.retrieve_file_bytes(&file_ref).await.unwrap();
105 assert_eq!(retrieved_content, test_content);
106
107 let output_path = media_temp_dir.path().join("retrieved_file.txt");
109 client
110 .retrieve_file(&file_ref, output_path.clone())
111 .await
112 .unwrap();
113
114 let disk_content = fs::read(&output_path).await.unwrap();
115 assert_eq!(disk_content, test_content);
116 }
117
118 #[tokio::test]
119 async fn test_client_relay_management_offline() {
120 let (client, _media_temp_dir, _db_temp_dir) = create_test_client_offline().await;
121
122 assert!(client.get_relay_status().await.unwrap().is_empty());
124 assert!(!client.has_connected_relays().await);
125
126 let relay_keypair = KeyPair::generate(&mut rand::thread_rng());
128 let relay_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080);
129 let relay_address =
130 RelayAddress::new(relay_keypair.public_key()).with_address(relay_addr.into());
131
132 let result = client.add_relay(relay_address).await;
133 assert!(result.is_err());
134
135 let status = client.get_relay_status().await.unwrap();
137 assert_eq!(status.len(), 1);
138 assert!(matches!(
139 status[0].status,
140 RelayConnectionStatus::Failed { .. }
141 ));
142 }
143
144 #[tokio::test]
145 async fn test_client_builder_validation() {
146 let mut builder = ClientBuilder::default();
148 builder.db_storage_dir_pathbuf(TempDir::new().unwrap().path().to_path_buf());
149 let result = builder.build().await;
150 assert!(result.is_err());
151
152 let mut builder = ClientBuilder::default();
154 builder.media_storage_dir_pathbuf(TempDir::new().unwrap().path().to_path_buf());
155 let result = builder.build().await;
156 assert!(result.is_err());
157
158 let mut builder = ClientBuilder::default();
160 builder.media_storage_dir_pathbuf(TempDir::new().unwrap().path().to_path_buf());
161 builder.db_storage_dir_pathbuf(TempDir::new().unwrap().path().to_path_buf());
162 builder.autoconnect(true);
163 let result = builder.build().await;
164 assert!(result.is_err());
165 }
166
167 #[tokio::test]
168 async fn test_client_public_key_access() {
169 let (client, _media_temp_dir, _db_temp_dir) = create_test_client_offline().await;
170
171 let public_key = client.public_key();
172 let keypair = client.keypair();
173
174 assert_eq!(public_key, keypair.public_key());
176 assert_eq!(client.id_hex(), hex::encode(public_key.id()));
177 }
178
179 #[tokio::test]
180 async fn test_client_close() {
181 let (client, _media_temp_dir, _db_temp_dir) = create_test_client_offline().await;
182
183 client.close().await;
185 }
186}