1pub mod engine;
14
15pub use engine::{RedactError, RedactionConfig, RedactionEngine};
16
17#[cfg(test)]
18mod tests {
19 #![allow(
20 clippy::unwrap_used,
21 clippy::expect_used,
22 clippy::panic,
23 clippy::indexing_slicing
24 )]
25
26 use super::*;
27 use serde_json::json;
28
29 fn default_engine() -> RedactionEngine {
31 RedactionEngine::new(RedactionConfig::default()).unwrap()
32 }
33
34 fn _assert_send_sync<T: Send + Sync>() {}
39
40 #[test]
41 fn engine_is_send_and_sync() {
42 _assert_send_sync::<RedactionEngine>();
43 }
44
45 #[test]
50 fn bearer_token_is_redacted() {
51 let engine = default_engine();
52 let mut val = json!("Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123");
53 engine.redact_value(&mut val);
54 assert_eq!(val, json!("[REDACTED]"));
55 }
56
57 #[test]
58 fn bearer_in_context_preserves_surrounding_text() {
59 let engine = default_engine();
60 let mut val = json!("Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.abc123XY");
61 engine.redact_value(&mut val);
62 let s = val.as_str().unwrap();
63 assert!(s.starts_with("Authorization: [REDACTED]"), "got: {s}");
64 assert!(!s.contains("eyJ"));
65 }
66
67 #[test]
68 fn bearer_of_bad_news_is_not_redacted() {
69 let engine = default_engine();
70 let mut val = json!("Bearer of bad news");
71 engine.redact_value(&mut val);
72 assert_eq!(val, json!("Bearer of bad news"));
73 }
74
75 #[test]
80 fn sk_api_key_is_redacted() {
81 let engine = default_engine();
82 let mut val = json!("sk-abc123def456ghi789jk");
83 engine.redact_value(&mut val);
84 let s = val.as_str().unwrap();
85 assert!(s.contains("[REDACTED]"), "got: {s}");
86 assert!(!s.contains("sk-abc123"));
87 }
88
89 #[test]
90 fn key_api_key_is_redacted() {
91 let engine = default_engine();
92 let mut val = json!("key-ABCDEF0123456789XY");
93 engine.redact_value(&mut val);
94 let s = val.as_str().unwrap();
95 assert!(s.contains("[REDACTED]"), "got: {s}");
96 }
97
98 #[test]
99 fn key_value_is_not_redacted() {
100 let engine = default_engine();
101 let mut val = json!("key-value");
102 engine.redact_value(&mut val);
103 assert_eq!(val, json!("key-value"));
104 }
105
106 #[test]
107 fn segmented_api_keys_are_redacted_in_full() {
108 let engine = default_engine();
109 for key in [
110 "sk-proj-Ab3dEf_Gh1jK-Lm4nOp5qRs6tUv7wXy8zA9bC0dE_",
111 "sk-svcacct-AbCdEf0123456789_-",
112 "sk-admin-AbCdEf0123456789_-",
113 "sk-ant-api03-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789-_AbCdAA",
114 "key-AbCdEf0123456789_-",
115 ] {
116 let mut value = json!(format!("key: {key}; done"));
117 engine.redact_value(&mut value);
118 assert_eq!(value, json!("key: [REDACTED]; done"));
119 }
120 }
121
122 #[test]
123 fn sensitive_values_touching_unicode_text_are_redacted() {
124 let engine = default_engine();
125 for (input, expected) in [
126 ("カード番号は4111111111111111です", "カード番号は[REDACTED]です"),
127 ("社会保障番号123-45-6789です", "社会保障番号[REDACTED]です"),
128 ("IP地址8.8.8.8です", "IP地址[REDACTED]です"),
129 ("密钥sk-proj-AbCdEf0123456789_です", "密钥[REDACTED]です"),
130 ("Nº4111111111111111", "Nº[REDACTED]"),
131 ] {
132 let mut value = json!(input);
133 engine.redact_value(&mut value);
134 assert_eq!(value, json!(expected));
135 }
136 }
137
138 #[test]
139 fn task_something_is_not_redacted() {
140 let engine = default_engine();
141 let mut val = json!("task-manager");
142 engine.redact_value(&mut val);
143 assert_eq!(val, json!("task-manager"));
144 }
145
146 #[test]
151 fn email_is_redacted() {
152 let engine = default_engine();
153 let mut val = json!("user@example.com");
154 engine.redact_value(&mut val);
155 let s = val.as_str().unwrap();
156 assert!(s.contains("[REDACTED]"), "got: {s}");
157 assert!(!s.contains("user@"));
158 }
159
160 #[test]
161 fn email_in_sentence_preserves_context() {
162 let engine = default_engine();
163 let mut val = json!("Contact alice@example.org for help");
164 engine.redact_value(&mut val);
165 let s = val.as_str().unwrap();
166 assert!(s.starts_with("Contact "), "got: {s}");
167 assert!(s.ends_with(" for help"), "got: {s}");
168 assert!(!s.contains("alice@"));
169 }
170
171 #[test]
172 fn non_email_at_sign_is_not_redacted() {
173 let engine = default_engine();
174 let mut val = json!("@mention in chat");
175 engine.redact_value(&mut val);
176 assert_eq!(val, json!("@mention in chat"));
177 }
178
179 #[test]
184 fn valid_visa_card_is_redacted() {
185 let engine = default_engine();
186 let mut val = json!("4111111111111111");
188 engine.redact_value(&mut val);
189 let s = val.as_str().unwrap();
190 assert!(s.contains("[REDACTED]"), "got: {s}");
191 }
192
193 #[test]
194 fn valid_card_with_dashes_is_redacted() {
195 let engine = default_engine();
196 let mut val = json!("4111-1111-1111-1111");
197 engine.redact_value(&mut val);
198 let s = val.as_str().unwrap();
199 assert!(s.contains("[REDACTED]"), "got: {s}");
200 }
201
202 #[test]
203 fn valid_card_with_spaces_is_redacted() {
204 let engine = default_engine();
205 let mut val = json!("4111 1111 1111 1111");
206 engine.redact_value(&mut val);
207 let s = val.as_str().unwrap();
208 assert!(s.contains("[REDACTED]"), "got: {s}");
209 }
210
211 #[test]
212 fn invalid_luhn_card_is_not_redacted() {
213 let engine = default_engine();
214 let mut val = json!("4111111111111112");
216 engine.redact_value(&mut val);
217 assert_eq!(val, json!("4111111111111112"));
218 }
219
220 #[test]
225 fn ssn_is_redacted() {
226 let engine = default_engine();
227 let mut val = json!("SSN: 123-45-6789");
228 engine.redact_value(&mut val);
229 let s = val.as_str().unwrap();
230 assert!(s.contains("[REDACTED]"), "got: {s}");
231 assert!(!s.contains("123-45-6789"));
232 }
233
234 #[test]
235 fn non_ssn_dashes_are_not_redacted() {
236 let engine = default_engine();
237 let mut val = json!("phone: 555-1234");
238 engine.redact_value(&mut val);
239 assert_eq!(val, json!("phone: 555-1234"));
240 }
241
242 #[test]
247 fn public_ipv4_is_redacted() {
248 let engine = default_engine();
249 let mut val = json!("Connected from 8.8.8.8 on port 443");
250 engine.redact_value(&mut val);
251 let s = val.as_str().unwrap();
252 assert!(s.contains("[REDACTED]"), "got: {s}");
253 assert!(!s.contains("8.8.8.8"));
254 assert!(s.contains("on port 443"), "surrounding text lost: {s}");
255 }
256
257 #[test]
258 fn private_10_x_is_not_redacted() {
259 let engine = default_engine();
260 let mut val = json!("10.0.0.1");
261 engine.redact_value(&mut val);
262 assert_eq!(val, json!("10.0.0.1"));
263 }
264
265 #[test]
266 fn private_172_16_is_not_redacted() {
267 let engine = default_engine();
268 let mut val = json!("172.16.0.1");
269 engine.redact_value(&mut val);
270 assert_eq!(val, json!("172.16.0.1"));
271 }
272
273 #[test]
274 fn private_172_31_is_not_redacted() {
275 let engine = default_engine();
276 let mut val = json!("172.31.255.255");
277 engine.redact_value(&mut val);
278 assert_eq!(val, json!("172.31.255.255"));
279 }
280
281 #[test]
282 fn public_172_15_is_redacted() {
283 let engine = default_engine();
284 let mut val = json!("172.15.255.255");
285 engine.redact_value(&mut val);
286 let s = val.as_str().unwrap();
287 assert!(s.contains("[REDACTED]"), "got: {s}");
288 }
289
290 #[test]
291 fn public_172_32_is_redacted() {
292 let engine = default_engine();
293 let mut val = json!("172.32.0.0");
294 engine.redact_value(&mut val);
295 let s = val.as_str().unwrap();
296 assert!(s.contains("[REDACTED]"), "got: {s}");
297 }
298
299 #[test]
300 fn private_192_168_is_not_redacted() {
301 let engine = default_engine();
302 let mut val = json!("192.168.1.1");
303 engine.redact_value(&mut val);
304 assert_eq!(val, json!("192.168.1.1"));
305 }
306
307 #[test]
308 fn invalid_ipv4_999_is_not_redacted() {
309 let engine = default_engine();
310 let mut val = json!("999.1.1.1");
311 engine.redact_value(&mut val);
312 assert_eq!(val, json!("999.1.1.1"));
313 }
314
315 #[test]
320 fn custom_pattern_works() {
321 let config = RedactionConfig {
322 regex_patterns: vec![r"secret_[a-z]+".to_owned()],
323 include_builtin_patterns: false,
324 ..RedactionConfig::default()
325 };
326 let engine = RedactionEngine::new(config).unwrap();
327 let mut val = json!("my secret_token is here");
328 engine.redact_value(&mut val);
329 let s = val.as_str().unwrap();
330 assert!(s.contains("[REDACTED]"), "got: {s}");
331 assert!(!s.contains("secret_token"));
332 assert!(s.contains("my "));
333 }
334
335 #[test]
336 fn invalid_regex_returns_error() {
337 let config = RedactionConfig {
338 regex_patterns: vec![r"[invalid".to_owned()],
339 include_builtin_patterns: false,
340 ..RedactionConfig::default()
341 };
342 let result = RedactionEngine::new(config);
343 assert!(result.is_err());
344 let err = result.unwrap_err();
345 assert!(matches!(err, RedactError::InvalidPattern { .. }));
346 }
347
348 #[test]
353 fn custom_replacement_string() {
354 let config = RedactionConfig {
355 regex_patterns: vec![r"secret".to_owned()],
356 replacement: "***".to_owned(),
357 include_builtin_patterns: false,
358 ..RedactionConfig::default()
359 };
360 let engine = RedactionEngine::new(config).unwrap();
361 let mut val = json!("the secret is here");
362 engine.redact_value(&mut val);
363 assert_eq!(val, json!("the *** is here"));
364 }
365
366 #[test]
371 fn pointer_path_redacts_string_value() {
372 let config = RedactionConfig {
373 pointer_paths: vec!["/password".to_owned()],
374 include_builtin_patterns: false,
375 ..RedactionConfig::default()
376 };
377 let engine = RedactionEngine::new(config).unwrap();
378 let mut val = json!({"username": "alice", "password": "hunter2"});
379 engine.redact_value(&mut val);
380 assert_eq!(val["password"], json!("[REDACTED]"));
381 assert_eq!(val["username"], json!("alice"));
382 }
383
384 #[test]
385 fn pointer_path_redacts_nested_value() {
386 let config = RedactionConfig {
387 pointer_paths: vec!["/credentials/secret".to_owned()],
388 include_builtin_patterns: false,
389 ..RedactionConfig::default()
390 };
391 let engine = RedactionEngine::new(config).unwrap();
392 let mut val = json!({"credentials": {"secret": "abc", "id": "xyz"}});
393 engine.redact_value(&mut val);
394 assert_eq!(val["credentials"]["secret"], json!("[REDACTED]"));
395 assert_eq!(val["credentials"]["id"], json!("xyz"));
396 }
397
398 #[test]
399 fn pointer_path_redacts_non_string_value() {
400 let config = RedactionConfig {
401 pointer_paths: vec!["/debug_flag".to_owned()],
402 include_builtin_patterns: false,
403 ..RedactionConfig::default()
404 };
405 let engine = RedactionEngine::new(config).unwrap();
406 let mut val = json!({"debug_flag": true, "name": "test"});
407 engine.redact_value(&mut val);
408 assert_eq!(val["debug_flag"], json!("[REDACTED]"));
409 assert_eq!(val["name"], json!("test"));
410 }
411
412 #[test]
413 fn invalid_pointer_path_returns_error() {
414 let config = RedactionConfig {
415 pointer_paths: vec!["no-leading-slash".to_owned()],
416 include_builtin_patterns: false,
417 ..RedactionConfig::default()
418 };
419 let result = RedactionEngine::new(config);
420 assert!(result.is_err());
421 assert!(matches!(
422 result.unwrap_err(),
423 RedactError::InvalidPointerPath { .. }
424 ));
425 }
426
427 #[test]
428 fn missing_pointer_path_is_harmless() {
429 let config = RedactionConfig {
430 pointer_paths: vec!["/nonexistent".to_owned()],
431 include_builtin_patterns: false,
432 ..RedactionConfig::default()
433 };
434 let engine = RedactionEngine::new(config).unwrap();
435 let mut val = json!({"name": "alice"});
436 engine.redact_value(&mut val);
437 assert_eq!(val, json!({"name": "alice"}));
438 }
439
440 #[test]
445 fn idempotency_across_all_patterns() {
446 let engine = default_engine();
447 let fixtures = vec![
448 json!("Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123"),
449 json!("sk-abc123def456ghi789jk"),
450 json!("user@example.com"),
451 json!("4111111111111111"),
452 json!("123-45-6789"),
453 json!("8.8.8.8"),
454 json!({"password": "secret", "email": "a@b.com"}),
455 json!(["Bearer token123456789012", "10.0.0.1"]),
456 ];
457 for original in fixtures {
458 let mut first = original.clone();
459 engine.redact_value(&mut first);
460 let mut second = first.clone();
461 engine.redact_value(&mut second);
462 assert_eq!(first, second, "idempotency failed for {original}");
463 }
464 }
465
466 #[test]
467 fn replacement_string_is_not_re_redacted() {
468 let engine = default_engine();
469 let mut val = json!("[REDACTED]");
470 engine.redact_value(&mut val);
471 assert_eq!(val, json!("[REDACTED]"));
472 }
473
474 #[test]
479 fn nested_objects_are_walked() {
480 let engine = default_engine();
481 let mut val = json!({
482 "outer": {
483 "inner": {
484 "deep": "user@example.com"
485 }
486 }
487 });
488 engine.redact_value(&mut val);
489 let deep = val["outer"]["inner"]["deep"].as_str().unwrap();
490 assert!(deep.contains("[REDACTED]"), "got: {deep}");
491 }
492
493 #[test]
494 fn array_elements_are_walked() {
495 let engine = default_engine();
496 let mut val = json!([
497 "safe text",
498 "user@example.com",
499 {"key": "123-45-6789"}
500 ]);
501 engine.redact_value(&mut val);
502 assert_eq!(val[0], json!("safe text"));
503 let email = val[1].as_str().unwrap();
504 assert!(email.contains("[REDACTED]"), "email not redacted: {email}");
505 let ssn = val[2]["key"].as_str().unwrap();
506 assert!(ssn.contains("[REDACTED]"), "SSN not redacted: {ssn}");
507 }
508
509 #[test]
514 fn numbers_are_not_modified() {
515 let engine = default_engine();
516 let mut val = json!({"count": 42, "price": 9.99});
517 engine.redact_value(&mut val);
518 assert_eq!(val["count"], json!(42));
519 assert_eq!(val["price"], json!(9.99));
520 }
521
522 #[test]
523 fn user_integer_cards_are_redacted_without_changing_typed_event_numbers() {
524 let engine = default_engine();
525 let input = json!({"card": 4_111_111_111_111_111_u64, "count": 42, "price": 9.99});
526 let mut event = input.clone();
527 engine.redact_value(&mut event);
528 assert_eq!(event, input);
529 let mut arguments = input;
530 engine.redact_user_value(&mut arguments);
531 assert_eq!(
532 arguments,
533 json!({"card": "[REDACTED]", "count": 42, "price": 9.99})
534 );
535 }
536
537 #[test]
538 fn redacted_user_keys_preserve_colliding_values_and_are_idempotent() {
539 let engine = default_engine();
540 let mut value = json!({"share_with": {
541 "alice@example.com": "editor",
542 "bob@example.com": ["viewer", "reader"],
543 "[REDACTED]": "owner",
544 "safe": "kept"
545 }});
546 engine.redact_user_value(&mut value);
547 assert_eq!(
548 value,
549 json!({"share_with": {
550 "[REDACTED]": ["editor", ["viewer", "reader"], "owner"],
551 "safe": "kept"
552 }})
553 );
554 let once = value.clone();
555 engine.redact_user_value(&mut value);
556 assert_eq!(value, once);
557 }
558
559 #[test]
560 fn booleans_are_not_modified() {
561 let engine = default_engine();
562 let mut val = json!({"active": true, "deleted": false});
563 engine.redact_value(&mut val);
564 assert_eq!(val["active"], json!(true));
565 assert_eq!(val["deleted"], json!(false));
566 }
567
568 #[test]
569 fn null_is_not_modified() {
570 let engine = default_engine();
571 let mut val = json!({"field": null});
572 engine.redact_value(&mut val);
573 assert!(val["field"].is_null());
574 }
575
576 #[test]
581 fn empty_string_is_left_alone() {
582 let engine = default_engine();
583 let mut val = json!("");
584 engine.redact_value(&mut val);
585 assert_eq!(val, json!(""));
586 }
587
588 #[test]
589 fn whitespace_only_string_is_left_alone() {
590 let engine = default_engine();
591 let mut val = json!(" \t\n ");
592 engine.redact_value(&mut val);
593 assert_eq!(val, json!(" \t\n "));
594 }
595
596 #[test]
601 fn redact_bytes_round_trip() {
602 let engine = default_engine();
603 let input = br#"{"email":"user@example.com","safe":"hello"}"#;
604 let output = engine.redact_bytes(input).unwrap();
605 let parsed: serde_json::Value = serde_json::from_slice(&output).unwrap();
606 let email = parsed["email"].as_str().unwrap();
607 assert!(email.contains("[REDACTED]"), "got: {email}");
608 assert_eq!(parsed["safe"], json!("hello"));
609 }
610
611 #[test]
612 fn redact_bytes_invalid_json_returns_error() {
613 let engine = default_engine();
614 let result = engine.redact_bytes(b"not json");
615 assert!(result.is_err());
616 assert!(matches!(result.unwrap_err(), RedactError::JsonParse(_)));
617 }
618
619 #[test]
620 fn redact_bytes_idempotency() {
621 let engine = default_engine();
622 let input = br#"{"token":"Bearer eyJhbGciOiJIUzI1NiJ9.test1234"}"#;
623 let first = engine.redact_bytes(input).unwrap();
624 let second = engine.redact_bytes(&first).unwrap();
625 assert_eq!(first, second);
626 }
627
628 #[test]
633 fn uuid_is_not_redacted() {
634 let engine = default_engine();
635 let mut val = json!("550e8400-e29b-41d4-a716-446655440000");
636 engine.redact_value(&mut val);
637 assert_eq!(val, json!("550e8400-e29b-41d4-a716-446655440000"));
638 }
639
640 #[test]
641 fn iso_date_is_not_redacted() {
642 let engine = default_engine();
643 let mut val = json!("2024-01-15T10:30:00Z");
644 engine.redact_value(&mut val);
645 assert_eq!(val, json!("2024-01-15T10:30:00Z"));
646 }
647
648 #[test]
649 fn plain_text_is_not_redacted() {
650 let engine = default_engine();
651 let mut val = json!("This is a normal log message with no sensitive data");
652 engine.redact_value(&mut val);
653 assert_eq!(val, json!("This is a normal log message with no sensitive data"));
654 }
655
656 #[test]
661 fn replacement_matching_a_pattern_is_rejected() {
662 let config = RedactionConfig {
663 regex_patterns: vec![r"REDACTED".to_owned()],
664 replacement: "REDACTED".to_owned(),
665 include_builtin_patterns: false,
666 ..RedactionConfig::default()
667 };
668 let result = RedactionEngine::new(config);
669 assert!(result.is_err());
670 assert!(matches!(
671 result.unwrap_err(),
672 RedactError::ReplacementMatchesPattern { .. }
673 ));
674 }
675
676 #[test]
681 fn mixed_payload_redaction() {
682 let engine = default_engine();
683 let mut val = json!({
684 "event": "login",
685 "user_email": "admin@corp.io",
686 "source_ip": "203.0.113.5",
687 "internal_ip": "10.1.2.3",
688 "metadata": {
689 "auth_header": "Bearer eyJhbGciOiJIUzI1NiJ9.test1234",
690 "timestamp": "2024-01-15T10:30:00Z",
691 "count": 42
692 },
693 "tags": ["production", "us-east"]
694 });
695 engine.redact_value(&mut val);
696
697 let email = val["user_email"].as_str().unwrap();
699 assert!(email.contains("[REDACTED]"), "email not redacted: {email}");
700
701 let src_ip = val["source_ip"].as_str().unwrap();
703 assert!(src_ip.contains("[REDACTED]"), "public IP not redacted: {src_ip}");
704
705 assert_eq!(val["internal_ip"], json!("10.1.2.3"));
707
708 let auth = val["metadata"]["auth_header"].as_str().unwrap();
710 assert!(auth.contains("[REDACTED]"), "bearer not redacted: {auth}");
711
712 assert_eq!(val["metadata"]["timestamp"], json!("2024-01-15T10:30:00Z"));
714
715 assert_eq!(val["metadata"]["count"], json!(42));
717
718 assert_eq!(val["tags"][0], json!("production"));
720 }
721
722 #[test]
727 fn card_with_trailing_cvv_is_redacted() {
728 let engine = default_engine();
729 let mut val = json!("4111111111111111 123");
730 engine.redact_value(&mut val);
731 assert_eq!(val, json!("[REDACTED] 123"));
732 }
733
734 #[test]
735 fn card_with_trailing_year_is_redacted() {
736 let engine = default_engine();
737 let mut val = json!("4111 1111 1111 1111 2025");
738 engine.redact_value(&mut val);
739 assert_eq!(val, json!("[REDACTED] 2025"));
740 }
741
742 #[test]
743 fn card_with_leading_ref_number() {
744 let engine = default_engine();
745 let mut val = json!("Ref 1234 4111 1111 1111 1111");
746 engine.redact_value(&mut val);
747 assert_eq!(val, json!("Ref 1234 [REDACTED]"));
748 }
749
750 #[test]
751 fn amex_card_in_standard_spacing() {
752 let engine = default_engine();
753 let mut val = json!("3782 822463 10005");
755 engine.redact_value(&mut val);
756 assert_eq!(val, json!("[REDACTED]"));
757 }
758
759 #[test]
760 fn card_extraction_prefers_the_longest_valid_window() {
761 let engine = default_engine();
762 let mut value = json!("000 000 000 000 000 000");
764 engine.redact_value(&mut value);
765 assert_eq!(value, json!("[REDACTED]"));
766 }
767
768 #[test]
769 fn long_digit_group_runs_complete_with_bounded_work() {
770 let engine = default_engine();
771 let input = "100 ".repeat(100_000);
774 let mut value = json!(input);
775 let started = std::time::Instant::now();
776 engine.redact_value(&mut value);
777 assert!(started.elapsed() < std::time::Duration::from_secs(10));
778 assert_eq!(value, json!(input));
779 }
780
781 #[test]
786 fn adjacent_email_and_ssn_idempotency() {
787 let engine = default_engine();
788 let mut val = json!("user@example.com123-45-6789");
789 engine.redact_value(&mut val);
790 let s = val.as_str().unwrap();
792 assert!(!s.contains("user@"), "email leaked: {s}");
793 assert!(!s.contains("123-45-6789"), "SSN leaked: {s}");
794 let mut second = val.clone();
796 engine.redact_value(&mut second);
797 assert_eq!(val, second, "idempotency failed for adjacent email+SSN");
798 }
799
800 #[test]
805 fn lowercase_bearer_is_redacted() {
806 let engine = default_engine();
807 let mut val = json!("bearer eyJhbGciOiJIUzI1NiJ9.test1234");
808 engine.redact_value(&mut val);
809 assert_eq!(val, json!("[REDACTED]"));
810 }
811
812 #[test]
813 fn mixed_case_bearer_is_redacted() {
814 let engine = default_engine();
815 let mut val = json!("BEARER eyJhbGciOiJIUzI1NiJ9.test1234");
816 engine.redact_value(&mut val);
817 assert_eq!(val, json!("[REDACTED]"));
818 }
819
820 #[test]
825 fn bearer_exact_output() {
826 let engine = default_engine();
827 let mut val = json!("Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc123");
828 engine.redact_value(&mut val);
829 assert_eq!(val, json!("Authorization: [REDACTED]"));
830 }
831
832 #[test]
833 fn email_exact_output() {
834 let engine = default_engine();
835 let mut val = json!("Contact alice@example.org for help");
836 engine.redact_value(&mut val);
837 assert_eq!(val, json!("Contact [REDACTED] for help"));
838 }
839
840 #[test]
841 fn ssn_exact_output() {
842 let engine = default_engine();
843 let mut val = json!("SSN: 123-45-6789");
844 engine.redact_value(&mut val);
845 assert_eq!(val, json!("SSN: [REDACTED]"));
846 }
847
848 #[test]
849 fn public_ipv4_exact_output() {
850 let engine = default_engine();
851 let mut val = json!("Connected from 8.8.8.8 on port 443");
852 engine.redact_value(&mut val);
853 assert_eq!(val, json!("Connected from [REDACTED] on port 443"));
854 }
855}