Index: tests/utils.rs ================================================================== --- tests/utils.rs +++ tests/utils.rs @@ -1,51 +1,81 @@ use smtp2tg::utils::{ validate, RE_CLOSING, RE_DOMAIN, }; -use stacked_errors::Result; - -#[test] -fn test_validate_valid_html() -> Result<()> { - let html = "

Some valid HTML

"; - let escaped = validate(html)?; - assert_eq!(escaped, "<p>Some <b>valid</b> HTML</p>"); - Ok(()) -} - -#[test] -fn test_validate_closing_tag() -> Result<()> { - let html = "

Some valid HTML

Link injection!"; - assert!(validate(html).is_err()); - assert!(validate("").is_err()); - assert!(validate("").is_err()); - assert!(validate("\n").is_err()); - assert!(validate("\t").is_err()); - Ok(()) -} - -#[test] -fn test_validate_empty_string() -> Result<()> { - assert_eq!(validate("").unwrap(), ""); - Ok(()) -} - -#[test] -fn test_validate_whitespace() -> Result<()> { - assert_eq!(validate(" \t\n").unwrap(), " \t\n"); // no escaping for whitespace - Ok(()) -} - -#[test] -fn test_regex_closing_tag_matches() { - assert!(RE_CLOSING.is_match("")); - assert!(RE_CLOSING.is_match("\t>")); - assert!(!RE_CLOSING.is_match("")); // Not a pre/code tag -} - -#[test] -fn test_regex_domain_matches() { - assert!(RE_DOMAIN.is_match("example.com")); - assert!(RE_DOMAIN.is_match("sub.example.co.uk")); - assert!(!RE_DOMAIN.is_match("invalid@domain.com")); + +use std::{ + borrow::Cow, + mem::discriminant, +}; + +use stacked_errors::Result; + +#[test] +fn test_validate_escaping_behavior () -> Result<()> { + let cases: &[(&str, Cow)] = &[ + // `validate` escapes HTML special characters. + ("

Some valid HTML

", Cow::Owned("<p>Some <b>valid</b> HTML</p>".into())), + // Empty input is returned unchanged. + ("", Cow::Borrowed("")), + // Whitespace-only input needs no escaping. + (" \t\n", Cow::Borrowed(" \t\n")), + // `validate` returns `Cow<'a, str>` borrowed from its input lifetime `'a`. + // These two cases exercise both branches of that `Cow` to make sure the + // explicit lifetime introduced on `validate` still lets callers observe a + // zero-copy borrow when no escaping is required. + ("plain text without special html characters", Cow::Borrowed("plain text without special html characters")), + ("5 > 3 & 2 < 4", Cow::Owned("5 > 3 & 2 < 4".into())), + ]; + for (input, expected) in cases { + let result = validate(input)?; + assert_eq!(&result, expected, "unexpected output for input {input:?}"); + assert_eq!(discriminant(&result), discriminant(expected), "wrong Cow variant for input {input:?}"); + } + Ok(()) +} + +#[test] +fn test_validate_closing_tag_behavior () { + let cases = [ + ("", true), + ("", true), + ("", false), + ("", true), + ("", true), + ("\t", true), + ("\t>", true), + ("", false), // Not a pre/code tag + ("", true), + ("", true), + ("\n", true), + ("

Some valid HTML

Link injection!", true), + ("
", false),
+	];
+	for (input, expected) in cases {
+		assert_eq!(RE_CLOSING.is_match(input), expected, "unexpected match result for {input:?}");
+	}
+}
+
+#[test]
+fn test_regex_domain_behavior() {
+	let cases = [
+		("", false),
+		("-example.com", false),
+		(".example.com", false),
+		("123.456", true),
+		("EXAMPLE.COM", false),
+		("a", true),
+		("a.b", true),
+		("example-.com", false),
+		("example..com", false),
+		("example.com", true),
+		("example.com.", false),
+		("invalid@domain.com", false),
+		("my-host.example.com", true),
+		("sub.example.co.uk", true),
+	];
+	for (input, expected) in cases {
+		assert_eq!(RE_DOMAIN.is_match(input), expected, "unexpected match result for {input:?}");
+	}
 }