use super::{CommandExecutor, CommandOutput, DockerCommand};
use crate::error::Result;
use async_trait::async_trait;
use std::fmt;
#[derive(Debug, Clone)]
pub struct LogoutCommand {
server: Option<String>,
pub executor: CommandExecutor,
}
#[derive(Debug, Clone)]
pub struct LogoutOutput {
pub output: CommandOutput,
}
impl LogoutCommand {
#[must_use]
pub fn new() -> Self {
Self {
server: None,
executor: CommandExecutor::default(),
}
}
#[must_use]
pub fn server(mut self, server: impl Into<String>) -> Self {
self.server = Some(server.into());
self
}
#[must_use]
pub fn executor(mut self, executor: CommandExecutor) -> Self {
self.executor = executor;
self
}
#[must_use]
pub fn get_server(&self) -> Option<&str> {
self.server.as_deref()
}
#[must_use]
pub fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
#[must_use]
pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
}
impl Default for LogoutCommand {
fn default() -> Self {
Self::new()
}
}
impl LogoutOutput {
#[must_use]
pub fn success(&self) -> bool {
self.output.success
}
#[must_use]
pub fn is_logged_out(&self) -> bool {
self.success()
&& (self.output.stdout.contains("Removing login credentials")
|| self.output.stdout.contains("Not logged in")
|| self.output.stdout.is_empty() && self.output.stderr.is_empty())
}
#[must_use]
pub fn warnings(&self) -> Vec<&str> {
self.output
.stderr
.lines()
.filter(|line| line.to_lowercase().contains("warning"))
.collect()
}
#[must_use]
pub fn info_messages(&self) -> Vec<&str> {
self.output
.stdout
.lines()
.filter(|line| !line.trim().is_empty())
.collect()
}
}
#[async_trait]
impl DockerCommand for LogoutCommand {
type Output = LogoutOutput;
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
fn build_command_args(&self) -> Vec<String> {
let mut args = vec!["logout".to_string()];
if let Some(ref server) = self.server {
args.push(server.clone());
}
args.extend(self.executor.raw_args.clone());
args
}
async fn execute(&self) -> Result<Self::Output> {
let args = self.build_command_args();
let output = self.execute_command(args).await?;
Ok(LogoutOutput { output })
}
}
impl fmt::Display for LogoutCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "docker logout")?;
if let Some(ref server) = self.server {
write!(f, " {server}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_logout_command_basic() {
let logout = LogoutCommand::new();
assert_eq!(logout.get_server(), None);
let args = logout.build_command_args();
assert_eq!(args, vec!["logout"]);
}
#[test]
fn test_logout_command_with_server() {
let logout = LogoutCommand::new().server("gcr.io");
assert_eq!(logout.get_server(), Some("gcr.io"));
let args = logout.build_command_args();
assert_eq!(args, vec!["logout", "gcr.io"]);
}
#[test]
fn test_logout_command_with_private_registry() {
let logout = LogoutCommand::new().server("my-registry.example.com:5000");
let args = logout.build_command_args();
assert_eq!(args, vec!["logout", "my-registry.example.com:5000"]);
}
#[test]
fn test_logout_command_daemon_default() {
let logout = LogoutCommand::new();
assert_eq!(logout.get_server(), None);
let args = logout.build_command_args();
assert_eq!(args, vec!["logout"]);
}
#[test]
fn test_logout_command_display() {
let logout = LogoutCommand::new().server("example.com");
let display = format!("{logout}");
assert_eq!(display, "docker logout example.com");
}
#[test]
fn test_logout_command_display_no_server() {
let logout = LogoutCommand::new();
let display = format!("{logout}");
assert_eq!(display, "docker logout");
}
#[test]
fn test_logout_command_default() {
let logout = LogoutCommand::default();
assert_eq!(logout.get_server(), None);
let args = logout.build_command_args();
assert_eq!(args, vec!["logout"]);
}
#[test]
fn test_logout_output_success_with_credentials_removal() {
let output = CommandOutput {
stdout: "Removing login credentials for https://index.docker.io/v1/".to_string(),
stderr: String::new(),
exit_code: 0,
success: true,
};
let logout_output = LogoutOutput { output };
assert!(logout_output.success());
assert!(logout_output.is_logged_out());
}
#[test]
fn test_logout_output_success_not_logged_in() {
let output = CommandOutput {
stdout: "Not logged in to https://index.docker.io/v1/".to_string(),
stderr: String::new(),
exit_code: 0,
success: true,
};
let logout_output = LogoutOutput { output };
assert!(logout_output.success());
assert!(logout_output.is_logged_out());
}
#[test]
fn test_logout_output_success_empty() {
let output = CommandOutput {
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
success: true,
};
let logout_output = LogoutOutput { output };
assert!(logout_output.success());
assert!(logout_output.is_logged_out());
}
#[test]
fn test_logout_output_warnings() {
let output = CommandOutput {
stdout: "Removing login credentials for registry".to_string(),
stderr: "WARNING: credentials may still be cached\ninfo: using default registry"
.to_string(),
exit_code: 0,
success: true,
};
let logout_output = LogoutOutput { output };
let warnings = logout_output.warnings();
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("WARNING"));
}
#[test]
fn test_logout_output_info_messages() {
let output = CommandOutput {
stdout: "Removing login credentials for https://registry.example.com\nLogout completed"
.to_string(),
stderr: String::new(),
exit_code: 0,
success: true,
};
let logout_output = LogoutOutput { output };
let info = logout_output.info_messages();
assert_eq!(info.len(), 2);
assert!(info[0].contains("Removing login credentials"));
assert!(info[1].contains("Logout completed"));
}
#[test]
fn test_logout_output_failure() {
let output = CommandOutput {
stdout: String::new(),
stderr: "Error: unable to logout".to_string(),
exit_code: 1,
success: false,
};
let logout_output = LogoutOutput { output };
assert!(!logout_output.success());
assert!(!logout_output.is_logged_out());
}
#[test]
fn test_logout_multiple_servers_concept() {
let daemon_default_logout = LogoutCommand::new();
let gcr_logout = LogoutCommand::new().server("gcr.io");
let private_logout = LogoutCommand::new().server("my-registry.com");
assert_eq!(daemon_default_logout.get_server(), None);
assert_eq!(gcr_logout.get_server(), Some("gcr.io"));
assert_eq!(private_logout.get_server(), Some("my-registry.com"));
}
#[test]
fn test_logout_builder_pattern() {
let logout = LogoutCommand::new().server("registry.example.com");
assert_eq!(logout.get_server(), Some("registry.example.com"));
}
#[test]
fn test_logout_various_server_formats() {
let test_cases = vec![
"gcr.io",
"registry-1.docker.io",
"localhost:5000",
"my-registry.com:443",
"registry.example.com/path",
];
for server in test_cases {
let logout = LogoutCommand::new().server(server);
assert_eq!(logout.get_server(), Some(server));
let args = logout.build_command_args();
assert!(args.contains(&server.to_string()));
}
}
}