1#![forbid(unsafe_code)]
4mod batch;
16mod emit;
17mod json;
18mod text;
19
20pub use batch::{
21 build_tunnel_closed, build_tunnel_listening, print_exec_batch, print_health_batch,
22 print_scp_batch, print_transfer_json, print_tunnel_closed_json, print_tunnel_listening_json,
23 TunnelClosedInput,
24};
25#[cfg(feature = "ssh-real")]
28pub use batch::{
29 print_sftp_batch, print_sftp_fs_op_json, print_sftp_list_json, print_sftp_stat_json,
30 print_sftp_transfer_json,
31};
32pub(crate) use emit::report_json_serialize_error;
33pub use emit::{
34 emit_success, emit_success_fmt, is_quiet, print_error, print_error_envelope, print_error_fmt,
35 print_human_banner, print_json_value, print_success, print_success_fmt, print_warning,
36 print_warning_fmt, set_json_errors, set_quiet, wants_json_errors, write_line, write_line_fmt,
37 write_line_to, write_line_to_fmt, write_lines, write_stderr_fmt, write_stderr_line,
38 write_stderr_line_to, write_stderr_line_to_fmt,
39};
40pub use json::{
41 export_envelope_json, export_hosts_to_json, print_details_json, print_execution_output_json,
42 print_health_check_json, print_list_json, record_to_masked_json,
43};
44pub use text::{
45 print_details_text, print_doctor_text, print_execution_output, print_health_check,
46 print_list_text,
47};
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::ssh::ExecutionOutput;
53 use crate::vps::model::VpsRecord;
54 use secrecy::SecretString;
55
56 fn registro_teste() -> VpsRecord {
57 VpsRecord::test_new(
58 "vps-teste",
59 "1.2.3.4",
60 22,
61 "root",
62 SecretString::from("senha-super-secreta".to_string()),
63 None,
64 None,
65 Some(5000),
66 Some(1000),
67 Some(1000),
68 Some(SecretString::from("sudo-password-longa-aqui".to_string())),
69 None,
70 false,
71 )
72 }
73
74 #[test]
75 fn masked_json_contains_required_fields() {
76 let r = registro_teste();
77 let m = record_to_masked_json(&r);
78 let json = serde_json::to_value(&m).unwrap();
79 assert_eq!(json["name"], "vps-teste");
80 assert_eq!(json["host"], "1.2.3.4");
81 assert_eq!(json["port"], 22);
82 assert_eq!(json["user"], "root");
83 assert_eq!(json["password"].as_str().unwrap(), "***");
84 assert_eq!(json["sudo_password"].as_str().unwrap(), "***");
85 assert!(json["su_password"].is_null());
86 assert_eq!(json["timeout_ms"], 5000);
87 assert_eq!(json["max_command_chars"], 1000);
88 assert_eq!(json["max_output_chars"], 1000);
89 assert_eq!(json["schema_version"], 3);
90 }
91
92 #[test]
93 fn masked_json_sudo_null_when_unset() {
94 let mut r = registro_teste();
95 r.sudo_password = None;
96 let json = serde_json::to_value(record_to_masked_json(&r)).unwrap();
97 assert!(json["sudo_password"].is_null());
98 }
99
100 #[test]
101 fn masked_json_su_password_present() {
102 let mut r = registro_teste();
103 r.su_password = Some(SecretString::from("senha-su-muito-longa-aqui".to_string()));
104 let json = serde_json::to_value(record_to_masked_json(&r)).unwrap();
105 assert_eq!(json["su_password"].as_str().unwrap(), "***");
106 }
107
108 #[test]
109 fn masked_json_password_null_when_empty() {
110 let mut r = registro_teste();
111 r.password = SecretString::from(String::new());
112 let json = serde_json::to_value(record_to_masked_json(&r)).unwrap();
113 assert!(json["password"].is_null());
114 }
115
116 #[test]
117 fn write_line_ok() {
118 let result = write_line("write test");
119 assert!(result.is_ok());
120 }
121
122 #[test]
123 fn write_line_special_chars() {
124 let result = write_line("line with \t tab and \"quotes\"");
125 assert!(result.is_ok());
126 }
127
128 #[test]
129 fn execution_output_fully_formatted() {
130 let output = ExecutionOutput {
131 stdout: "output do comando".to_string(),
132 stderr: "command error".to_string(),
133 exit_code: Some(0),
134 truncated_stdout: false,
135 truncated_stderr: false,
136 duration_ms: 150,
137 };
138 let result = write_line_fmt(format_args!(
139 "stdout: {}, stderr: {}, exit: {:?}",
140 output.stdout, output.stderr, output.exit_code
141 ));
142 assert!(result.is_ok());
143 }
144
145 #[test]
146 fn print_warning_fmt_composes_prefix_without_owned_string() {
147 print_warning_fmt(format_args!("timeout={t}", t = 5u64));
149 }
150
151 #[test]
152 fn execution_output_without_exit_code() {
153 let output = ExecutionOutput {
154 stdout: "".to_string(),
155 stderr: "".to_string(),
156 exit_code: None,
157 truncated_stdout: false,
158 truncated_stderr: false,
159 duration_ms: 0,
160 };
161 let code_str = output
162 .exit_code
163 .map(|c| c.to_string())
164 .unwrap_or_else(|| "N/A".to_string());
165 assert_eq!(code_str, "N/A");
166 }
167
168 #[test]
169 fn vps_record_debug_does_not_expose_password() {
170 let r = registro_teste();
171 let json_str = serde_json::to_string(&record_to_masked_json(&r)).unwrap();
172 assert!(!json_str.contains("senha-super-secreta"));
173 assert!(!json_str.contains("sudo-password-longa-aqui"));
174 assert!(!json_str.contains('\n'), "agent wire must be compact");
175 }
176
177 #[test]
178 fn execution_output_truncated_shows_warning() {
179 let output = ExecutionOutput {
180 stdout: "output".to_string(),
181 stderr: "error".to_string(),
182 exit_code: Some(1),
183 truncated_stdout: true,
184 truncated_stderr: true,
185 duration_ms: 100,
186 };
187 assert!(output.truncated_stdout);
188 assert!(output.truncated_stderr);
189 }
190
191 #[test]
192 fn execution_output_numeric_exit_code() {
193 let output = ExecutionOutput {
194 stdout: "".to_string(),
195 stderr: "".to_string(),
196 exit_code: Some(127),
197 truncated_stdout: false,
198 truncated_stderr: false,
199 duration_ms: 0,
200 };
201 let code_str = output
202 .exit_code
203 .map(|c| c.to_string())
204 .unwrap_or_else(|| "N/A".to_string());
205 assert_eq!(code_str, "127");
206 }
207
208 #[test]
209 fn write_line_empty_string() {
210 let result = write_line("");
211 assert!(result.is_ok());
212 }
213
214 #[test]
215 fn write_line_brazilian_unicode() {
216 let result = write_line("ação você está Itaú");
217 assert!(result.is_ok());
218 }
219
220 #[test]
221 fn write_line_with_emojis() {
222 let result = write_line("texto com 🚀 e 🔐");
223 assert!(result.is_ok());
224 }
225
226 #[test]
227 fn write_line_with_newlines() {
228 let result = write_line("linha1\nlinha2\nlinha3");
229 assert!(result.is_ok());
230 }
231
232 #[test]
233 fn write_line_long_text() {
234 let long_text = "a".repeat(10000);
235 let result = write_line(&long_text);
236 assert!(result.is_ok());
237 }
238
239 #[test]
240 fn masked_json_short_password_asterisks() {
241 let mut r = registro_teste();
242 r.password = SecretString::from("curta".to_string());
243 let json = serde_json::to_value(record_to_masked_json(&r)).unwrap();
244 let password_str = json["password"].as_str().unwrap();
245 assert_eq!(password_str, "***");
246 }
247
248 #[test]
249 fn masked_json_with_sudo_and_su_set() {
250 let mut r = registro_teste();
251 r.sudo_password = Some(SecretString::from("sudo-pass-longa-aqui".to_string()));
252 r.su_password = Some(SecretString::from("su-pass-longa-aqui".to_string()));
253 let json = serde_json::to_value(record_to_masked_json(&r)).unwrap();
254 assert!(!json["sudo_password"].is_null());
255 assert!(!json["su_password"].is_null());
256 assert_eq!(json["sudo_password"].as_str().unwrap(), "***");
257 assert_eq!(json["su_password"].as_str().unwrap(), "***");
258 }
259
260 #[test]
261 fn write_line_to_appends_lf_and_flushes() {
262 use std::io::Cursor;
263 let mut buf = Cursor::new(Vec::new());
264 write_line_to(&mut buf, "agent-ok").expect("write");
265 assert_eq!(String::from_utf8(buf.into_inner()).unwrap(), "agent-ok\n");
266 }
267
268 #[test]
269 fn write_line_to_fmt_avoids_owned_string() {
270 use std::io::Cursor;
271 let mut buf = Cursor::new(Vec::new());
272 let port = 22u16;
273 write_line_to_fmt(&mut buf, format_args!("port={port}")).expect("write_fmt");
274 assert_eq!(String::from_utf8(buf.into_inner()).unwrap(), "port=22\n");
275 }
276
277 #[test]
278 fn write_stderr_line_to_fmt_treats_broken_pipe_as_ok() {
279 use std::io::{self, Write};
280
281 struct Broken;
282 impl Write for Broken {
283 fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
284 Err(io::Error::new(io::ErrorKind::BrokenPipe, "closed"))
285 }
286 fn flush(&mut self) -> io::Result<()> {
287 Ok(())
288 }
289 }
290
291 write_stderr_line_to_fmt(&mut Broken, format_args!("x"))
292 .expect("EPIPE is ok on stderr fmt path");
293 }
294
295 #[test]
296 fn write_stderr_line_to_treats_broken_pipe_as_ok() {
297 use std::io::{self, Write};
298
299 struct Broken;
300 impl Write for Broken {
301 fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
302 Err(io::Error::new(io::ErrorKind::BrokenPipe, "closed"))
303 }
304 fn flush(&mut self) -> io::Result<()> {
305 Ok(())
306 }
307 }
308
309 write_stderr_line_to(&mut Broken, "x").expect("EPIPE is ok on stderr path");
310 }
311
312 #[test]
313 fn execution_output_full_formatting() {
314 let output = ExecutionOutput {
315 stdout: "comando executado".to_string(),
316 stderr: "aviso harmless".to_string(),
317 exit_code: Some(0),
318 truncated_stdout: false,
319 truncated_stderr: false,
320 duration_ms: 1000,
321 };
322 assert_eq!(output.stdout, "comando executado");
323 assert_eq!(output.stderr, "aviso harmless");
324 assert_eq!(output.exit_code, Some(0));
325 assert_eq!(output.duration_ms, 1000);
326 assert!(!output.truncated_stdout);
327 assert!(!output.truncated_stderr);
328 }
329
330 #[test]
331 fn execution_output_without_stderr() {
332 let output = ExecutionOutput {
333 stdout: "ok".to_string(),
334 stderr: String::new(),
335 exit_code: Some(0),
336 truncated_stdout: false,
337 truncated_stderr: false,
338 duration_ms: 50,
339 };
340 assert!(output.stderr.is_empty());
341 }
342
343 #[test]
344 fn execution_output_signal_instead_of_exit() {
345 let output = ExecutionOutput {
346 stdout: String::new(),
347 stderr: "signal received".to_string(),
348 exit_code: None,
349 truncated_stdout: false,
350 truncated_stderr: false,
351 duration_ms: 5000,
352 };
353 assert!(output.exit_code.is_none());
354 }
355
356 #[test]
357 fn execution_output_json_required_fields() {
358 let output = ExecutionOutput {
359 stdout: "output".to_string(),
360 stderr: "error".to_string(),
361 exit_code: Some(0),
362 truncated_stdout: false,
363 truncated_stderr: false,
364 duration_ms: 100,
365 };
366 print_execution_output_json(&output).expect("json print in unit test");
367 }
368}