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_lista_texto(registros: &[VpsRegistro]) {
39 if registros.is_empty() {
40 println!(
41 "{}",
42 crate::i18n::t(crate::i18n::Mensagem::VpsRegistroVazio)
43 );
44 return;
45 }
46
47 println!(
48 "{:<20} {:<30} {:<6} {:<15} {:<20}",
49 "NOME", "HOST", "PORTA", "USUÁRIO", "SENHA"
50 );
51 for r in registros {
52 println!(
53 "{:<20} {:<30} {:<6} {:<15} {:<20}",
54 r.nome,
55 r.host,
56 r.porta,
57 r.usuario,
58 mascarar(r.senha.expose_secret())
59 );
60 }
61}
62
63pub fn imprimir_lista_json(registros: &[VpsRegistro]) {
65 let lista: Vec<_> = registros.iter().map(registro_para_json_mascarado).collect();
66 match serde_json::to_string_pretty(&lista) {
67 Ok(s) => println!("{s}"),
68 Err(erro) => eprintln!("erro ao serializar JSON: {erro}"),
69 }
70}
71
72pub fn imprimir_detalhes_texto(r: &VpsRegistro) {
74 println!("Nome: {}", r.nome);
75 println!("Host: {}", r.host);
76 println!("Porta: {}", r.porta);
77 println!("Usuário: {}", r.usuario);
78 println!("Senha: {}", mascarar(r.senha.expose_secret()));
79 println!(
80 "Senha sudo: {}",
81 r.senha_sudo
82 .as_ref()
83 .map_or_else(|| "(não definida)".into(), |s| mascarar(s.expose_secret()))
84 );
85 println!(
86 "Senha su: {}",
87 r.senha_su
88 .as_ref()
89 .map_or_else(|| "(não definida)".into(), |s| mascarar(s.expose_secret()))
90 );
91 println!("Timeout (ms): {}", r.timeout_ms);
92 println!("Max chars: {}", r.max_chars);
93 println!("Schema version: {}", r.schema_version);
94 println!("Adicionado em: {}", r.adicionado_em);
95}
96
97pub fn imprimir_detalhes_json(r: &VpsRegistro) {
99 let v = registro_para_json_mascarado(r);
100 match serde_json::to_string_pretty(&v) {
101 Ok(s) => println!("{s}"),
102 Err(erro) => eprintln!("erro ao serializar JSON: {erro}"),
103 }
104}
105
106fn registro_para_json_mascarado(r: &VpsRegistro) -> serde_json::Value {
107 json!({
108 "name": r.nome,
109 "host": r.host,
110 "port": r.porta,
111 "user": r.usuario,
112 "password": mascarar(r.senha.expose_secret()),
113 "sudo_password": r.senha_sudo.as_ref().map(|s| mascarar(s.expose_secret())),
114 "su_password": r.senha_su.as_ref().map(|s| mascarar(s.expose_secret())),
115 "timeout_ms": r.timeout_ms,
116 "max_chars": r.max_chars,
117 "schema_version": r.schema_version,
118 "added_at": r.adicionado_em,
119 })
120}
121
122pub fn imprimir_saida_execucao(saida: &SaidaExecucao) {
133 println!("--- stdout ---");
134 if saida.stdout.is_empty() {
135 println!("(vazio)");
136 } else {
137 println!("{}", saida.stdout);
138 }
139 println!("--- stderr ---");
140 if saida.stderr.is_empty() {
141 println!("(vazio)");
142 } else {
143 println!("{}", saida.stderr);
144 }
145 let code_str = saida
146 .exit_code
147 .map(|c| c.to_string())
148 .unwrap_or_else(|| "N/A".to_string());
149 println!("--- exit code: {} ({}ms) ---", code_str, saida.duracao_ms);
150 if saida.truncado_stdout {
151 println!("(stdout foi truncado)");
152 }
153 if saida.truncado_stderr {
154 println!("(stderr foi truncado)");
155 }
156}
157
158pub fn imprimir_saida_execucao_json(saida: &SaidaExecucao) {
160 let v = json!({
161 "stdout": saida.stdout,
162 "stderr": saida.stderr,
163 "exit_code": saida.exit_code,
164 "truncated_stdout": saida.truncado_stdout,
165 "truncated_stderr": saida.truncado_stderr,
166 "duration_ms": saida.duracao_ms,
167 });
168 match serde_json::to_string_pretty(&v) {
169 Ok(s) => println!("{s}"),
170 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
171 }
172}
173
174pub fn imprimir_health_check(nome: &str, latencia_ms: u64) {
176 println!(
177 "{}",
178 crate::i18n::t(crate::i18n::Mensagem::HealthCheckOk {
179 nome: nome.to_string(),
180 })
181 );
182 println!(" latência: {latencia_ms}ms");
183}
184
185pub fn imprimir_health_check_json(nome: &str, latencia_ms: u64) {
187 let v = json!({
188 "name": nome,
189 "status": "ok",
190 "latency_ms": latencia_ms,
191 });
192 match serde_json::to_string_pretty(&v) {
193 Ok(s) => println!("{s}"),
194 Err(e) => eprintln!("erro ao serializar JSON: {e}"),
195 }
196}
197
198#[cfg(test)]
199mod testes {
200 use super::*;
201 use crate::ssh::SaidaExecucao;
202 use crate::vps::modelo::VpsRegistro;
203 use secrecy::SecretString;
204
205 fn registro_teste() -> VpsRegistro {
206 VpsRegistro::novo(
207 "vps-teste".into(),
208 "1.2.3.4".into(),
209 22,
210 "root".into(),
211 SecretString::from("senha-super-secreta".to_string()),
212 Some(5000),
213 Some(1000),
214 Some(SecretString::from("sudo-password-longa-aqui".to_string())),
215 None,
216 )
217 }
218
219 #[test]
220 fn registro_para_json_mascarado_contem_campos_obrigatorios() {
221 let r = registro_teste();
222 let json = registro_para_json_mascarado(&r);
223 assert_eq!(json["name"], "vps-teste");
224 assert_eq!(json["host"], "1.2.3.4");
225 assert_eq!(json["port"], 22);
226 assert_eq!(json["user"], "root");
227 assert!(json["password"].as_str().unwrap().contains("..."));
228 assert!(json["sudo_password"].as_str().unwrap().contains("..."));
229 assert!(json["su_password"].is_null());
230 assert_eq!(json["timeout_ms"], 5000);
231 assert_eq!(json["max_chars"], 1000);
232 assert_eq!(json["schema_version"], 1);
233 }
234
235 #[test]
236 fn registro_para_json_mascarado_senha_sudo_nula_quando_nao_definida() {
237 let mut r = registro_teste();
238 r.senha_sudo = None;
239 let json = registro_para_json_mascarado(&r);
240 assert!(json["sudo_password"].is_null());
241 }
242
243 #[test]
244 fn registro_para_json_mascarado_su_password_presente() {
245 let mut r = registro_teste();
246 r.senha_su = Some(SecretString::from("senha-su-muito-longa-aqui".to_string()));
247 let json = registro_para_json_mascarado(&r);
248 assert!(json["su_password"].as_str().unwrap().contains("..."));
249 }
250
251 #[test]
252 fn escribir_linha_ok() {
253 let resultado = escrever_linha("teste de escrita");
254 assert!(resultado.is_ok());
255 }
256
257 #[test]
258 fn escribir_linha_com_caracteres_especiais() {
259 let resultado = escrever_linha("linha com \t tab e \"aspas\"");
260 assert!(resultado.is_ok());
261 }
262
263 #[test]
264 fn salida_execucao_completa_formatada() {
265 let saida = SaidaExecucao {
266 stdout: "output do comando".to_string(),
267 stderr: "erro do comando".to_string(),
268 exit_code: Some(0),
269 truncado_stdout: false,
270 truncado_stderr: false,
271 duracao_ms: 150,
272 };
273 let resultado = escrever_linha(&format!(
274 "stdout: {}, stderr: {}, exit: {:?}",
275 saida.stdout, saida.stderr, saida.exit_code
276 ));
277 assert!(resultado.is_ok());
278 }
279
280 #[test]
281 fn salida_execucao_sem_exit_code() {
282 let saida = SaidaExecucao {
283 stdout: "".to_string(),
284 stderr: "".to_string(),
285 exit_code: None,
286 truncado_stdout: false,
287 truncado_stderr: false,
288 duracao_ms: 0,
289 };
290 let code_str = saida
291 .exit_code
292 .map(|c| c.to_string())
293 .unwrap_or_else(|| "N/A".to_string());
294 assert_eq!(code_str, "N/A");
295 }
296
297 #[test]
298 fn vps_registro_debug_nao_expoe_senha() {
299 let r = registro_teste();
300 let json = registro_para_json_mascarado(&r);
301 let json_str = serde_json::to_string(&json).unwrap();
302 assert!(!json_str.contains("senha-super-secreta"));
303 assert!(!json_str.contains("sudo-password-longa-aqui"));
304 }
305
306 #[test]
307 fn salida_execucao_truncada_mostra_aviso() {
308 let saida = SaidaExecucao {
309 stdout: "output".to_string(),
310 stderr: "erro".to_string(),
311 exit_code: Some(1),
312 truncado_stdout: true,
313 truncado_stderr: true,
314 duracao_ms: 100,
315 };
316 assert!(saida.truncado_stdout);
317 assert!(saida.truncado_stderr);
318 }
319
320 #[test]
321 fn salida_execucao_com_exit_code_numerico() {
322 let saida = SaidaExecucao {
323 stdout: "".to_string(),
324 stderr: "".to_string(),
325 exit_code: Some(127),
326 truncado_stdout: false,
327 truncado_stderr: false,
328 duracao_ms: 0,
329 };
330 let code_str = saida
331 .exit_code
332 .map(|c| c.to_string())
333 .unwrap_or_else(|| "N/A".to_string());
334 assert_eq!(code_str, "127");
335 }
336
337 #[test]
338 fn escribir_linha_string_vazia() {
339 let resultado = escrever_linha("");
340 assert!(resultado.is_ok());
341 }
342
343 #[test]
344 fn escribir_linha_com_unicode_brasileiro() {
345 let resultado = escrever_linha("ação você está Itaú");
346 assert!(resultado.is_ok());
347 }
348
349 #[test]
350 fn escribir_linha_com_emojis() {
351 let resultado = escrever_linha("texto com 🚀 e 🔐");
352 assert!(resultado.is_ok());
353 }
354
355 #[test]
356 fn escribir_linha_com_newlines() {
357 let resultado = escrever_linha("linha1\nlinha2\nlinha3");
358 assert!(resultado.is_ok());
359 }
360
361 #[test]
362 fn escribir_linha_longo_texto() {
363 let texto_longo = "a".repeat(10000);
364 let resultado = escrever_linha(&texto_longo);
365 assert!(resultado.is_ok());
366 }
367
368 #[test]
369 fn registro_para_json_mascarado_com_senha_curta_mascara_com_asteriscos() {
370 let mut r = registro_teste();
371 r.senha = SecretString::from("curta".to_string());
372 let json = registro_para_json_mascarado(&r);
373 let senha_str = json["password"].as_str().unwrap();
374 assert_eq!(senha_str, "***");
375 }
376
377 #[test]
378 fn registro_para_json_mascarado_com_sudo_e_su_definidos() {
379 let mut r = registro_teste();
380 r.senha_sudo = Some(SecretString::from("sudo-pass-longa-aqui".to_string()));
381 r.senha_su = Some(SecretString::from("su-pass-longa-aqui".to_string()));
382 let json = registro_para_json_mascarado(&r);
383 assert!(!json["sudo_password"].is_null());
384 assert!(!json["su_password"].is_null());
385 assert!(json["sudo_password"].as_str().unwrap().contains("..."));
386 assert!(json["su_password"].as_str().unwrap().contains("..."));
387 }
388
389 #[test]
390 fn saida_execucao_formatacao_completa() {
391 let saida = SaidaExecucao {
392 stdout: "comando executado".to_string(),
393 stderr: "aviso harmless".to_string(),
394 exit_code: Some(0),
395 truncado_stdout: false,
396 truncado_stderr: false,
397 duracao_ms: 1000,
398 };
399 assert_eq!(saida.stdout, "comando executado");
400 assert_eq!(saida.stderr, "aviso harmless");
401 assert_eq!(saida.exit_code, Some(0));
402 assert_eq!(saida.duracao_ms, 1000);
403 assert!(!saida.truncado_stdout);
404 assert!(!saida.truncado_stderr);
405 }
406
407 #[test]
408 fn saida_execucao_sem_stderr() {
409 let saida = SaidaExecucao {
410 stdout: "ok".to_string(),
411 stderr: String::new(),
412 exit_code: Some(0),
413 truncado_stdout: false,
414 truncado_stderr: false,
415 duracao_ms: 50,
416 };
417 assert!(saida.stderr.is_empty());
418 }
419
420 #[test]
421 fn saida_execucao_com_sinal_em_vez_de_exit_code() {
422 let saida = SaidaExecucao {
423 stdout: String::new(),
424 stderr: "signal received".to_string(),
425 exit_code: None,
426 truncado_stdout: false,
427 truncado_stderr: false,
428 duracao_ms: 5000,
429 };
430 assert!(saida.exit_code.is_none());
431 }
432
433 #[test]
434 fn saida_execucao_json_contem_campos_obrigatorios() {
435 let saida = SaidaExecucao {
436 stdout: "output".to_string(),
437 stderr: "erro".to_string(),
438 exit_code: Some(0),
439 truncado_stdout: false,
440 truncado_stderr: false,
441 duracao_ms: 100,
442 };
443 imprimir_saida_execucao_json(&saida);
444 }
445}