docker_wrapper/command/
logout.rs1use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use std::fmt;
10
11#[derive(Debug, Clone)]
29pub struct LogoutCommand {
30 server: Option<String>,
32 pub executor: CommandExecutor,
34}
35
36#[derive(Debug, Clone)]
41pub struct LogoutOutput {
42 pub output: CommandOutput,
44}
45
46impl LogoutCommand {
47 #[must_use]
59 pub fn new() -> Self {
60 Self {
61 server: None,
62 executor: CommandExecutor::default(),
63 }
64 }
65
66 #[must_use]
83 pub fn server(mut self, server: impl Into<String>) -> Self {
84 self.server = Some(server.into());
85 self
86 }
87
88 #[must_use]
94 pub fn executor(mut self, executor: CommandExecutor) -> Self {
95 self.executor = executor;
96 self
97 }
98
99 #[must_use]
101 pub fn get_server(&self) -> Option<&str> {
102 self.server.as_deref()
103 }
104
105 #[must_use]
107 pub fn get_executor(&self) -> &CommandExecutor {
108 &self.executor
109 }
110
111 #[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 #[must_use]
127 pub fn success(&self) -> bool {
128 self.output.success
129 }
130
131 #[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 #[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 #[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 if let Some(ref server) = self.server {
178 args.push(server.clone());
179 }
180
181 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 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 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}