tsk/context/
mod.rs

1pub mod docker_client;
2pub mod file_system;
3pub mod git_operations;
4pub mod repository_context;
5pub mod terminal;
6pub mod tsk_client;
7
8use crate::notifications::NotificationClient;
9use crate::storage::XdgDirectories;
10use docker_client::DockerClient;
11use file_system::FileSystemOperations;
12use git_operations::GitOperations;
13use repository_context::RepositoryContext;
14use terminal::TerminalOperations;
15
16use std::sync::Arc;
17use tsk_client::TskClient;
18
19// Re-export terminal trait for tests
20#[cfg(test)]
21pub use terminal::TerminalOperations as TerminalOperationsTrait;
22
23#[derive(Clone)]
24pub struct AppContext {
25    docker_client: Arc<dyn DockerClient>,
26    file_system: Arc<dyn FileSystemOperations>,
27    git_operations: Arc<dyn GitOperations>,
28    notification_client: Arc<dyn NotificationClient>,
29    repository_context: Arc<dyn RepositoryContext>,
30    terminal_operations: Arc<dyn TerminalOperations>,
31    tsk_client: Arc<dyn TskClient>,
32    xdg_directories: Arc<XdgDirectories>,
33}
34
35impl AppContext {
36    pub fn builder() -> AppContextBuilder {
37        AppContextBuilder::new()
38    }
39
40    pub fn docker_client(&self) -> Arc<dyn DockerClient> {
41        Arc::clone(&self.docker_client)
42    }
43
44    pub fn file_system(&self) -> Arc<dyn FileSystemOperations> {
45        Arc::clone(&self.file_system)
46    }
47
48    pub fn git_operations(&self) -> Arc<dyn GitOperations> {
49        Arc::clone(&self.git_operations)
50    }
51
52    pub fn notification_client(&self) -> Arc<dyn NotificationClient> {
53        Arc::clone(&self.notification_client)
54    }
55
56    pub fn repository_context(&self) -> Arc<dyn RepositoryContext> {
57        Arc::clone(&self.repository_context)
58    }
59
60    pub fn terminal_operations(&self) -> Arc<dyn TerminalOperations> {
61        Arc::clone(&self.terminal_operations)
62    }
63
64    pub fn tsk_client(&self) -> Arc<dyn TskClient> {
65        Arc::clone(&self.tsk_client)
66    }
67
68    pub fn xdg_directories(&self) -> Arc<XdgDirectories> {
69        Arc::clone(&self.xdg_directories)
70    }
71}
72
73pub struct AppContextBuilder {
74    docker_client: Option<Arc<dyn DockerClient>>,
75    file_system: Option<Arc<dyn FileSystemOperations>>,
76    git_operations: Option<Arc<dyn GitOperations>>,
77    notification_client: Option<Arc<dyn NotificationClient>>,
78    repository_context: Option<Arc<dyn RepositoryContext>>,
79    terminal_operations: Option<Arc<dyn TerminalOperations>>,
80    tsk_client: Option<Arc<dyn TskClient>>,
81    xdg_directories: Option<Arc<XdgDirectories>>,
82}
83
84impl Default for AppContextBuilder {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl AppContextBuilder {
91    pub fn new() -> Self {
92        Self {
93            docker_client: None,
94            file_system: None,
95            git_operations: None,
96            notification_client: None,
97            repository_context: None,
98            terminal_operations: None,
99            tsk_client: None,
100            xdg_directories: None,
101        }
102    }
103
104    pub fn with_docker_client(mut self, docker_client: Arc<dyn DockerClient>) -> Self {
105        self.docker_client = Some(docker_client);
106        self
107    }
108
109    pub fn with_file_system(mut self, file_system: Arc<dyn FileSystemOperations>) -> Self {
110        self.file_system = Some(file_system);
111        self
112    }
113
114    pub fn with_git_operations(mut self, git_operations: Arc<dyn GitOperations>) -> Self {
115        self.git_operations = Some(git_operations);
116        self
117    }
118
119    pub fn with_notification_client(
120        mut self,
121        notification_client: Arc<dyn NotificationClient>,
122    ) -> Self {
123        self.notification_client = Some(notification_client);
124        self
125    }
126
127    pub fn with_repository_context(
128        mut self,
129        repository_context: Arc<dyn RepositoryContext>,
130    ) -> Self {
131        self.repository_context = Some(repository_context);
132        self
133    }
134
135    pub fn with_terminal_operations(
136        mut self,
137        terminal_operations: Arc<dyn TerminalOperations>,
138    ) -> Self {
139        self.terminal_operations = Some(terminal_operations);
140        self
141    }
142
143    pub fn with_tsk_client(mut self, tsk_client: Arc<dyn TskClient>) -> Self {
144        self.tsk_client = Some(tsk_client);
145        self
146    }
147
148    pub fn with_xdg_directories(mut self, xdg_directories: Arc<XdgDirectories>) -> Self {
149        self.xdg_directories = Some(xdg_directories);
150        self
151    }
152
153    pub fn build(self) -> AppContext {
154        let xdg_directories = self.xdg_directories.unwrap_or_else(|| {
155            let xdg = XdgDirectories::new().expect("Failed to initialize XDG directories");
156            // Ensure directories exist
157            xdg.ensure_directories()
158                .expect("Failed to create XDG directories");
159            Arc::new(xdg)
160        });
161
162        let tsk_client = self.tsk_client.unwrap_or_else(|| {
163            Arc::new(tsk_client::DefaultTskClient::new(xdg_directories.clone()))
164        });
165
166        let docker_client = self
167            .docker_client
168            .unwrap_or_else(|| Arc::new(docker_client::DefaultDockerClient::new()));
169
170        let file_system = self
171            .file_system
172            .unwrap_or_else(|| Arc::new(file_system::DefaultFileSystem));
173
174        let repository_context = self.repository_context.unwrap_or_else(|| {
175            Arc::new(repository_context::DefaultRepositoryContext::new(
176                file_system.clone(),
177            ))
178        });
179
180        AppContext {
181            docker_client,
182            file_system,
183            git_operations: self
184                .git_operations
185                .unwrap_or_else(|| Arc::new(git_operations::DefaultGitOperations)),
186            notification_client: self
187                .notification_client
188                .unwrap_or_else(|| crate::notifications::create_notification_client()),
189            repository_context,
190            terminal_operations: self
191                .terminal_operations
192                .unwrap_or_else(|| Arc::new(terminal::DefaultTerminalOperations::new())),
193            tsk_client,
194            xdg_directories,
195        }
196    }
197}
198
199#[cfg(test)]
200impl AppContext {
201    pub fn new_with_test_docker(docker_client: Arc<dyn DockerClient>) -> Self {
202        AppContextBuilder::new()
203            .with_docker_client(docker_client)
204            .build()
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::test_utils::FixedResponseDockerClient;
212    use std::sync::Arc;
213
214    #[test]
215    fn test_app_context_creation() {
216        let docker_client = Arc::new(FixedResponseDockerClient::default());
217        let app_context = AppContext::builder()
218            .with_docker_client(docker_client)
219            .build();
220
221        // Verify we can get the docker client back
222        let client = app_context.docker_client();
223        assert!(client.as_any().is::<FixedResponseDockerClient>());
224    }
225
226    #[tokio::test]
227    async fn test_app_context_with_tsk_client() {
228        use crate::test_utils::NoOpTskClient;
229
230        let tsk_client = Arc::new(NoOpTskClient);
231        let app_context = AppContext::builder()
232            .with_tsk_client(tsk_client.clone())
233            .build();
234
235        // Verify we can use the TSK client
236        let client = app_context.tsk_client();
237        assert!(!client.is_server_available().await);
238        assert!(client.list_tasks().await.unwrap().is_empty());
239    }
240
241    #[tokio::test]
242    async fn test_app_context_docker_client_usage() {
243        let docker_client = Arc::new(FixedResponseDockerClient::default());
244        let app_context = AppContext::builder()
245            .with_docker_client(docker_client.clone())
246            .build();
247
248        // Test that we can use the docker client through the context
249        let client = app_context.docker_client();
250        let container_id = client
251            .create_container(None, bollard::container::Config::default())
252            .await
253            .unwrap();
254
255        assert_eq!(container_id, "test-container-id");
256    }
257
258    #[test]
259    fn test_app_context_test_constructor() {
260        let docker_client = Arc::new(FixedResponseDockerClient::default());
261        let app_context = AppContext::new_with_test_docker(docker_client);
262
263        // Verify we can get the docker client back
264        let client = app_context.docker_client();
265        assert!(client.as_any().is::<FixedResponseDockerClient>());
266    }
267
268    #[tokio::test]
269    async fn test_app_context_with_file_system() {
270        use crate::context::file_system::tests::MockFileSystem;
271
272        let docker_client = Arc::new(FixedResponseDockerClient::default());
273        let file_system =
274            Arc::new(MockFileSystem::new().with_file("/test/file.txt", "test content"));
275
276        let app_context = AppContext::builder()
277            .with_docker_client(docker_client)
278            .with_file_system(file_system.clone())
279            .build();
280
281        // Verify we can use the file system
282        let fs = app_context.file_system();
283        let content = fs
284            .read_file(std::path::Path::new("/test/file.txt"))
285            .await
286            .unwrap();
287        assert_eq!(content, "test content");
288    }
289
290    #[test]
291    fn test_app_context_with_terminal_operations() {
292        use std::sync::Mutex;
293
294        // Create a mock terminal operations implementation
295        #[derive(Default)]
296        struct MockTerminalOperations {
297            titles: Mutex<Vec<String>>,
298            restore_called: Mutex<bool>,
299        }
300
301        impl TerminalOperations for MockTerminalOperations {
302            fn set_title(&self, title: &str) {
303                self.titles.lock().unwrap().push(title.to_string());
304            }
305
306            fn restore_title(&self) {
307                *self.restore_called.lock().unwrap() = true;
308            }
309        }
310
311        let mock_terminal = Arc::new(MockTerminalOperations::default());
312        let app_context = AppContext::builder()
313            .with_terminal_operations(mock_terminal.clone())
314            .build();
315
316        // Use terminal operations through context
317        let terminal = app_context.terminal_operations();
318        terminal.set_title("Test Title 1");
319        terminal.set_title("Test Title 2");
320        terminal.restore_title();
321
322        // Verify the mock recorded the calls
323        assert_eq!(
324            *mock_terminal.titles.lock().unwrap(),
325            vec!["Test Title 1", "Test Title 2"]
326        );
327        assert!(*mock_terminal.restore_called.lock().unwrap());
328    }
329}