docker_wrapper/command/
logout.rs

1//! Docker logout command implementation
2//!
3//! This module provides functionality to log out from Docker registries.
4//! It supports logging out from specific registries or using the daemon default.
5
6use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use std::fmt;
10
11/// Command for logging out from Docker registries
12///
13/// The `LogoutCommand` provides a builder pattern for constructing Docker logout commands
14/// to remove stored authentication credentials for Docker registries.
15///
16/// # Examples
17///
18/// ```rust
19/// use docker_wrapper::LogoutCommand;
20///
21/// // Logout from default registry (daemon-defined)
22/// let logout = LogoutCommand::new();
23///
24/// // Logout from specific registry
25/// let logout = LogoutCommand::new()
26///     .server("my-registry.com");
27/// ```
28#[derive(Debug, Clone)]
29pub struct LogoutCommand {
30    /// Registry server URL (None for daemon default)
31    server: Option<String>,
32    /// Command executor for running the command
33    pub executor: CommandExecutor,
34}
35
36/// Output from a logout command execution
37///
38/// Contains the raw output from the Docker logout command and provides
39/// convenience methods for checking logout status.
40#[derive(Debug, Clone)]
41pub struct LogoutOutput {
42    /// Raw output from the Docker command
43    pub output: CommandOutput,
44}
45
46impl LogoutCommand {
47    /// Creates a new logout command
48    ///
49    /// By default, logs out from the daemon-defined default registry
50    ///
51    /// # Examples
52    ///
53    /// ```rust
54    /// use docker_wrapper::LogoutCommand;
55    ///
56    /// let logout = LogoutCommand::new();
57    /// ```
58    #[must_use]
59    pub fn new() -> Self {
60        Self {
61            server: None,
62            executor: CommandExecutor::default(),
63        }
64    }
65
66    /// Sets the registry server to logout from
67    ///
68    /// If not specified, uses the daemon-defined default registry
69    ///
70    /// # Arguments
71    ///
72    /// * `server` - The registry server URL
73    ///
74    /// # Examples
75    ///
76    /// ```rust
77    /// use docker_wrapper::LogoutCommand;
78    ///
79    /// let logout = LogoutCommand::new()
80    ///     .server("gcr.io");
81    /// ```
82    #[must_use]
83    pub fn server(mut self, server: impl Into<String>) -> Self {
84        self.server = Some(server.into());
85        self
86    }
87
88    /// Sets a custom command executor
89    ///
90    /// # Arguments
91    ///
92    /// * `executor` - Custom command executor
93    #[must_use]
94    pub fn executor(mut self, executor: CommandExecutor) -> Self {
95        self.executor = executor;
96        self
97    }
98
99    /// Gets the server (if set)
100    #[must_use]
101    pub fn get_server(&self) -> Option<&str> {
102        self.server.as_deref()
103    }
104
105    /// Get a reference to the command executor
106    #[must_use]
107    pub fn get_executor(&self) -> &CommandExecutor {
108        &self.executor
109    }
110
111    /// Get a mutable reference to the command executor
112    #[must_use]
113    pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
114        &mut self.executor
115    }
116}
117
118impl Default for LogoutCommand {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl LogoutOutput {
125    /// Returns true if the logout was successful
126    #[must_use]
127    pub fn success(&self) -> bool {
128        self.output.success
129    }
130
131    /// Returns true if the output indicates successful logout
132    #[must_use]
133    pub fn is_logged_out(&self) -> bool {
134        self.success()
135            && (self.output.stdout.contains("Removing login credentials")
136                || self.output.stdout.contains("Not logged in")
137                || self.output.stdout.is_empty() && self.output.stderr.is_empty())
138    }
139
140    /// Gets any warning messages from the logout output
141    #[must_use]
142    pub fn warnings(&self) -> Vec<&str> {
143        self.output
144            .stderr
145            .lines()
146            .filter(|line| line.to_lowercase().contains("warning"))
147            .collect()
148    }
149
150    /// Gets any info messages from the logout output
151    #[must_use]
152    pub fn info_messages(&self) -> Vec<&str> {
153        self.output
154            .stdout
155            .lines()
156            .filter(|line| !line.trim().is_empty())
157            .collect()
158    }
159}
160
161#[async_trait]
162impl DockerCommand for LogoutCommand {
163    type Output = LogoutOutput;
164
165    fn get_executor(&self) -> &CommandExecutor {
166        &self.executor
167    }
168
169    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
170        &mut self.executor
171    }
172
173    fn build_command_args(&self) -> Vec<String> {
174        let mut args = vec!["logout".to_string()];
175
176        // Add server if specified
177        if let Some(ref server) = self.server {
178            args.push(server.clone());
179        }
180
181        // Add raw args from executor
182        args.extend(self.executor.raw_args.clone());
183
184        args
185    }
186
187    async fn execute(&self) -> Result<Self::Output> {
188        let args = self.build_command_args();
189        let output = self.executor.execute_command("docker", args).await?;
190
191        Ok(LogoutOutput { output })
192    }
193}
194
195impl fmt::Display for LogoutCommand {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        write!(f, "docker logout")?;
198
199        if let Some(ref server) = self.server {
200            write!(f, " {server}")?;
201        }
202
203        Ok(())
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_logout_command_basic() {
213        let logout = LogoutCommand::new();
214
215        assert_eq!(logout.get_server(), None);
216
217        let args = logout.build_command_args();
218        assert_eq!(args, vec!["logout"]);
219    }
220
221    #[test]
222    fn test_logout_command_with_server() {
223        let logout = LogoutCommand::new().server("gcr.io");
224
225        assert_eq!(logout.get_server(), Some("gcr.io"));
226
227        let args = logout.build_command_args();
228        assert_eq!(args, vec!["logout", "gcr.io"]);
229    }
230
231    #[test]
232    fn test_logout_command_with_private_registry() {
233        let logout = LogoutCommand::new().server("my-registry.example.com:5000");
234
235        let args = logout.build_command_args();
236        assert_eq!(args, vec!["logout", "my-registry.example.com:5000"]);
237    }
238
239    #[test]
240    fn test_logout_command_daemon_default() {
241        let logout = LogoutCommand::new();
242
243        // No server specified should use daemon default
244        assert_eq!(logout.get_server(), None);
245
246        let args = logout.build_command_args();
247        assert_eq!(args, vec!["logout"]);
248    }
249
250    #[test]
251    fn test_logout_command_display() {
252        let logout = LogoutCommand::new().server("example.com");
253
254        let display = format!("{logout}");
255        assert_eq!(display, "docker logout example.com");
256    }
257
258    #[test]
259    fn test_logout_command_display_no_server() {
260        let logout = LogoutCommand::new();
261
262        let display = format!("{logout}");
263        assert_eq!(display, "docker logout");
264    }
265
266    #[test]
267    fn test_logout_command_default() {
268        let logout = LogoutCommand::default();
269
270        assert_eq!(logout.get_server(), None);
271        let args = logout.build_command_args();
272        assert_eq!(args, vec!["logout"]);
273    }
274
275    #[test]
276    fn test_logout_output_success_with_credentials_removal() {
277        let output = CommandOutput {
278            stdout: "Removing login credentials for https://index.docker.io/v1/".to_string(),
279            stderr: String::new(),
280            exit_code: 0,
281            success: true,
282        };
283        let logout_output = LogoutOutput { output };
284
285        assert!(logout_output.success());
286        assert!(logout_output.is_logged_out());
287    }
288
289    #[test]
290    fn test_logout_output_success_not_logged_in() {
291        let output = CommandOutput {
292            stdout: "Not logged in to https://index.docker.io/v1/".to_string(),
293            stderr: String::new(),
294            exit_code: 0,
295            success: true,
296        };
297        let logout_output = LogoutOutput { output };
298
299        assert!(logout_output.success());
300        assert!(logout_output.is_logged_out());
301    }
302
303    #[test]
304    fn test_logout_output_success_empty() {
305        let output = CommandOutput {
306            stdout: String::new(),
307            stderr: String::new(),
308            exit_code: 0,
309            success: true,
310        };
311        let logout_output = LogoutOutput { output };
312
313        assert!(logout_output.success());
314        assert!(logout_output.is_logged_out());
315    }
316
317    #[test]
318    fn test_logout_output_warnings() {
319        let output = CommandOutput {
320            stdout: "Removing login credentials for registry".to_string(),
321            stderr: "WARNING: credentials may still be cached\ninfo: using default registry"
322                .to_string(),
323            exit_code: 0,
324            success: true,
325        };
326        let logout_output = LogoutOutput { output };
327
328        let warnings = logout_output.warnings();
329        assert_eq!(warnings.len(), 1);
330        assert!(warnings[0].contains("WARNING"));
331    }
332
333    #[test]
334    fn test_logout_output_info_messages() {
335        let output = CommandOutput {
336            stdout: "Removing login credentials for https://registry.example.com\nLogout completed"
337                .to_string(),
338            stderr: String::new(),
339            exit_code: 0,
340            success: true,
341        };
342        let logout_output = LogoutOutput { output };
343
344        let info = logout_output.info_messages();
345        assert_eq!(info.len(), 2);
346        assert!(info[0].contains("Removing login credentials"));
347        assert!(info[1].contains("Logout completed"));
348    }
349
350    #[test]
351    fn test_logout_output_failure() {
352        let output = CommandOutput {
353            stdout: String::new(),
354            stderr: "Error: unable to logout".to_string(),
355            exit_code: 1,
356            success: false,
357        };
358        let logout_output = LogoutOutput { output };
359
360        assert!(!logout_output.success());
361        assert!(!logout_output.is_logged_out());
362    }
363
364    #[test]
365    fn test_logout_multiple_servers_concept() {
366        // Test that we can create logout commands for different servers
367        let daemon_default_logout = LogoutCommand::new();
368        let gcr_logout = LogoutCommand::new().server("gcr.io");
369        let private_logout = LogoutCommand::new().server("my-registry.com");
370
371        assert_eq!(daemon_default_logout.get_server(), None);
372        assert_eq!(gcr_logout.get_server(), Some("gcr.io"));
373        assert_eq!(private_logout.get_server(), Some("my-registry.com"));
374    }
375
376    #[test]
377    fn test_logout_builder_pattern() {
378        let logout = LogoutCommand::new().server("registry.example.com");
379
380        assert_eq!(logout.get_server(), Some("registry.example.com"));
381    }
382
383    #[test]
384    fn test_logout_various_server_formats() {
385        let test_cases = vec![
386            "gcr.io",
387            "registry-1.docker.io",
388            "localhost:5000",
389            "my-registry.com:443",
390            "registry.example.com/path",
391        ];
392
393        for server in test_cases {
394            let logout = LogoutCommand::new().server(server);
395            assert_eq!(logout.get_server(), Some(server));
396
397            let args = logout.build_command_args();
398            assert!(args.contains(&server.to_string()));
399        }
400    }
401}