docker_wrapper/command/
logout.rs1use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use std::ffi::OsStr;
10use std::fmt;
11
12#[derive(Debug, Clone)]
30pub struct LogoutCommand {
31 server: Option<String>,
33 executor: CommandExecutor,
35}
36
37#[derive(Debug, Clone)]
42pub struct LogoutOutput {
43 pub output: CommandOutput,
45}
46
47impl LogoutCommand {
48 #[must_use]
60 pub fn new() -> Self {
61 Self {
62 server: None,
63 executor: CommandExecutor::default(),
64 }
65 }
66
67 #[must_use]
84 pub fn server(mut self, server: impl Into<String>) -> Self {
85 self.server = Some(server.into());
86 self
87 }
88
89 #[must_use]
95 pub fn executor(mut self, executor: CommandExecutor) -> Self {
96 self.executor = executor;
97 self
98 }
99
100 fn build_command_args(&self) -> Vec<String> {
102 let mut args = vec!["logout".to_string()];
103
104 if let Some(ref server) = self.server {
106 args.push(server.clone());
107 }
108
109 args
110 }
111
112 #[must_use]
114 pub fn get_server(&self) -> Option<&str> {
115 self.server.as_deref()
116 }
117}
118
119impl Default for LogoutCommand {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125impl LogoutOutput {
126 #[must_use]
128 pub fn success(&self) -> bool {
129 self.output.success
130 }
131
132 #[must_use]
134 pub fn is_logged_out(&self) -> bool {
135 self.success()
136 && (self.output.stdout.contains("Removing login credentials")
137 || self.output.stdout.contains("Not logged in")
138 || self.output.stdout.is_empty() && self.output.stderr.is_empty())
139 }
140
141 #[must_use]
143 pub fn warnings(&self) -> Vec<&str> {
144 self.output
145 .stderr
146 .lines()
147 .filter(|line| line.to_lowercase().contains("warning"))
148 .collect()
149 }
150
151 #[must_use]
153 pub fn info_messages(&self) -> Vec<&str> {
154 self.output
155 .stdout
156 .lines()
157 .filter(|line| !line.trim().is_empty())
158 .collect()
159 }
160}
161
162#[async_trait]
163impl DockerCommand for LogoutCommand {
164 type Output = LogoutOutput;
165
166 fn command_name(&self) -> &'static str {
167 "logout"
168 }
169
170 fn build_args(&self) -> Vec<String> {
171 self.build_command_args()
172 }
173
174 async fn execute(&self) -> Result<Self::Output> {
175 let output = self
176 .executor
177 .execute_command(self.command_name(), self.build_args())
178 .await?;
179
180 Ok(LogoutOutput { output })
181 }
182
183 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
184 self.executor.add_arg(arg);
185 self
186 }
187
188 fn args<I, S>(&mut self, args: I) -> &mut Self
189 where
190 I: IntoIterator<Item = S>,
191 S: AsRef<OsStr>,
192 {
193 self.executor.add_args(args);
194 self
195 }
196
197 fn flag(&mut self, flag: &str) -> &mut Self {
198 self.executor.add_flag(flag);
199 self
200 }
201
202 fn option(&mut self, key: &str, value: &str) -> &mut Self {
203 self.executor.add_option(key, value);
204 self
205 }
206}
207
208impl fmt::Display for LogoutCommand {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 write!(f, "docker logout")?;
211
212 if let Some(ref server) = self.server {
213 write!(f, " {server}")?;
214 }
215
216 Ok(())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_logout_command_basic() {
226 let logout = LogoutCommand::new();
227
228 assert_eq!(logout.get_server(), None);
229
230 let args = logout.build_command_args();
231 assert_eq!(args, vec!["logout"]);
232 }
233
234 #[test]
235 fn test_logout_command_with_server() {
236 let logout = LogoutCommand::new().server("gcr.io");
237
238 assert_eq!(logout.get_server(), Some("gcr.io"));
239
240 let args = logout.build_command_args();
241 assert_eq!(args, vec!["logout", "gcr.io"]);
242 }
243
244 #[test]
245 fn test_logout_command_with_private_registry() {
246 let logout = LogoutCommand::new().server("my-registry.example.com:5000");
247
248 let args = logout.build_command_args();
249 assert_eq!(args, vec!["logout", "my-registry.example.com:5000"]);
250 }
251
252 #[test]
253 fn test_logout_command_daemon_default() {
254 let logout = LogoutCommand::new();
255
256 assert_eq!(logout.get_server(), None);
258
259 let args = logout.build_command_args();
260 assert_eq!(args, vec!["logout"]);
261 }
262
263 #[test]
264 fn test_logout_command_display() {
265 let logout = LogoutCommand::new().server("example.com");
266
267 let display = format!("{logout}");
268 assert_eq!(display, "docker logout example.com");
269 }
270
271 #[test]
272 fn test_logout_command_display_no_server() {
273 let logout = LogoutCommand::new();
274
275 let display = format!("{logout}");
276 assert_eq!(display, "docker logout");
277 }
278
279 #[test]
280 fn test_logout_command_default() {
281 let logout = LogoutCommand::default();
282
283 assert_eq!(logout.get_server(), None);
284 let args = logout.build_command_args();
285 assert_eq!(args, vec!["logout"]);
286 }
287
288 #[test]
289 fn test_logout_output_success_with_credentials_removal() {
290 let output = CommandOutput {
291 stdout: "Removing login credentials for https://index.docker.io/v1/".to_string(),
292 stderr: String::new(),
293 exit_code: 0,
294 success: true,
295 };
296 let logout_output = LogoutOutput { output };
297
298 assert!(logout_output.success());
299 assert!(logout_output.is_logged_out());
300 }
301
302 #[test]
303 fn test_logout_output_success_not_logged_in() {
304 let output = CommandOutput {
305 stdout: "Not logged in to https://index.docker.io/v1/".to_string(),
306 stderr: String::new(),
307 exit_code: 0,
308 success: true,
309 };
310 let logout_output = LogoutOutput { output };
311
312 assert!(logout_output.success());
313 assert!(logout_output.is_logged_out());
314 }
315
316 #[test]
317 fn test_logout_output_success_empty() {
318 let output = CommandOutput {
319 stdout: String::new(),
320 stderr: String::new(),
321 exit_code: 0,
322 success: true,
323 };
324 let logout_output = LogoutOutput { output };
325
326 assert!(logout_output.success());
327 assert!(logout_output.is_logged_out());
328 }
329
330 #[test]
331 fn test_logout_output_warnings() {
332 let output = CommandOutput {
333 stdout: "Removing login credentials for registry".to_string(),
334 stderr: "WARNING: credentials may still be cached\ninfo: using default registry"
335 .to_string(),
336 exit_code: 0,
337 success: true,
338 };
339 let logout_output = LogoutOutput { output };
340
341 let warnings = logout_output.warnings();
342 assert_eq!(warnings.len(), 1);
343 assert!(warnings[0].contains("WARNING"));
344 }
345
346 #[test]
347 fn test_logout_output_info_messages() {
348 let output = CommandOutput {
349 stdout: "Removing login credentials for https://registry.example.com\nLogout completed"
350 .to_string(),
351 stderr: String::new(),
352 exit_code: 0,
353 success: true,
354 };
355 let logout_output = LogoutOutput { output };
356
357 let info = logout_output.info_messages();
358 assert_eq!(info.len(), 2);
359 assert!(info[0].contains("Removing login credentials"));
360 assert!(info[1].contains("Logout completed"));
361 }
362
363 #[test]
364 fn test_logout_output_failure() {
365 let output = CommandOutput {
366 stdout: String::new(),
367 stderr: "Error: unable to logout".to_string(),
368 exit_code: 1,
369 success: false,
370 };
371 let logout_output = LogoutOutput { output };
372
373 assert!(!logout_output.success());
374 assert!(!logout_output.is_logged_out());
375 }
376
377 #[test]
378 fn test_logout_command_name() {
379 let logout = LogoutCommand::new();
380 assert_eq!(logout.command_name(), "logout");
381 }
382
383 #[test]
384 fn test_logout_command_extensibility() {
385 let mut logout = LogoutCommand::new();
386
387 logout
389 .arg("extra")
390 .args(vec!["more", "args"])
391 .flag("--verbose")
392 .option("key", "value");
393
394 assert_eq!(logout.command_name(), "logout");
396 }
397
398 #[test]
399 fn test_logout_multiple_servers_concept() {
400 let daemon_default_logout = LogoutCommand::new();
402 let gcr_logout = LogoutCommand::new().server("gcr.io");
403 let private_logout = LogoutCommand::new().server("my-registry.com");
404
405 assert_eq!(daemon_default_logout.get_server(), None);
406 assert_eq!(gcr_logout.get_server(), Some("gcr.io"));
407 assert_eq!(private_logout.get_server(), Some("my-registry.com"));
408 }
409
410 #[test]
411 fn test_logout_builder_pattern() {
412 let logout = LogoutCommand::new().server("registry.example.com");
413
414 assert_eq!(logout.get_server(), Some("registry.example.com"));
415 assert_eq!(logout.command_name(), "logout");
416 }
417
418 #[test]
419 fn test_logout_various_server_formats() {
420 let test_cases = vec![
421 "gcr.io",
422 "registry-1.docker.io",
423 "localhost:5000",
424 "my-registry.com:443",
425 "registry.example.com/path",
426 ];
427
428 for server in test_cases {
429 let logout = LogoutCommand::new().server(server);
430 assert_eq!(logout.get_server(), Some(server));
431
432 let args = logout.build_command_args();
433 assert!(args.contains(&server.to_string()));
434 }
435 }
436}