1use crate::mascaramento::mascarar;
8use crate::ssh::SaidaExecucao;
9use crate::vps::modelo::VpsRegistro;
10use secrecy::ExposeSecret;
11use serde_json::json;
12use std::io::{self, BufRead, IsTerminal, Write};
13
14pub fn escrever_linha(conteudo: &str) -> io::Result<()> {
19 let stdout = io::stdout();
20 let mut handle = stdout.lock();
21 handle.write_all(conteudo.as_bytes())?;
22 handle.write_all(b"\n")?;
23 handle.flush()?;
24 Ok(())
25}
26
27pub fn imprimir_sucesso(mensagem: &str) {
29 println!("{mensagem}");
30}
31
32#[must_use]
34pub fn stdin_e_tty() -> bool {
35 io::stdin().is_terminal()
36}
37
38pub fn ler_confirmacao<R: BufRead, W: Write>(
48 reader: &mut R,
49 writer: &mut W,
50 prompt: &str,
51) -> io::Result<bool> {
52 writer.write_all(prompt.as_bytes())?;
53 writer.flush()?;
54 let mut linha = String::new();
55 let lidos = reader.read_line(&mut linha)?;
56 if lidos == 0 {
57 return Ok(false);
59 }
60 let resposta = linha.trim().to_lowercase();
61 Ok(matches!(resposta.as_str(), "s" | "sim" | "y" | "yes"))
62}
63
64pub fn perguntar_confirmacao(prompt: &str) -> io::Result<bool> {
73 let stdin = io::stdin();
74 let mut reader = stdin.lock();
75 let stderr = io::stderr();
76 let mut writer = stderr.lock();
77 ler_confirmacao(&mut reader, &mut writer, prompt)
78}
79
80pub fn imprimir_erro(mensagem: &str) {
82 eprintln!("{mensagem}");
83}
84
85pub fn imprimir_erro_runtime(mensagem: &str) {
91 eprintln!("erro ao criar runtime: {mensagem}");
92}
93
94pub fn imprimir_erro_dominio(erro: &crate::erros::ErroSshCli) {
100 eprintln!("{}", erro.mensagem_i18n());
101}
102
103pub fn imprimir_erro_generico(erro: &anyhow::Error) {
109 eprintln!("{erro}");
110 for causa in erro.chain().skip(1) {
111 eprintln!(" causado por: {causa}");
112 }
113}
114
115pub fn imprimir_lista_texto(registros: &[VpsRegistro]) {
117 if registros.is_empty() {
118 println!(
119 "{}",
120 crate::i18n::t(crate::i18n::Mensagem::VpsRegistroVazio)
121 );
122 return;
123 }
124
125 println!(
126 "{:<20} {:<30} {:<6} {:<15} {:<20}",
127 "NOME", "HOST", "PORTA", "USUÁRIO", "SENHA"
128 );
129 for r in registros {
130 println!(
131 "{:<20} {:<30} {:<6} {:<15} {:<20}",
132 r.nome,
133 r.host,
134 r.porta,
135 r.usuario,
136 mascarar(r.senha.expose_secret())
137 );
138 }
139}
140
141pub fn imprimir_lista_json(registros: &[VpsRegistro]) {
143 let lista: Vec<_> = registros.iter().map(registro_para_json_mascarado).collect();
144 match serde_json::to_string_pretty(&lista) {
145 Ok(s) => println!("{s}"),
146 Err(erro) => eprintln!("erro ao serializar JSON: {erro}"),
147 }
148}
149
150pub fn imprimir_detalhes_texto(r: &VpsRegistro) {
152 println!("Nome: {}", r.nome);
153 println!("Host: {}", r.host);
154 println!("Porta: {}", r.porta);
155 println!("Usuário: {}", r.usuario);
156 println!("Senha: {}", mascarar(r.senha.expose_secret()));
157 println!(
158 "Senha sudo: {}",
159 r.senha_sudo
160 .as_ref()
161 .map_or_else(|| "(não definida)".into(), |s| mascarar(s.expose_secret()))
162 );
163 println!(
164 "Senha su: {}",
165 r.senha_su
166 .as_ref()
167 .map_or_else(|| "(não definida)".into(), |s| mascarar(s.expose_secret()))
168 );
169 println!("Timeout (ms): {}", r.timeout_ms);
170 println!("Max chars: {}", r.max_chars);
171 println!("Schema version: {}", r.schema_version);
172 println!("Adicionado em: {}", r.adicionado_em);
173}
174
175pub fn imprimir_detalhes_json(r: &VpsRegistro) {
177 let v = registro_para_json_mascarado(r);
178 match serde_json::to_string_pretty(&v) {
179 Ok(s) => println!("{s}"),
180 Err(erro) => eprintln!("erro ao serializar JSON: {erro}"),
181 }
182}
183
184fn registro_para_json_mascarado(r: &VpsRegistro) -> serde_json::Value {
185 json!({
186 "name": r.nome,
187 "host": r.host,
188 "port": r.porta,
189 "user": r.usuario,
190 "password": mascarar(r.senha.expose_secret()),
191 "sudo_password": r.senha_sudo.as_ref().map(|s| mascarar(s.expose_secret())),
192 "su_password": r.senha_su.as_ref().map(|s| mascarar(s.expose_secret())),
193 "timeout_ms": r.timeout_ms,
194 "max_chars": r.max_chars,
195 "schema_version": r.schema_version,
196 "added_at": r.adicionado_em,
197 })
198}
199
200pub fn imprimir_saida_execucao(saida: &SaidaExecucao) {
211 println!("--- stdout ---");
212 if saida.stdout.is_empty() {
213 println!("(vazio)");
214 } else {
215 println!("{}", saida.stdout);
216 }
217 println!("--- stderr ---");
218 if saida.stderr.is_empty() {
219 println!("(vazio)");
220 } else {
221 println!("{}", saida.stderr);
222 }
223 let code_str = saida
224 .exit_code
225 .map(|c| c.to_string())
226 .unwrap_or_else(|| "N/A".to_string());
227 println!("--- exit code: {} ({}ms) ---", code_str, saida.duracao_ms);
228 if saida.truncado_stdout {
229 println!("(stdout foi truncado)");
230 }
231 if saida.truncado_stderr {
232 println!("(stderr foi truncado)");
233 }
234}
235
236pub fn imprimir_saida_execucao_json(saida: &SaidaExecucao) {
238 let v = json!({
239 "stdout": saida.stdout,
240 "stderr": saida.stderr,
241 "exit_code": saida.exit_code,
242 "truncated_stdout": saida.truncado_stdout,
243 "truncated_stderr": saida.truncado_stderr,
244 "duration_ms": saida.duracao_ms,
245 });
246 match serde_json::to_string_pretty(&v) {
247 Ok(s) => println!("{s}"),
248 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
249 }
250}
251
252pub fn imprimir_health_check(nome: &str, latencia_ms: u64) {
254 println!(
255 "{}",
256 crate::i18n::t(crate::i18n::Mensagem::HealthCheckOk {
257 nome: nome.to_string(),
258 })
259 );
260 println!(" latência: {latencia_ms}ms");
261}
262
263pub fn imprimir_health_check_json(nome: &str, latencia_ms: u64) {
265 let v = json!({
266 "name": nome,
267 "status": "ok",
268 "latency_ms": latencia_ms,
269 });
270 match serde_json::to_string_pretty(&v) {
271 Ok(s) => println!("{s}"),
272 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
273 }
274}
275
276#[cfg(test)]
277mod testes {
278 use super::*;
279 use crate::ssh::SaidaExecucao;
280 use crate::vps::modelo::VpsRegistro;
281 use secrecy::SecretString;
282
283 fn registro_teste() -> VpsRegistro {
284 VpsRegistro::novo(
285 "vps-teste".into(),
286 "1.2.3.4".into(),
287 22,
288 "root".into(),
289 SecretString::from("senha-super-secreta".to_string()),
290 Some(5000),
291 Some(1000),
292 Some(SecretString::from("sudo-password-longa-aqui".to_string())),
293 None,
294 )
295 }
296
297 #[test]
298 fn registro_para_json_mascarado_contem_campos_obrigatorios() {
299 let r = registro_teste();
300 let json = registro_para_json_mascarado(&r);
301 assert_eq!(json["name"], "vps-teste");
302 assert_eq!(json["host"], "1.2.3.4");
303 assert_eq!(json["port"], 22);
304 assert_eq!(json["user"], "root");
305 assert!(json["password"].as_str().unwrap().contains("..."));
306 assert!(json["sudo_password"].as_str().unwrap().contains("..."));
307 assert!(json["su_password"].is_null());
308 assert_eq!(json["timeout_ms"], 5000);
309 assert_eq!(json["max_chars"], 1000);
310 assert_eq!(json["schema_version"], 1);
311 }
312
313 #[test]
314 fn registro_para_json_mascarado_senha_sudo_nula_quando_nao_definida() {
315 let mut r = registro_teste();
316 r.senha_sudo = None;
317 let json = registro_para_json_mascarado(&r);
318 assert!(json["sudo_password"].is_null());
319 }
320
321 #[test]
322 fn registro_para_json_mascarado_su_password_presente() {
323 let mut r = registro_teste();
324 r.senha_su = Some(SecretString::from("senha-su-muito-longa-aqui".to_string()));
325 let json = registro_para_json_mascarado(&r);
326 assert!(json["su_password"].as_str().unwrap().contains("..."));
327 }
328
329 #[test]
330 fn escribir_linha_ok() {
331 let resultado = escrever_linha("teste de escrita");
332 assert!(resultado.is_ok());
333 }
334
335 #[test]
336 fn escribir_linha_com_caracteres_especiais() {
337 let resultado = escrever_linha("linha com \t tab e \"aspas\"");
338 assert!(resultado.is_ok());
339 }
340
341 #[test]
342 fn salida_execucao_completa_formatada() {
343 let saida = SaidaExecucao {
344 stdout: "output do comando".to_string(),
345 stderr: "erro do comando".to_string(),
346 exit_code: Some(0),
347 truncado_stdout: false,
348 truncado_stderr: false,
349 duracao_ms: 150,
350 };
351 let resultado = escrever_linha(&format!(
352 "stdout: {}, stderr: {}, exit: {:?}",
353 saida.stdout, saida.stderr, saida.exit_code
354 ));
355 assert!(resultado.is_ok());
356 }
357
358 #[test]
359 fn salida_execucao_sem_exit_code() {
360 let saida = SaidaExecucao {
361 stdout: "".to_string(),
362 stderr: "".to_string(),
363 exit_code: None,
364 truncado_stdout: false,
365 truncado_stderr: false,
366 duracao_ms: 0,
367 };
368 let code_str = saida
369 .exit_code
370 .map(|c| c.to_string())
371 .unwrap_or_else(|| "N/A".to_string());
372 assert_eq!(code_str, "N/A");
373 }
374
375 #[test]
376 fn vps_registro_debug_nao_expoe_senha() {
377 let r = registro_teste();
378 let json = registro_para_json_mascarado(&r);
379 let json_str = serde_json::to_string(&json).unwrap();
380 assert!(!json_str.contains("senha-super-secreta"));
381 assert!(!json_str.contains("sudo-password-longa-aqui"));
382 }
383
384 #[test]
385 fn salida_execucao_truncada_mostra_aviso() {
386 let saida = SaidaExecucao {
387 stdout: "output".to_string(),
388 stderr: "erro".to_string(),
389 exit_code: Some(1),
390 truncado_stdout: true,
391 truncado_stderr: true,
392 duracao_ms: 100,
393 };
394 assert!(saida.truncado_stdout);
395 assert!(saida.truncado_stderr);
396 }
397
398 #[test]
399 fn salida_execucao_com_exit_code_numerico() {
400 let saida = SaidaExecucao {
401 stdout: "".to_string(),
402 stderr: "".to_string(),
403 exit_code: Some(127),
404 truncado_stdout: false,
405 truncado_stderr: false,
406 duracao_ms: 0,
407 };
408 let code_str = saida
409 .exit_code
410 .map(|c| c.to_string())
411 .unwrap_or_else(|| "N/A".to_string());
412 assert_eq!(code_str, "127");
413 }
414
415 #[test]
416 fn escribir_linha_string_vazia() {
417 let resultado = escrever_linha("");
418 assert!(resultado.is_ok());
419 }
420
421 #[test]
422 fn escribir_linha_com_unicode_brasileiro() {
423 let resultado = escrever_linha("ação você está Itaú");
424 assert!(resultado.is_ok());
425 }
426
427 #[test]
428 fn escribir_linha_com_emojis() {
429 let resultado = escrever_linha("texto com 🚀 e 🔐");
430 assert!(resultado.is_ok());
431 }
432
433 #[test]
434 fn escribir_linha_com_newlines() {
435 let resultado = escrever_linha("linha1\nlinha2\nlinha3");
436 assert!(resultado.is_ok());
437 }
438
439 #[test]
440 fn escribir_linha_longo_texto() {
441 let texto_longo = "a".repeat(10000);
442 let resultado = escrever_linha(&texto_longo);
443 assert!(resultado.is_ok());
444 }
445
446 #[test]
447 fn registro_para_json_mascarado_com_senha_curta_mascara_com_asteriscos() {
448 let mut r = registro_teste();
449 r.senha = SecretString::from("curta".to_string());
450 let json = registro_para_json_mascarado(&r);
451 let senha_str = json["password"].as_str().unwrap();
452 assert_eq!(senha_str, "***");
453 }
454
455 #[test]
456 fn registro_para_json_mascarado_com_sudo_e_su_definidos() {
457 let mut r = registro_teste();
458 r.senha_sudo = Some(SecretString::from("sudo-pass-longa-aqui".to_string()));
459 r.senha_su = Some(SecretString::from("su-pass-longa-aqui".to_string()));
460 let json = registro_para_json_mascarado(&r);
461 assert!(!json["sudo_password"].is_null());
462 assert!(!json["su_password"].is_null());
463 assert!(json["sudo_password"].as_str().unwrap().contains("..."));
464 assert!(json["su_password"].as_str().unwrap().contains("..."));
465 }
466
467 #[test]
468 fn saida_execucao_formatacao_completa() {
469 let saida = SaidaExecucao {
470 stdout: "comando executado".to_string(),
471 stderr: "aviso harmless".to_string(),
472 exit_code: Some(0),
473 truncado_stdout: false,
474 truncado_stderr: false,
475 duracao_ms: 1000,
476 };
477 assert_eq!(saida.stdout, "comando executado");
478 assert_eq!(saida.stderr, "aviso harmless");
479 assert_eq!(saida.exit_code, Some(0));
480 assert_eq!(saida.duracao_ms, 1000);
481 assert!(!saida.truncado_stdout);
482 assert!(!saida.truncado_stderr);
483 }
484
485 #[test]
486 fn saida_execucao_sem_stderr() {
487 let saida = SaidaExecucao {
488 stdout: "ok".to_string(),
489 stderr: String::new(),
490 exit_code: Some(0),
491 truncado_stdout: false,
492 truncado_stderr: false,
493 duracao_ms: 50,
494 };
495 assert!(saida.stderr.is_empty());
496 }
497
498 #[test]
499 fn saida_execucao_com_sinal_em_vez_de_exit_code() {
500 let saida = SaidaExecucao {
501 stdout: String::new(),
502 stderr: "signal received".to_string(),
503 exit_code: None,
504 truncado_stdout: false,
505 truncado_stderr: false,
506 duracao_ms: 5000,
507 };
508 assert!(saida.exit_code.is_none());
509 }
510
511 #[test]
512 fn saida_execucao_json_contem_campos_obrigatorios() {
513 let saida = SaidaExecucao {
514 stdout: "output".to_string(),
515 stderr: "erro".to_string(),
516 exit_code: Some(0),
517 truncado_stdout: false,
518 truncado_stderr: false,
519 duracao_ms: 100,
520 };
521 imprimir_saida_execucao_json(&saida);
522 }
523
524 #[test]
525 fn imprimir_erro_runtime_nao_panica_com_mensagem_simples() {
526 imprimir_erro_runtime("falha ao bindar socket");
527 }
528
529 #[test]
530 fn imprimir_erro_runtime_nao_panica_com_mensagem_vazia() {
531 imprimir_erro_runtime("");
532 }
533
534 #[test]
535 fn imprimir_erro_runtime_nao_panica_com_unicode() {
536 imprimir_erro_runtime("erro acentuação: operação não concluída");
537 }
538
539 #[test]
540 fn imprimir_erro_dominio_nao_panica_com_variante_simples() {
541 let erro = crate::erros::ErroSshCli::VpsNaoEncontrada("producao".into());
542 imprimir_erro_dominio(&erro);
543 }
544
545 #[test]
546 fn imprimir_erro_dominio_nao_panica_com_variante_estruturada() {
547 let erro = crate::erros::ErroSshCli::ComandoFalhou {
548 exit_code: 127,
549 stderr: "command not found".into(),
550 };
551 imprimir_erro_dominio(&erro);
552 }
553
554 #[test]
555 fn imprimir_erro_dominio_nao_panica_com_autenticacao_falhou() {
556 let erro = crate::erros::ErroSshCli::AutenticacaoFalhou;
557 imprimir_erro_dominio(&erro);
558 }
559
560 #[test]
561 fn imprimir_erro_generico_nao_panica_com_erro_simples() {
562 let erro = anyhow::anyhow!("falha genérica no pipeline");
563 imprimir_erro_generico(&erro);
564 }
565
566 #[test]
567 fn imprimir_erro_generico_nao_panica_com_chain_de_causas() {
568 let raiz = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "acesso negado");
569 let intermediario = anyhow::Error::new(raiz).context("falha ao abrir socket");
570 let topo = intermediario.context("falha ao inicializar conexão");
571 imprimir_erro_generico(&topo);
572 }
573
574 #[test]
575 fn ler_confirmacao_aceita_s_minusculo() {
576 let input = b"s\n";
577 let mut reader: &[u8] = input;
578 let mut writer: Vec<u8> = Vec::new();
579 let r = ler_confirmacao(&mut reader, &mut writer, "prompt: ").unwrap();
580 assert!(r);
581 assert_eq!(writer, b"prompt: ");
582 }
583
584 #[test]
585 fn ler_confirmacao_aceita_sim_maiusculo() {
586 let input = b"SIM\n";
587 let mut reader: &[u8] = input;
588 let mut writer: Vec<u8> = Vec::new();
589 let r = ler_confirmacao(&mut reader, &mut writer, "p: ").unwrap();
590 assert!(r);
591 }
592
593 #[test]
594 fn ler_confirmacao_aceita_yes_com_espaco() {
595 let input = b" yes \n";
596 let mut reader: &[u8] = input;
597 let mut writer: Vec<u8> = Vec::new();
598 let r = ler_confirmacao(&mut reader, &mut writer, "p: ").unwrap();
599 assert!(r);
600 }
601
602 #[test]
603 fn ler_confirmacao_aceita_y() {
604 let input = b"y\n";
605 let mut reader: &[u8] = input;
606 let mut writer: Vec<u8> = Vec::new();
607 let r = ler_confirmacao(&mut reader, &mut writer, "p: ").unwrap();
608 assert!(r);
609 }
610
611 #[test]
612 fn ler_confirmacao_rejeita_n() {
613 let input = b"n\n";
614 let mut reader: &[u8] = input;
615 let mut writer: Vec<u8> = Vec::new();
616 let r = ler_confirmacao(&mut reader, &mut writer, "p: ").unwrap();
617 assert!(!r);
618 }
619
620 #[test]
621 fn ler_confirmacao_rejeita_linha_vazia() {
622 let input = b"\n";
623 let mut reader: &[u8] = input;
624 let mut writer: Vec<u8> = Vec::new();
625 let r = ler_confirmacao(&mut reader, &mut writer, "p: ").unwrap();
626 assert!(!r);
627 }
628
629 #[test]
630 fn ler_confirmacao_rejeita_eof() {
631 let input: &[u8] = b"";
632 let mut reader: &[u8] = input;
633 let mut writer: Vec<u8> = Vec::new();
634 let r = ler_confirmacao(&mut reader, &mut writer, "p: ").unwrap();
635 assert!(!r);
636 }
637
638 #[test]
639 fn ler_confirmacao_rejeita_texto_arbitrario() {
640 let input = b"talvez\n";
641 let mut reader: &[u8] = input;
642 let mut writer: Vec<u8> = Vec::new();
643 let r = ler_confirmacao(&mut reader, &mut writer, "p: ").unwrap();
644 assert!(!r);
645 }
646
647 #[test]
648 fn imprimir_erro_generico_com_chain_contem_multiplas_causas() {
649 let raiz = std::io::Error::new(std::io::ErrorKind::NotFound, "arquivo ausente");
650 let erro = anyhow::Error::new(raiz)
651 .context("falha ao carregar config")
652 .context("falha ao inicializar");
653 let total_causas = erro.chain().count();
654 assert!(total_causas >= 2);
655 imprimir_erro_generico(&erro);
656 }
657}