1use 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)]
35pub struct LoginCommand {
36 username: String,
38 password: String,
40 registry: Option<String>,
42 password_stdin: bool,
44 executor: CommandExecutor,
46}
47
48#[derive(Debug, Clone)]
53pub struct LoginOutput {
54 pub output: CommandOutput,
56}
57
58impl LoginCommand {
59 pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
74 Self {
75 username: username.into(),
76 password: password.into(),
77 registry: None,
78 password_stdin: false,
79 executor: CommandExecutor::default(),
80 }
81 }
82
83 #[must_use]
100 pub fn registry(mut self, registry: impl Into<String>) -> Self {
101 self.registry = Some(registry.into());
102 self
103 }
104
105 #[must_use]
119 pub fn password_stdin(mut self) -> Self {
120 self.password_stdin = true;
121 self
122 }
123
124 #[must_use]
130 pub fn executor(mut self, executor: CommandExecutor) -> Self {
131 self.executor = executor;
132 self
133 }
134
135 fn build_command_args(&self) -> Vec<String> {
137 let mut args = vec!["login".to_string()];
138
139 args.push("--username".to_string());
141 args.push(self.username.clone());
142
143 if self.password_stdin {
145 args.push("--password-stdin".to_string());
146 } else {
147 args.push("--password".to_string());
148 args.push(self.password.clone());
149 }
150
151 if let Some(ref registry) = self.registry {
153 args.push(registry.clone());
154 }
155
156 args
157 }
158
159 #[must_use]
161 pub fn get_username(&self) -> &str {
162 &self.username
163 }
164
165 #[must_use]
167 pub fn get_registry(&self) -> Option<&str> {
168 self.registry.as_deref()
169 }
170
171 #[must_use]
173 pub fn is_password_stdin(&self) -> bool {
174 self.password_stdin
175 }
176}
177
178impl Default for LoginCommand {
179 fn default() -> Self {
180 Self::new("", "")
181 }
182}
183
184impl LoginOutput {
185 #[must_use]
187 pub fn success(&self) -> bool {
188 self.output.success
189 }
190
191 #[must_use]
193 pub fn is_authenticated(&self) -> bool {
194 self.success()
195 && (self.output.stdout.contains("Login Succeeded")
196 || self.output.stdout.contains("login succeeded"))
197 }
198
199 #[must_use]
201 pub fn warnings(&self) -> Vec<&str> {
202 self.output
203 .stderr
204 .lines()
205 .filter(|line| line.to_lowercase().contains("warning"))
206 .collect()
207 }
208}
209
210#[async_trait]
211impl DockerCommand for LoginCommand {
212 type Output = LoginOutput;
213
214 fn command_name(&self) -> &'static str {
215 "login"
216 }
217
218 fn build_args(&self) -> Vec<String> {
219 self.build_command_args()
220 }
221
222 async fn execute(&self) -> Result<Self::Output> {
223 let output = self
224 .executor
225 .execute_command(self.command_name(), self.build_args())
226 .await?;
227
228 Ok(LoginOutput { output })
229 }
230
231 fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
232 self.executor.add_arg(arg);
233 self
234 }
235
236 fn args<I, S>(&mut self, args: I) -> &mut Self
237 where
238 I: IntoIterator<Item = S>,
239 S: AsRef<OsStr>,
240 {
241 self.executor.add_args(args);
242 self
243 }
244
245 fn flag(&mut self, flag: &str) -> &mut Self {
246 self.executor.add_flag(flag);
247 self
248 }
249
250 fn option(&mut self, key: &str, value: &str) -> &mut Self {
251 self.executor.add_option(key, value);
252 self
253 }
254}
255
256impl fmt::Display for LoginCommand {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 write!(f, "docker login")?;
259
260 if let Some(ref registry) = self.registry {
261 write!(f, " {registry}")?;
262 }
263
264 write!(f, " --username {}", self.username)?;
265
266 if self.password_stdin {
267 write!(f, " --password-stdin")?;
268 } else {
269 write!(f, " --password [HIDDEN]")?;
270 }
271
272 Ok(())
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_login_command_basic() {
282 let login = LoginCommand::new("testuser", "testpass");
283
284 assert_eq!(login.get_username(), "testuser");
285 assert_eq!(login.get_registry(), None);
286 assert!(!login.is_password_stdin());
287
288 let args = login.build_command_args();
289 assert_eq!(
290 args,
291 vec!["login", "--username", "testuser", "--password", "testpass"]
292 );
293 }
294
295 #[test]
296 fn test_login_command_with_registry() {
297 let login = LoginCommand::new("user", "pass").registry("gcr.io");
298
299 assert_eq!(login.get_registry(), Some("gcr.io"));
300
301 let args = login.build_command_args();
302 assert_eq!(
303 args,
304 vec![
305 "login",
306 "--username",
307 "user",
308 "--password",
309 "pass",
310 "gcr.io"
311 ]
312 );
313 }
314
315 #[test]
316 fn test_login_command_password_stdin() {
317 let login = LoginCommand::new("user", "ignored").password_stdin();
318
319 assert!(login.is_password_stdin());
320
321 let args = login.build_command_args();
322 assert_eq!(
323 args,
324 vec!["login", "--username", "user", "--password-stdin"]
325 );
326 }
327
328 #[test]
329 fn test_login_command_with_private_registry() {
330 let login = LoginCommand::new("admin", "secret").registry("my-registry.example.com:5000");
331
332 let args = login.build_command_args();
333 assert_eq!(
334 args,
335 vec![
336 "login",
337 "--username",
338 "admin",
339 "--password",
340 "secret",
341 "my-registry.example.com:5000"
342 ]
343 );
344 }
345
346 #[test]
347 fn test_login_command_docker_hub_default() {
348 let login = LoginCommand::new("dockeruser", "dockerpass");
349
350 assert_eq!(login.get_registry(), None);
352
353 let args = login.build_command_args();
354 assert!(!args.contains(&"index.docker.io".to_string()));
355 }
356
357 #[test]
358 fn test_login_command_display() {
359 let login = LoginCommand::new("testuser", "testpass").registry("example.com");
360
361 let display = format!("{login}");
362 assert!(display.contains("docker login"));
363 assert!(display.contains("example.com"));
364 assert!(display.contains("--username testuser"));
365 assert!(display.contains("--password [HIDDEN]"));
366 assert!(!display.contains("testpass"));
367 }
368
369 #[test]
370 fn test_login_command_display_stdin() {
371 let login = LoginCommand::new("testuser", "").password_stdin();
372
373 let display = format!("{login}");
374 assert!(display.contains("--password-stdin"));
375 assert!(!display.contains("[HIDDEN]"));
376 }
377
378 #[test]
379 fn test_login_command_default() {
380 let login = LoginCommand::default();
381
382 assert_eq!(login.get_username(), "");
383 assert_eq!(login.get_registry(), None);
384 assert!(!login.is_password_stdin());
385 }
386
387 #[test]
388 fn test_login_output_success_detection() {
389 let output = CommandOutput {
390 stdout: "Login Succeeded".to_string(),
391 stderr: String::new(),
392 exit_code: 0,
393 success: true,
394 };
395 let login_output = LoginOutput { output };
396
397 assert!(login_output.success());
398 assert!(login_output.is_authenticated());
399 }
400
401 #[test]
402 fn test_login_output_alternative_success_message() {
403 let output = CommandOutput {
404 stdout: "login succeeded for user@registry".to_string(),
405 stderr: String::new(),
406 exit_code: 0,
407 success: true,
408 };
409 let login_output = LoginOutput { output };
410
411 assert!(login_output.is_authenticated());
412 }
413
414 #[test]
415 fn test_login_output_warnings() {
416 let output = CommandOutput {
417 stdout: "Login Succeeded".to_string(),
418 stderr: "WARNING: login credentials saved in plaintext\ninfo: using default registry"
419 .to_string(),
420 exit_code: 0,
421 success: true,
422 };
423 let login_output = LoginOutput { output };
424
425 let warnings = login_output.warnings();
426 assert_eq!(warnings.len(), 1);
427 assert!(warnings[0].contains("WARNING"));
428 }
429
430 #[test]
431 fn test_login_output_failure() {
432 let output = CommandOutput {
433 stdout: String::new(),
434 stderr: "Error: authentication failed".to_string(),
435 exit_code: 1,
436 success: false,
437 };
438 let login_output = LoginOutput { output };
439
440 assert!(!login_output.success());
441 assert!(!login_output.is_authenticated());
442 }
443
444 #[test]
445 fn test_login_command_name() {
446 let login = LoginCommand::new("user", "pass");
447 assert_eq!(login.command_name(), "login");
448 }
449
450 #[test]
451 fn test_login_command_extensibility() {
452 let mut login = LoginCommand::new("user", "pass");
453
454 login
456 .arg("extra")
457 .args(vec!["more", "args"])
458 .flag("--verbose")
459 .option("key", "value");
460
461 assert_eq!(login.command_name(), "login");
463 }
464}