1use crate::mascaramento::mascarar;
8use crate::ssh::SaidaExecucao;
9use crate::vps::modelo::VpsRegistro;
10use secrecy::ExposeSecret;
11use serde_json::json;
12use std::io::{self, 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
32pub fn imprimir_erro(mensagem: &str) {
34 eprintln!("{mensagem}");
35}
36
37pub fn imprimir_erro_runtime(mensagem: &str) {
43 eprintln!("erro ao criar runtime: {mensagem}");
44}
45
46pub fn imprimir_erro_dominio(erro: &crate::erros::ErroSshCli) {
52 eprintln!("{erro}");
53}
54
55pub fn imprimir_erro_generico(erro: &anyhow::Error) {
61 eprintln!("{erro}");
62 for causa in erro.chain().skip(1) {
63 eprintln!(" causado por: {causa}");
64 }
65}
66
67pub fn imprimir_lista_texto(registros: &[VpsRegistro]) {
69 if registros.is_empty() {
70 println!(
71 "{}",
72 crate::i18n::t(crate::i18n::Mensagem::VpsRegistroVazio)
73 );
74 return;
75 }
76
77 println!(
78 "{:<20} {:<30} {:<6} {:<15} {:<20}",
79 "NOME", "HOST", "PORTA", "USUÁRIO", "SENHA"
80 );
81 for r in registros {
82 println!(
83 "{:<20} {:<30} {:<6} {:<15} {:<20}",
84 r.nome,
85 r.host,
86 r.porta,
87 r.usuario,
88 mascarar(r.senha.expose_secret())
89 );
90 }
91}
92
93pub fn imprimir_lista_json(registros: &[VpsRegistro]) {
95 let lista: Vec<_> = registros.iter().map(registro_para_json_mascarado).collect();
96 match serde_json::to_string_pretty(&lista) {
97 Ok(s) => println!("{s}"),
98 Err(erro) => eprintln!("erro ao serializar JSON: {erro}"),
99 }
100}
101
102pub fn imprimir_detalhes_texto(r: &VpsRegistro) {
104 println!("Nome: {}", r.nome);
105 println!("Host: {}", r.host);
106 println!("Porta: {}", r.porta);
107 println!("Usuário: {}", r.usuario);
108 println!("Senha: {}", mascarar(r.senha.expose_secret()));
109 println!(
110 "Senha sudo: {}",
111 r.senha_sudo
112 .as_ref()
113 .map_or_else(|| "(não definida)".into(), |s| mascarar(s.expose_secret()))
114 );
115 println!(
116 "Senha su: {}",
117 r.senha_su
118 .as_ref()
119 .map_or_else(|| "(não definida)".into(), |s| mascarar(s.expose_secret()))
120 );
121 println!("Timeout (ms): {}", r.timeout_ms);
122 println!("Max chars: {}", r.max_chars);
123 println!("Schema version: {}", r.schema_version);
124 println!("Adicionado em: {}", r.adicionado_em);
125}
126
127pub fn imprimir_detalhes_json(r: &VpsRegistro) {
129 let v = registro_para_json_mascarado(r);
130 match serde_json::to_string_pretty(&v) {
131 Ok(s) => println!("{s}"),
132 Err(erro) => eprintln!("erro ao serializar JSON: {erro}"),
133 }
134}
135
136fn registro_para_json_mascarado(r: &VpsRegistro) -> serde_json::Value {
137 json!({
138 "name": r.nome,
139 "host": r.host,
140 "port": r.porta,
141 "user": r.usuario,
142 "password": mascarar(r.senha.expose_secret()),
143 "sudo_password": r.senha_sudo.as_ref().map(|s| mascarar(s.expose_secret())),
144 "su_password": r.senha_su.as_ref().map(|s| mascarar(s.expose_secret())),
145 "timeout_ms": r.timeout_ms,
146 "max_chars": r.max_chars,
147 "schema_version": r.schema_version,
148 "added_at": r.adicionado_em,
149 })
150}
151
152pub fn imprimir_saida_execucao(saida: &SaidaExecucao) {
163 println!("--- stdout ---");
164 if saida.stdout.is_empty() {
165 println!("(vazio)");
166 } else {
167 println!("{}", saida.stdout);
168 }
169 println!("--- stderr ---");
170 if saida.stderr.is_empty() {
171 println!("(vazio)");
172 } else {
173 println!("{}", saida.stderr);
174 }
175 let code_str = saida
176 .exit_code
177 .map(|c| c.to_string())
178 .unwrap_or_else(|| "N/A".to_string());
179 println!("--- exit code: {} ({}ms) ---", code_str, saida.duracao_ms);
180 if saida.truncado_stdout {
181 println!("(stdout foi truncado)");
182 }
183 if saida.truncado_stderr {
184 println!("(stderr foi truncado)");
185 }
186}
187
188pub fn imprimir_saida_execucao_json(saida: &SaidaExecucao) {
190 let v = json!({
191 "stdout": saida.stdout,
192 "stderr": saida.stderr,
193 "exit_code": saida.exit_code,
194 "truncated_stdout": saida.truncado_stdout,
195 "truncated_stderr": saida.truncado_stderr,
196 "duration_ms": saida.duracao_ms,
197 });
198 match serde_json::to_string_pretty(&v) {
199 Ok(s) => println!("{s}"),
200 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
201 }
202}
203
204pub fn imprimir_health_check(nome: &str, latencia_ms: u64) {
206 println!(
207 "{}",
208 crate::i18n::t(crate::i18n::Mensagem::HealthCheckOk {
209 nome: nome.to_string(),
210 })
211 );
212 println!(" latência: {latencia_ms}ms");
213}
214
215pub fn imprimir_health_check_json(nome: &str, latencia_ms: u64) {
217 let v = json!({
218 "name": nome,
219 "status": "ok",
220 "latency_ms": latencia_ms,
221 });
222 match serde_json::to_string_pretty(&v) {
223 Ok(s) => println!("{s}"),
224 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
225 }
226}
227
228#[cfg(test)]
229mod testes {
230 use super::*;
231 use crate::ssh::SaidaExecucao;
232 use crate::vps::modelo::VpsRegistro;
233 use secrecy::SecretString;
234
235 fn registro_teste() -> VpsRegistro {
236 VpsRegistro::novo(
237 "vps-teste".into(),
238 "1.2.3.4".into(),
239 22,
240 "root".into(),
241 SecretString::from("senha-super-secreta".to_string()),
242 Some(5000),
243 Some(1000),
244 Some(SecretString::from("sudo-password-longa-aqui".to_string())),
245 None,
246 )
247 }
248
249 #[test]
250 fn registro_para_json_mascarado_contem_campos_obrigatorios() {
251 let r = registro_teste();
252 let json = registro_para_json_mascarado(&r);
253 assert_eq!(json["name"], "vps-teste");
254 assert_eq!(json["host"], "1.2.3.4");
255 assert_eq!(json["port"], 22);
256 assert_eq!(json["user"], "root");
257 assert!(json["password"].as_str().unwrap().contains("..."));
258 assert!(json["sudo_password"].as_str().unwrap().contains("..."));
259 assert!(json["su_password"].is_null());
260 assert_eq!(json["timeout_ms"], 5000);
261 assert_eq!(json["max_chars"], 1000);
262 assert_eq!(json["schema_version"], 1);
263 }
264
265 #[test]
266 fn registro_para_json_mascarado_senha_sudo_nula_quando_nao_definida() {
267 let mut r = registro_teste();
268 r.senha_sudo = None;
269 let json = registro_para_json_mascarado(&r);
270 assert!(json["sudo_password"].is_null());
271 }
272
273 #[test]
274 fn registro_para_json_mascarado_su_password_presente() {
275 let mut r = registro_teste();
276 r.senha_su = Some(SecretString::from("senha-su-muito-longa-aqui".to_string()));
277 let json = registro_para_json_mascarado(&r);
278 assert!(json["su_password"].as_str().unwrap().contains("..."));
279 }
280
281 #[test]
282 fn escribir_linha_ok() {
283 let resultado = escrever_linha("teste de escrita");
284 assert!(resultado.is_ok());
285 }
286
287 #[test]
288 fn escribir_linha_com_caracteres_especiais() {
289 let resultado = escrever_linha("linha com \t tab e \"aspas\"");
290 assert!(resultado.is_ok());
291 }
292
293 #[test]
294 fn salida_execucao_completa_formatada() {
295 let saida = SaidaExecucao {
296 stdout: "output do comando".to_string(),
297 stderr: "erro do comando".to_string(),
298 exit_code: Some(0),
299 truncado_stdout: false,
300 truncado_stderr: false,
301 duracao_ms: 150,
302 };
303 let resultado = escrever_linha(&format!(
304 "stdout: {}, stderr: {}, exit: {:?}",
305 saida.stdout, saida.stderr, saida.exit_code
306 ));
307 assert!(resultado.is_ok());
308 }
309
310 #[test]
311 fn salida_execucao_sem_exit_code() {
312 let saida = SaidaExecucao {
313 stdout: "".to_string(),
314 stderr: "".to_string(),
315 exit_code: None,
316 truncado_stdout: false,
317 truncado_stderr: false,
318 duracao_ms: 0,
319 };
320 let code_str = saida
321 .exit_code
322 .map(|c| c.to_string())
323 .unwrap_or_else(|| "N/A".to_string());
324 assert_eq!(code_str, "N/A");
325 }
326
327 #[test]
328 fn vps_registro_debug_nao_expoe_senha() {
329 let r = registro_teste();
330 let json = registro_para_json_mascarado(&r);
331 let json_str = serde_json::to_string(&json).unwrap();
332 assert!(!json_str.contains("senha-super-secreta"));
333 assert!(!json_str.contains("sudo-password-longa-aqui"));
334 }
335
336 #[test]
337 fn salida_execucao_truncada_mostra_aviso() {
338 let saida = SaidaExecucao {
339 stdout: "output".to_string(),
340 stderr: "erro".to_string(),
341 exit_code: Some(1),
342 truncado_stdout: true,
343 truncado_stderr: true,
344 duracao_ms: 100,
345 };
346 assert!(saida.truncado_stdout);
347 assert!(saida.truncado_stderr);
348 }
349
350 #[test]
351 fn salida_execucao_com_exit_code_numerico() {
352 let saida = SaidaExecucao {
353 stdout: "".to_string(),
354 stderr: "".to_string(),
355 exit_code: Some(127),
356 truncado_stdout: false,
357 truncado_stderr: false,
358 duracao_ms: 0,
359 };
360 let code_str = saida
361 .exit_code
362 .map(|c| c.to_string())
363 .unwrap_or_else(|| "N/A".to_string());
364 assert_eq!(code_str, "127");
365 }
366
367 #[test]
368 fn escribir_linha_string_vazia() {
369 let resultado = escrever_linha("");
370 assert!(resultado.is_ok());
371 }
372
373 #[test]
374 fn escribir_linha_com_unicode_brasileiro() {
375 let resultado = escrever_linha("ação você está Itaú");
376 assert!(resultado.is_ok());
377 }
378
379 #[test]
380 fn escribir_linha_com_emojis() {
381 let resultado = escrever_linha("texto com 🚀 e 🔐");
382 assert!(resultado.is_ok());
383 }
384
385 #[test]
386 fn escribir_linha_com_newlines() {
387 let resultado = escrever_linha("linha1\nlinha2\nlinha3");
388 assert!(resultado.is_ok());
389 }
390
391 #[test]
392 fn escribir_linha_longo_texto() {
393 let texto_longo = "a".repeat(10000);
394 let resultado = escrever_linha(&texto_longo);
395 assert!(resultado.is_ok());
396 }
397
398 #[test]
399 fn registro_para_json_mascarado_com_senha_curta_mascara_com_asteriscos() {
400 let mut r = registro_teste();
401 r.senha = SecretString::from("curta".to_string());
402 let json = registro_para_json_mascarado(&r);
403 let senha_str = json["password"].as_str().unwrap();
404 assert_eq!(senha_str, "***");
405 }
406
407 #[test]
408 fn registro_para_json_mascarado_com_sudo_e_su_definidos() {
409 let mut r = registro_teste();
410 r.senha_sudo = Some(SecretString::from("sudo-pass-longa-aqui".to_string()));
411 r.senha_su = Some(SecretString::from("su-pass-longa-aqui".to_string()));
412 let json = registro_para_json_mascarado(&r);
413 assert!(!json["sudo_password"].is_null());
414 assert!(!json["su_password"].is_null());
415 assert!(json["sudo_password"].as_str().unwrap().contains("..."));
416 assert!(json["su_password"].as_str().unwrap().contains("..."));
417 }
418
419 #[test]
420 fn saida_execucao_formatacao_completa() {
421 let saida = SaidaExecucao {
422 stdout: "comando executado".to_string(),
423 stderr: "aviso harmless".to_string(),
424 exit_code: Some(0),
425 truncado_stdout: false,
426 truncado_stderr: false,
427 duracao_ms: 1000,
428 };
429 assert_eq!(saida.stdout, "comando executado");
430 assert_eq!(saida.stderr, "aviso harmless");
431 assert_eq!(saida.exit_code, Some(0));
432 assert_eq!(saida.duracao_ms, 1000);
433 assert!(!saida.truncado_stdout);
434 assert!(!saida.truncado_stderr);
435 }
436
437 #[test]
438 fn saida_execucao_sem_stderr() {
439 let saida = SaidaExecucao {
440 stdout: "ok".to_string(),
441 stderr: String::new(),
442 exit_code: Some(0),
443 truncado_stdout: false,
444 truncado_stderr: false,
445 duracao_ms: 50,
446 };
447 assert!(saida.stderr.is_empty());
448 }
449
450 #[test]
451 fn saida_execucao_com_sinal_em_vez_de_exit_code() {
452 let saida = SaidaExecucao {
453 stdout: String::new(),
454 stderr: "signal received".to_string(),
455 exit_code: None,
456 truncado_stdout: false,
457 truncado_stderr: false,
458 duracao_ms: 5000,
459 };
460 assert!(saida.exit_code.is_none());
461 }
462
463 #[test]
464 fn saida_execucao_json_contem_campos_obrigatorios() {
465 let saida = SaidaExecucao {
466 stdout: "output".to_string(),
467 stderr: "erro".to_string(),
468 exit_code: Some(0),
469 truncado_stdout: false,
470 truncado_stderr: false,
471 duracao_ms: 100,
472 };
473 imprimir_saida_execucao_json(&saida);
474 }
475
476 #[test]
477 fn imprimir_erro_runtime_nao_panica_com_mensagem_simples() {
478 imprimir_erro_runtime("falha ao bindar socket");
479 }
480
481 #[test]
482 fn imprimir_erro_runtime_nao_panica_com_mensagem_vazia() {
483 imprimir_erro_runtime("");
484 }
485
486 #[test]
487 fn imprimir_erro_runtime_nao_panica_com_unicode() {
488 imprimir_erro_runtime("erro acentuação: operação não concluída");
489 }
490
491 #[test]
492 fn imprimir_erro_dominio_nao_panica_com_variante_simples() {
493 let erro = crate::erros::ErroSshCli::VpsNaoEncontrada("producao".into());
494 imprimir_erro_dominio(&erro);
495 }
496
497 #[test]
498 fn imprimir_erro_dominio_nao_panica_com_variante_estruturada() {
499 let erro = crate::erros::ErroSshCli::ComandoFalhou {
500 exit_code: 127,
501 stderr: "command not found".into(),
502 };
503 imprimir_erro_dominio(&erro);
504 }
505
506 #[test]
507 fn imprimir_erro_dominio_nao_panica_com_autenticacao_falhou() {
508 let erro = crate::erros::ErroSshCli::AutenticacaoFalhou;
509 imprimir_erro_dominio(&erro);
510 }
511
512 #[test]
513 fn imprimir_erro_generico_nao_panica_com_erro_simples() {
514 let erro = anyhow::anyhow!("falha genérica no pipeline");
515 imprimir_erro_generico(&erro);
516 }
517
518 #[test]
519 fn imprimir_erro_generico_nao_panica_com_chain_de_causas() {
520 let raiz = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "acesso negado");
521 let intermediario = anyhow::Error::new(raiz).context("falha ao abrir socket");
522 let topo = intermediario.context("falha ao inicializar conexão");
523 imprimir_erro_generico(&topo);
524 }
525
526 #[test]
527 fn imprimir_erro_generico_com_chain_contem_multiplas_causas() {
528 let raiz = std::io::Error::new(std::io::ErrorKind::NotFound, "arquivo ausente");
529 let erro = anyhow::Error::new(raiz)
530 .context("falha ao carregar config")
531 .context("falha ao inicializar");
532 let total_causas = erro.chain().count();
533 assert!(total_causas >= 2);
534 imprimir_erro_generico(&erro);
535 }
536}