use rumdl_lib::config::MarkdownFlavor;
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD034NoBareUrls;
#[test]
fn test_url_encoded_characters() {
let rule = MD034NoBareUrls;
let test_cases = [
(
"https://example.com/path%20with%20spaces",
1,
"<https://example.com/path%20with%20spaces>",
),
(
"https://example.com/search?q=hello%20world",
1,
"<https://example.com/search?q=hello%20world>",
),
(
"https://example.com/%E4%B8%AD%E6%96%87", 1,
"<https://example.com/%E4%B8%AD%E6%96%87>",
),
];
for (content, expected_count, expected_fix) in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), expected_count, "URL encoded: {content}");
if expected_count > 0 {
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, expected_fix, "Fix for: {content}");
}
}
}
#[test]
fn test_urls_with_complex_query_strings() {
let rule = MD034NoBareUrls;
let test_cases = [
("https://example.com?a=1&b=2&c=3", 1),
("https://example.com?url=https%3A%2F%2Fother.com", 1),
("https://example.com?ids[]=1&ids[]=2", 1),
("https://example.com?data={\"key\":\"value\"}", 1),
];
for (content, expected_count) in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), expected_count, "Query string URL: {content}");
}
}
#[test]
fn test_urls_with_special_fragments() {
let rule = MD034NoBareUrls;
let test_cases = [
("https://example.com#section-1", 1),
("https://example.com#L123-L456", 1), ("https://example.com#user-content-heading", 1),
("https://example.com/page#this.is.a.fragment", 1),
];
for (content, expected_count) in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), expected_count, "Fragment URL: {content}");
}
}
#[test]
fn test_urls_in_nested_blockquotes() {
let rule = MD034NoBareUrls;
let content1 = "> Visit https://example.com for info";
let ctx1 = LintContext::new(content1, MarkdownFlavor::Standard, None);
let result1 = rule.check(&ctx1).unwrap();
assert_eq!(result1.len(), 1, "Single blockquote should flag URL");
let content2 = "> > Nested quote with https://example.com URL";
let ctx2 = LintContext::new(content2, MarkdownFlavor::Standard, None);
let result2 = rule.check(&ctx2).unwrap();
assert_eq!(result2.len(), 1, "Nested blockquote should flag URL");
let content3 = "> > > Deep nesting https://example.com";
let ctx3 = LintContext::new(content3, MarkdownFlavor::Standard, None);
let result3 = rule.check(&ctx3).unwrap();
assert_eq!(result3.len(), 1, "Deep nested blockquote should flag URL");
}
#[test]
fn test_urls_in_list_items() {
let rule = MD034NoBareUrls;
let test_cases = [
("- https://example.com", 1),
("* https://example.com", 1),
("+ https://example.com", 1),
("1. https://example.com", 1),
("10. https://example.com", 1),
("99. https://example.com", 1),
("- Item\n - Nested https://example.com", 1),
("- First https://one.com\n- Second https://two.com", 2),
];
for (content, expected_count) in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), expected_count, "List item: {content}");
}
}
#[test]
fn test_urls_with_emphasis() {
let rule = MD034NoBareUrls;
let test_cases = [
("**Bold** https://example.com", 1),
("https://example.com **Bold**", 1),
("*Italic* https://example.com", 1),
("This is **important**: https://example.com", 1),
];
for (content, expected_count) in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), expected_count, "Emphasis context: {content}");
}
}
#[test]
fn test_urls_inside_emphasis_in_links() {
let rule = MD034NoBareUrls;
let content = "[**https://example.com**](https://example.com)";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL inside bold link text should not be flagged: {result:?}"
);
}
#[test]
fn test_urls_with_trailing_punctuation() {
let rule = MD034NoBareUrls;
let test_cases = [
("Visit https://example.com.", 1, "<https://example.com>"),
("Visit https://example.com!", 1, "<https://example.com>"),
("Visit https://example.com?", 1, "<https://example.com>"),
("Visit https://example.com,", 1, "<https://example.com>"),
("Visit https://example.com;", 1, "<https://example.com>"),
("Visit https://example.com:", 1, "<https://example.com:>"),
("Visit https://example.com...", 1, "<https://example.com>"),
("Visit https://example.com!!", 1, "<https://example.com>"),
];
for (content, expected_count, expected_url) in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), expected_count, "Punctuation: {content}");
if expected_count > 0 {
let fixed = rule.fix(&ctx).unwrap();
assert!(
fixed.contains(expected_url),
"Fix should contain {expected_url}: got {fixed}"
);
}
}
}
#[test]
fn test_urls_at_document_boundaries() {
let rule = MD034NoBareUrls;
let content1 = "https://example.com is the link";
let ctx1 = LintContext::new(content1, MarkdownFlavor::Standard, None);
let result1 = rule.check(&ctx1).unwrap();
assert_eq!(result1.len(), 1, "URL at start should be flagged");
assert_eq!(result1[0].column, 1, "Should start at column 1");
let content2 = "Visit https://example.com";
let ctx2 = LintContext::new(content2, MarkdownFlavor::Standard, None);
let result2 = rule.check(&ctx2).unwrap();
assert_eq!(result2.len(), 1, "URL at end should be flagged");
let content3 = "https://example.com";
let ctx3 = LintContext::new(content3, MarkdownFlavor::Standard, None);
let result3 = rule.check(&ctx3).unwrap();
assert_eq!(result3.len(), 1, "URL as only content should be flagged");
}
#[test]
fn test_urls_with_unusual_tlds() {
let rule = MD034NoBareUrls;
let test_cases = [
("https://example.museum", 1),
("https://example.technology", 1),
("https://example.photography", 1),
("https://example.international", 1),
("https://example.co.uk", 1),
("https://example.com.au", 1),
("https://example.gov.uk", 1),
];
for (content, expected_count) in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), expected_count, "Unusual TLD: {content}");
}
}
#[test]
fn test_internationalized_domain_names() {
let rule = MD034NoBareUrls;
let test_cases = [
("https://xn--n3h.com", 1), ("https://exämple.com", 1),
("https://例え.jp", 1), ("https://مثال.com", 1), ];
for (content, expected_count) in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), expected_count, "IDN: {content}");
}
}
#[test]
fn test_urls_in_inline_html_comments() {
let rule = MD034NoBareUrls;
let content = "Text <!-- https://example.com --> more text";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URL in HTML comment should not be flagged: {result:?}"
);
}
#[test]
fn test_urls_in_multiline_html_comments() {
let rule = MD034NoBareUrls;
let content = "Text\n<!--\nhttps://example.com\nhttps://another.com\n-->\nMore text";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"URLs in multiline HTML comment should not be flagged: {result:?}"
);
}
#[test]
fn test_url_after_html_comment_is_flagged() {
let rule = MD034NoBareUrls;
let content = "<!-- comment --> https://example.com";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "URL after HTML comment should be flagged");
}
#[test]
fn test_shortcut_reference_links_not_flagged() {
let rule = MD034NoBareUrls;
let content = "[https://example.com]";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Shortcut reference link [URL] should not be flagged: {result:?}"
);
}
#[test]
fn test_collapsed_reference_links_not_flagged() {
let rule = MD034NoBareUrls;
let content = "[https://example.com][]";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Collapsed reference link [URL][] should not be flagged: {result:?}"
);
}
#[test]
fn test_urls_in_table_cells() {
let rule = MD034NoBareUrls;
let content = "| Column 1 | Column 2 |\n|----------|----------|\n| https://example.com | text |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "URL in table cell should be flagged");
let content2 = "| https://one.com | https://two.com |\n|-----------------|-----------------|";
let ctx2 = LintContext::new(content2, MarkdownFlavor::Standard, None);
let result2 = rule.check(&ctx2).unwrap();
assert_eq!(result2.len(), 2, "URLs in multiple table cells should be flagged");
}
#[test]
fn test_urls_in_table_headers() {
let rule = MD034NoBareUrls;
let content = "| https://example.com | Header 2 |\n|---------------------|----------|\n| data | data |";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "URL in table header should be flagged");
}
#[test]
fn test_empty_content() {
let rule = MD034NoBareUrls;
let content = "";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Empty content should produce no warnings");
}
#[test]
fn test_whitespace_only_content() {
let rule = MD034NoBareUrls;
let content = " \n\n \t\t\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Whitespace-only content should produce no warnings");
}
#[test]
fn test_content_without_urls() {
let rule = MD034NoBareUrls;
let content = "# Heading\n\nThis is a paragraph without any URLs.\n\n- List item\n- Another item";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(result.is_empty(), "Content without URLs should produce no warnings");
}
#[test]
fn test_very_long_urls() {
let rule = MD034NoBareUrls;
let long_path = (0..20).map(|i| format!("segment{i}")).collect::<Vec<_>>().join("/");
let content = format!("https://example.com/{long_path}");
let ctx = LintContext::new(&content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert_eq!(result.len(), 1, "Very long URL should be flagged");
let long_query = (0..50)
.map(|i| format!("param{i}=value{i}"))
.collect::<Vec<_>>()
.join("&");
let content2 = format!("https://example.com?{long_query}");
let ctx2 = LintContext::new(&content2, MarkdownFlavor::Standard, None);
let result2 = rule.check(&ctx2).unwrap();
assert_eq!(result2.len(), 1, "URL with long query should be flagged");
}
#[test]
fn test_urls_with_credentials() {
let rule = MD034NoBareUrls;
let content = "https://user@example.com/path";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(!result.is_empty(), "URL with credentials should be detected");
}
#[test]
fn test_protocol_relative_urls_not_flagged() {
let rule = MD034NoBareUrls;
let content = "//example.com/path";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Protocol-relative URLs should not be flagged as bare URLs: {result:?}"
);
}
#[test]
fn test_custom_protocols_not_flagged() {
let rule = MD034NoBareUrls;
let test_cases = [
"grpc://example.com:50051",
"ws://example.com/socket",
"wss://example.com/socket",
"git://github.com/user/repo.git",
"vscode://file/path/to/file",
"slack://channel?id=123",
"discord://invite/abc123",
"redis://localhost:6379",
"mongodb://localhost:27017/db",
"postgresql://localhost:5432/db",
"mysql://localhost:3306/db",
];
for content in test_cases {
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty(),
"Custom protocol {content} should not be flagged: {result:?}"
);
}
}
#[test]
fn test_custom_protocols_with_email_patterns() {
let rule = MD034NoBareUrls;
let content = "ssh://git@github.com/repo.git";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let result = rule.check(&ctx).unwrap();
assert!(
result.is_empty() || result.iter().all(|w| w.message.contains("Email")),
"ssh:// URL should only flag email pattern if anything: {result:?}"
);
}
#[test]
fn test_fix_produces_valid_markdown() {
let rule = MD034NoBareUrls;
let content = "Visit https://example.com for more info.";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Visit <https://example.com> for more info.");
let ctx_fixed = LintContext::new(&fixed, MarkdownFlavor::Standard, None);
let result_fixed = rule.check(&ctx_fixed).unwrap();
assert!(result_fixed.is_empty(), "Fixed content should have no warnings");
}
#[test]
fn test_fix_multiple_urls_same_line() {
let rule = MD034NoBareUrls;
let content = "Visit https://one.com and https://two.com today";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert_eq!(fixed, "Visit <https://one.com> and <https://two.com> today");
let ctx_fixed = LintContext::new(&fixed, MarkdownFlavor::Standard, None);
let result_fixed = rule.check(&ctx_fixed).unwrap();
assert!(result_fixed.is_empty());
}
#[test]
fn test_fix_preserves_markdown_structure() {
let rule = MD034NoBareUrls;
let content = "# Heading\n\n> Blockquote with https://example.com\n\n- List item https://test.com\n";
let ctx = LintContext::new(content, MarkdownFlavor::Standard, None);
let fixed = rule.fix(&ctx).unwrap();
assert!(fixed.starts_with("# Heading\n"));
assert!(fixed.contains("> Blockquote with <https://example.com>"));
assert!(fixed.contains("- List item <https://test.com>"));
}