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