#![cfg_attr(coverage_nightly, coverage(off))]
use crate::services::doc_validator::{
classify_link, extract_links, DocValidator, Link, LinkType, ValidationResult, ValidationStatus,
ValidationSummary, ValidatorConfig,
};
use std::path::{Path, PathBuf};
#[test]
fn test_extract_links_empty_content() {
let links = extract_links("", Path::new("test.md"));
assert!(links.is_empty());
}
#[test]
fn test_extract_links_no_links() {
let content = "This is just plain text without any links.";
let links = extract_links(content, Path::new("test.md"));
assert!(links.is_empty());
}
#[test]
fn test_extract_links_single_http_link() {
let content = "[Example](https://example.com)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].text, "Example");
assert_eq!(links[0].target, "https://example.com");
assert_eq!(links[0].link_type, LinkType::ExternalHttp);
}
#[test]
fn test_extract_links_single_http_link_without_s() {
let content = "[Example](http://example.com)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].link_type, LinkType::ExternalHttp);
}
#[test]
fn test_extract_links_internal_link() {
let content = "[Local](./local.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].target, "./local.md");
assert_eq!(links[0].link_type, LinkType::Internal);
}
#[test]
fn test_extract_links_anchor_link() {
let content = "[Section](#section-title)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].target, "#section-title");
assert_eq!(links[0].link_type, LinkType::Anchor);
}
#[test]
fn test_extract_links_email_link() {
let content = "[Contact](mailto:user@example.com)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].link_type, LinkType::Email);
}
#[test]
fn test_extract_links_multiple_links_single_line() {
let content = "[Link1](url1.md) and [Link2](url2.md) and [Link3](https://example.com)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 3);
assert_eq!(links[0].text, "Link1");
assert_eq!(links[1].text, "Link2");
assert_eq!(links[2].text, "Link3");
}
#[test]
fn test_extract_links_multiple_lines() {
let content = "First paragraph with [link1](./a.md)\n\nSecond paragraph with [link2](./b.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 2);
assert_eq!(links[0].line_number, 1);
assert_eq!(links[1].line_number, 3);
}
#[test]
fn test_extract_links_skips_code_blocks() {
let content = "```\n[code_link](./should_skip.md)\n```\n[real_link](./keep.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 2);
}
#[test]
fn test_extract_links_skips_code_blocks_with_indent() {
let content = " ```rust\n [code](./skip.md)\n ```\n[real](./real.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 2);
}
#[test]
fn test_extract_links_preserves_source_file() {
let source = Path::new("/docs/spec/feature.md");
let content = "[test](./test.md)";
let links = extract_links(content, source);
assert_eq!(links[0].source_file, PathBuf::from("/docs/spec/feature.md"));
}
#[test]
fn test_extract_links_parent_path() {
let content = "[Parent](../parent.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].target, "../parent.md");
assert_eq!(links[0].link_type, LinkType::Internal);
}
#[test]
fn test_extract_links_absolute_path() {
let content = "[Absolute](/root/file.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].target, "/root/file.md");
assert_eq!(links[0].link_type, LinkType::Internal);
}
#[test]
fn test_extract_links_with_spaces_in_text() {
let content = "[Link with spaces](./file.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].text, "Link with spaces");
}
#[test]
fn test_extract_links_complex_url() {
let content = "[API](https://api.example.com/v1/users?limit=10&offset=0)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(
links[0].target,
"https://api.example.com/v1/users?limit=10&offset=0"
);
}
#[test]
fn test_extract_links_with_anchor_in_internal() {
let content = "[Section](./file.md#section)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].target, "./file.md#section");
assert_eq!(links[0].link_type, LinkType::Internal);
}
#[test]
fn test_classify_link_https() {
assert_eq!(classify_link("https://example.com"), LinkType::ExternalHttp);
}
#[test]
fn test_classify_link_http() {
assert_eq!(classify_link("http://example.com"), LinkType::ExternalHttp);
}
#[test]
fn test_classify_link_https_with_path() {
assert_eq!(
classify_link("https://example.com/path/to/page"),
LinkType::ExternalHttp
);
}
#[test]
fn test_classify_link_https_with_query() {
assert_eq!(
classify_link("https://example.com?q=search"),
LinkType::ExternalHttp
);
}
#[test]
fn test_classify_link_anchor() {
assert_eq!(classify_link("#section"), LinkType::Anchor);
}
#[test]
fn test_classify_link_anchor_complex() {
assert_eq!(classify_link("#section-with-dashes-123"), LinkType::Anchor);
}
#[test]
fn test_classify_link_mailto() {
assert_eq!(classify_link("mailto:user@example.com"), LinkType::Email);
}
#[test]
fn test_classify_link_mailto_with_subject() {
assert_eq!(
classify_link("mailto:user@example.com?subject=Hello"),
LinkType::Email
);
}
#[test]
fn test_classify_link_internal_relative() {
assert_eq!(classify_link("./file.md"), LinkType::Internal);
}
#[test]
fn test_classify_link_internal_parent() {
assert_eq!(classify_link("../parent/file.md"), LinkType::Internal);
}
#[test]
fn test_classify_link_internal_absolute() {
assert_eq!(classify_link("/root/file.md"), LinkType::Internal);
}
#[test]
fn test_classify_link_internal_simple() {
assert_eq!(classify_link("file.md"), LinkType::Internal);
}
#[test]
fn test_classify_link_internal_nested() {
assert_eq!(classify_link("docs/spec/feature.md"), LinkType::Internal);
}
#[test]
fn test_classify_link_other_ftp() {
let link_type = classify_link("ftp://files.example.com");
assert!(matches!(link_type, LinkType::Other(protocol) if protocol == "ftp"));
}
#[test]
fn test_classify_link_other_ssh() {
let link_type = classify_link("ssh://git@github.com/repo.git");
assert!(matches!(link_type, LinkType::Other(protocol) if protocol == "ssh"));
}
#[test]
fn test_classify_link_other_file() {
let link_type = classify_link("file:///local/path");
assert!(matches!(link_type, LinkType::Other(protocol) if protocol == "file"));
}
#[test]
fn test_classify_link_other_tel() {
let link_type = classify_link("tel://+1234567890");
assert!(matches!(link_type, LinkType::Other(protocol) if protocol == "tel"));
}
#[test]
fn test_link_type_equality() {
assert_eq!(LinkType::Internal, LinkType::Internal);
assert_eq!(LinkType::ExternalHttp, LinkType::ExternalHttp);
assert_eq!(LinkType::Anchor, LinkType::Anchor);
assert_eq!(LinkType::Email, LinkType::Email);
}
#[test]
fn test_link_type_inequality() {
assert_ne!(LinkType::Internal, LinkType::ExternalHttp);
assert_ne!(LinkType::Anchor, LinkType::Email);
}
#[test]
fn test_link_type_other_equality() {
assert_eq!(
LinkType::Other("ftp".to_string()),
LinkType::Other("ftp".to_string())
);
}
#[test]
fn test_link_type_other_inequality() {
assert_ne!(
LinkType::Other("ftp".to_string()),
LinkType::Other("ssh".to_string())
);
}
#[test]
fn test_link_type_clone() {
let original = LinkType::ExternalHttp;
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_link_type_other_clone() {
let original = LinkType::Other("custom".to_string());
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_link_type_debug() {
let link_type = LinkType::Internal;
let debug_str = format!("{:?}", link_type);
assert!(debug_str.contains("Internal"));
}
#[test]
fn test_link_creation() {
let link = Link {
text: "Example".to_string(),
target: "https://example.com".to_string(),
source_file: PathBuf::from("test.md"),
line_number: 10,
link_type: LinkType::ExternalHttp,
};
assert_eq!(link.text, "Example");
assert_eq!(link.target, "https://example.com");
assert_eq!(link.line_number, 10);
}
#[test]
fn test_link_equality() {
let link1 = Link {
text: "Test".to_string(),
target: "./test.md".to_string(),
source_file: PathBuf::from("source.md"),
line_number: 5,
link_type: LinkType::Internal,
};
let link2 = Link {
text: "Test".to_string(),
target: "./test.md".to_string(),
source_file: PathBuf::from("source.md"),
line_number: 5,
link_type: LinkType::Internal,
};
assert_eq!(link1, link2);
}
#[test]
fn test_link_inequality_text() {
let link1 = Link {
text: "Test1".to_string(),
target: "./test.md".to_string(),
source_file: PathBuf::from("source.md"),
line_number: 5,
link_type: LinkType::Internal,
};
let link2 = Link {
text: "Test2".to_string(),
target: "./test.md".to_string(),
source_file: PathBuf::from("source.md"),
line_number: 5,
link_type: LinkType::Internal,
};
assert_ne!(link1, link2);
}
#[test]
fn test_link_clone() {
let link = Link {
text: "Clone Test".to_string(),
target: "https://example.com".to_string(),
source_file: PathBuf::from("test.md"),
line_number: 1,
link_type: LinkType::ExternalHttp,
};
let cloned = link.clone();
assert_eq!(link, cloned);
}
#[test]
fn test_link_debug() {
let link = Link {
text: "Debug".to_string(),
target: "./file.md".to_string(),
source_file: PathBuf::from("source.md"),
line_number: 1,
link_type: LinkType::Internal,
};
let debug_str = format!("{:?}", link);
assert!(debug_str.contains("Debug"));
assert!(debug_str.contains("Internal"));
}
#[test]
fn test_validation_status_valid() {
let status = ValidationStatus::Valid;
assert_eq!(status, ValidationStatus::Valid);
}
#[test]
fn test_validation_status_not_found() {
let status = ValidationStatus::NotFound;
assert_eq!(status, ValidationStatus::NotFound);
}
#[test]
fn test_validation_status_http_error() {
let status = ValidationStatus::HttpError(500);
assert!(matches!(status, ValidationStatus::HttpError(500)));
}
#[test]
fn test_validation_status_http_error_different_codes() {
let status1 = ValidationStatus::HttpError(500);
let status2 = ValidationStatus::HttpError(503);
assert_ne!(status1, status2);
}
#[test]
fn test_validation_status_network_error() {
let status = ValidationStatus::NetworkError;
assert_eq!(status, ValidationStatus::NetworkError);
}
#[test]
fn test_validation_status_invalid_link() {
let status = ValidationStatus::InvalidLink;
assert_eq!(status, ValidationStatus::InvalidLink);
}
#[test]
fn test_validation_status_skipped() {
let status = ValidationStatus::Skipped;
assert_eq!(status, ValidationStatus::Skipped);
}
#[test]
fn test_validation_status_clone() {
let status = ValidationStatus::HttpError(404);
let cloned = status.clone();
assert_eq!(status, cloned);
}
#[test]
fn test_validation_status_debug() {
let status = ValidationStatus::NetworkError;
let debug_str = format!("{:?}", status);
assert!(debug_str.contains("NetworkError"));
}
#[test]
fn test_validator_config_default() {
let config = ValidatorConfig::default();
assert_eq!(config.root_dir, PathBuf::from("."));
assert_eq!(config.http_timeout_ms, 30000);
assert_eq!(config.max_retries, 3);
assert_eq!(config.retry_delay_ms, 1000);
assert_eq!(config.max_concurrent_requests, 10);
assert!(config.follow_redirects);
}
#[test]
fn test_validator_config_default_excludes() {
let config = ValidatorConfig::default();
assert!(config.exclude_patterns.contains(&"archive".to_string()));
assert!(config
.exclude_patterns
.contains(&"node_modules".to_string()));
assert!(config.exclude_patterns.contains(&".git".to_string()));
assert!(config.exclude_patterns.contains(&"target".to_string()));
}
#[test]
fn test_validator_config_user_agent() {
let config = ValidatorConfig::default();
assert!(config.user_agent.starts_with("pmat-doc-validator/"));
}
#[test]
fn test_validator_config_clone() {
let config = ValidatorConfig::default();
let cloned = config.clone();
assert_eq!(config.root_dir, cloned.root_dir);
assert_eq!(config.http_timeout_ms, cloned.http_timeout_ms);
}
#[test]
fn test_validator_config_custom() {
let config = ValidatorConfig {
root_dir: PathBuf::from("/custom/path"),
http_timeout_ms: 5000,
max_retries: 5,
retry_delay_ms: 500,
max_concurrent_requests: 20,
exclude_patterns: vec!["custom".to_string()],
follow_redirects: false,
user_agent: "custom-agent".to_string(),
};
assert_eq!(config.root_dir, PathBuf::from("/custom/path"));
assert_eq!(config.http_timeout_ms, 5000);
assert!(!config.follow_redirects);
}
#[test]
fn test_validation_result_creation() {
let link = Link {
text: "Test".to_string(),
target: "https://example.com".to_string(),
source_file: PathBuf::from("test.md"),
line_number: 1,
link_type: LinkType::ExternalHttp,
};
let result = ValidationResult {
link: link.clone(),
status: ValidationStatus::Valid,
error_message: None,
http_status_code: Some(200),
response_time_ms: Some(150),
};
assert_eq!(result.status, ValidationStatus::Valid);
assert_eq!(result.http_status_code, Some(200));
assert_eq!(result.response_time_ms, Some(150));
}
#[test]
fn test_validation_result_with_error() {
let link = Link {
text: "Broken".to_string(),
target: "./missing.md".to_string(),
source_file: PathBuf::from("test.md"),
line_number: 5,
link_type: LinkType::Internal,
};
let result = ValidationResult {
link,
status: ValidationStatus::NotFound,
error_message: Some("File not found: missing.md".to_string()),
http_status_code: None,
response_time_ms: Some(10),
};
assert_eq!(result.status, ValidationStatus::NotFound);
assert!(result.error_message.is_some());
}
#[test]
fn test_validation_result_clone() {
let link = Link {
text: "Clone".to_string(),
target: "./test.md".to_string(),
source_file: PathBuf::from("source.md"),
line_number: 1,
link_type: LinkType::Internal,
};
let result = ValidationResult {
link,
status: ValidationStatus::Valid,
error_message: None,
http_status_code: None,
response_time_ms: Some(5),
};
let cloned = result.clone();
assert_eq!(result.status, cloned.status);
}
#[test]
fn test_validation_summary_creation() {
let summary = ValidationSummary {
total_files: 10,
total_links: 50,
valid_links: 45,
broken_links: 3,
skipped_links: 2,
duration_ms: 5000,
results: vec![],
};
assert_eq!(summary.total_files, 10);
assert_eq!(summary.total_links, 50);
assert_eq!(summary.valid_links, 45);
assert_eq!(summary.broken_links, 3);
assert_eq!(summary.skipped_links, 2);
}
#[test]
fn test_validation_summary_clone() {
let summary = ValidationSummary {
total_files: 5,
total_links: 20,
valid_links: 18,
broken_links: 2,
skipped_links: 0,
duration_ms: 1000,
results: vec![],
};
let cloned = summary.clone();
assert_eq!(summary.total_files, cloned.total_files);
assert_eq!(summary.broken_links, cloned.broken_links);
}
#[test]
fn test_validation_summary_with_results() {
let link = Link {
text: "Test".to_string(),
target: "./test.md".to_string(),
source_file: PathBuf::from("source.md"),
line_number: 1,
link_type: LinkType::Internal,
};
let result = ValidationResult {
link,
status: ValidationStatus::Valid,
error_message: None,
http_status_code: None,
response_time_ms: Some(5),
};
let summary = ValidationSummary {
total_files: 1,
total_links: 1,
valid_links: 1,
broken_links: 0,
skipped_links: 0,
duration_ms: 100,
results: vec![result],
};
assert_eq!(summary.results.len(), 1);
}
#[test]
fn test_doc_validator_new() {
let config = ValidatorConfig::default();
let validator = DocValidator::new(config);
assert!(true, "DocValidator created successfully");
drop(validator);
}
#[test]
fn test_doc_validator_default() {
let validator = DocValidator::default();
assert!(true, "Default DocValidator created");
drop(validator);
}
#[test]
fn test_doc_validator_no_http_client() {
let config = ValidatorConfig {
http_timeout_ms: 0, ..ValidatorConfig::default()
};
let validator = DocValidator::new(config);
assert!(true, "DocValidator without HTTP client created");
drop(validator);
}
#[tokio::test]
async fn test_validate_internal_link_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let target_file = temp_dir.path().join("target.md");
std::fs::write(&target_file, "# Target").unwrap();
let source_file = temp_dir.path().join("source.md");
let link = Link {
text: "Target".to_string(),
target: "./target.md".to_string(),
source_file,
line_number: 1,
link_type: LinkType::Internal,
};
let validator = DocValidator::default();
let result = validator.validate_link(&link).await.unwrap();
assert_eq!(result.status, ValidationStatus::Valid);
}
#[tokio::test]
async fn test_validate_internal_link_missing() {
let temp_dir = tempfile::tempdir().unwrap();
let source_file = temp_dir.path().join("source.md");
let link = Link {
text: "Missing".to_string(),
target: "./nonexistent.md".to_string(),
source_file,
line_number: 1,
link_type: LinkType::Internal,
};
let validator = DocValidator::default();
let result = validator.validate_link(&link).await.unwrap();
assert_eq!(result.status, ValidationStatus::NotFound);
assert!(result.error_message.is_some());
}
#[tokio::test]
async fn test_validate_anchor_link() {
let link = Link {
text: "Section".to_string(),
target: "#section".to_string(),
source_file: PathBuf::from("test.md"),
line_number: 1,
link_type: LinkType::Anchor,
};
let validator = DocValidator::default();
let result = validator.validate_link(&link).await.unwrap();
assert_eq!(result.status, ValidationStatus::Valid);
}
#[tokio::test]
async fn test_validate_email_link() {
let link = Link {
text: "Contact".to_string(),
target: "mailto:test@example.com".to_string(),
source_file: PathBuf::from("test.md"),
line_number: 1,
link_type: LinkType::Email,
};
let validator = DocValidator::default();
let result = validator.validate_link(&link).await.unwrap();
assert_eq!(result.status, ValidationStatus::Valid);
}
#[tokio::test]
async fn test_validate_other_protocol_skipped() {
let link = Link {
text: "FTP".to_string(),
target: "ftp://files.example.com".to_string(),
source_file: PathBuf::from("test.md"),
line_number: 1,
link_type: LinkType::Other("ftp".to_string()),
};
let validator = DocValidator::default();
let result = validator.validate_link(&link).await.unwrap();
assert_eq!(result.status, ValidationStatus::Skipped);
}
#[tokio::test]
async fn test_validate_internal_link_with_anchor() {
let temp_dir = tempfile::tempdir().unwrap();
let target_file = temp_dir.path().join("target.md");
std::fs::write(&target_file, "# Section\n\nContent").unwrap();
let source_file = temp_dir.path().join("source.md");
let link = Link {
text: "Target Section".to_string(),
target: "./target.md#section".to_string(),
source_file,
line_number: 1,
link_type: LinkType::Internal,
};
let validator = DocValidator::default();
let result = validator.validate_link(&link).await.unwrap();
assert_eq!(result.status, ValidationStatus::Valid);
}
#[tokio::test]
async fn test_validate_internal_pure_anchor() {
let temp_dir = tempfile::tempdir().unwrap();
let source_file = temp_dir.path().join("source.md");
let link = Link {
text: "Section".to_string(),
target: "#section".to_string(),
source_file,
line_number: 1,
link_type: LinkType::Internal, };
let validator = DocValidator::default();
let result = validator.validate_link(&link).await.unwrap();
assert_eq!(result.status, ValidationStatus::Valid);
}
#[tokio::test]
async fn test_validate_directory_basic() {
let temp_dir = tempfile::tempdir().unwrap();
std::fs::write(temp_dir.path().join("readme.md"), "[link](./other.md)").unwrap();
std::fs::write(temp_dir.path().join("other.md"), "# Other").unwrap();
let validator = DocValidator::default();
let summary = validator.validate_directory(temp_dir.path()).await.unwrap();
assert_eq!(summary.total_files, 2);
assert_eq!(summary.total_links, 1);
assert_eq!(summary.valid_links, 1);
assert_eq!(summary.broken_links, 0);
}
#[tokio::test]
async fn test_validate_directory_with_broken_link() {
let temp_dir = tempfile::tempdir().unwrap();
std::fs::write(
temp_dir.path().join("readme.md"),
"[broken](./nonexistent.md)",
)
.unwrap();
let validator = DocValidator::default();
let summary = validator.validate_directory(temp_dir.path()).await.unwrap();
assert_eq!(summary.total_files, 1);
assert_eq!(summary.broken_links, 1);
}
#[tokio::test]
async fn test_validate_directory_excludes_archive() {
let temp_dir = tempfile::tempdir().unwrap();
std::fs::write(temp_dir.path().join("readme.md"), "[good](./good.md)").unwrap();
std::fs::write(temp_dir.path().join("good.md"), "Content").unwrap();
let archive = temp_dir.path().join("archive");
std::fs::create_dir(&archive).unwrap();
std::fs::write(archive.join("old.md"), "[broken](./missing.md)").unwrap();
let validator = DocValidator::default();
let summary = validator.validate_directory(temp_dir.path()).await.unwrap();
assert_eq!(summary.total_files, 2);
assert_eq!(summary.broken_links, 0);
}
#[tokio::test]
async fn test_validate_directory_excludes_node_modules() {
let temp_dir = tempfile::tempdir().unwrap();
std::fs::write(temp_dir.path().join("readme.md"), "# Readme").unwrap();
let node_modules = temp_dir.path().join("node_modules");
std::fs::create_dir(&node_modules).unwrap();
std::fs::write(node_modules.join("package.md"), "[broken](./x.md)").unwrap();
let validator = DocValidator::default();
let summary = validator.validate_directory(temp_dir.path()).await.unwrap();
assert_eq!(summary.total_files, 1);
}
#[tokio::test]
async fn test_validate_directory_empty() {
let temp_dir = tempfile::tempdir().unwrap();
let validator = DocValidator::default();
let summary = validator.validate_directory(temp_dir.path()).await.unwrap();
assert_eq!(summary.total_files, 0);
assert_eq!(summary.total_links, 0);
}
#[tokio::test]
async fn test_validate_http_no_client() {
let config = ValidatorConfig {
http_timeout_ms: 0,
..ValidatorConfig::default()
};
let validator = DocValidator::new(config);
let link = Link {
text: "HTTP".to_string(),
target: "https://example.com".to_string(),
source_file: PathBuf::from("test.md"),
line_number: 1,
link_type: LinkType::ExternalHttp,
};
let result = validator.validate_link(&link).await.unwrap();
assert_eq!(result.status, ValidationStatus::NetworkError);
assert!(result.error_message.unwrap().contains("not configured"));
}
#[tokio::test]
async fn test_response_time_captured() {
let temp_dir = tempfile::tempdir().unwrap();
let target_file = temp_dir.path().join("target.md");
std::fs::write(&target_file, "Content").unwrap();
let source_file = temp_dir.path().join("source.md");
let link = Link {
text: "Target".to_string(),
target: "./target.md".to_string(),
source_file,
line_number: 1,
link_type: LinkType::Internal,
};
let validator = DocValidator::default();
let result = validator.validate_link(&link).await.unwrap();
assert!(result.response_time_ms.is_some());
}
#[test]
fn test_extract_links_empty_link_text() {
let content = "[](./file.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 0);
}
#[test]
fn test_extract_links_unicode_text() {
let content = "[日本語](./japanese.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
assert_eq!(links[0].text, "日本語");
}
#[test]
fn test_extract_links_emoji_text() {
let content = "[Click here](./file.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 1);
}
#[test]
#[ignore = "nested bracket behavior differs"]
fn test_extract_links_nested_brackets_invalid() {
let content = "[[nested]](./file.md)";
let links = extract_links(content, Path::new("test.md"));
assert_eq!(links.len(), 0);
}
#[test]
fn test_classify_link_empty_string() {
let link_type = classify_link("");
assert_eq!(link_type, LinkType::Internal);
}
#[test]
fn test_classify_link_just_slash() {
let link_type = classify_link("/");
assert_eq!(link_type, LinkType::Internal);
}
#[test]
fn test_link_hash_impl() {
use std::collections::HashSet;
let link1 = Link {
text: "Test".to_string(),
target: "./test.md".to_string(),
source_file: PathBuf::from("source.md"),
line_number: 1,
link_type: LinkType::Internal,
};
let link2 = link1.clone();
let mut set = HashSet::new();
set.insert(link1);
set.insert(link2);
assert_eq!(set.len(), 1);
}
#[test]
fn test_link_type_hash_impl() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(LinkType::Internal);
set.insert(LinkType::Internal);
set.insert(LinkType::ExternalHttp);
assert_eq!(set.len(), 2);
}