use crate::error::RepoLensError;
use chrono::Utc;
use super::ReportRenderer;
use crate::rules::results::{AuditResults, Severity};
pub struct MarkdownReport {
detailed: bool,
}
impl MarkdownReport {
pub fn new(detailed: bool) -> Self {
Self { detailed }
}
}
impl ReportRenderer for MarkdownReport {
fn render_report(&self, results: &AuditResults) -> Result<String, RepoLensError> {
let mut output = String::new();
output.push_str(&format!(
"# RepoLens Audit Report\n\n\
**Repository:** {}\n\
**Preset:** {}\n\
**Generated:** {}\n\
**RepoLens Version:** {}\n\n",
results.repository_name,
results.preset,
Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
env!("CARGO_PKG_VERSION")
));
output.push_str("## Summary\n\n");
output.push_str("| Severity | Count |\n");
output.push_str("|----------|-------|\n");
output.push_str(&format!(
"| Critical | {} |\n",
results.count_by_severity(Severity::Critical)
));
output.push_str(&format!(
"| Warning | {} |\n",
results.count_by_severity(Severity::Warning)
));
output.push_str(&format!(
"| Info | {} |\n\n",
results.count_by_severity(Severity::Info)
));
let critical: Vec<_> = results.findings_by_severity(Severity::Critical).collect();
if !critical.is_empty() {
output.push_str("## Critical Issues\n\n");
output.push_str("These issues must be resolved before proceeding.\n\n");
for finding in critical {
output.push_str(&format!(
"### {} - {}\n\n",
finding.rule_id, finding.message
));
if let Some(location) = &finding.location {
output.push_str(&format!("**Location:** `{}`\n\n", location));
}
if self.detailed {
if let Some(description) = &finding.description {
output.push_str(&format!("{}\n\n", description));
}
if let Some(remediation) = &finding.remediation {
output.push_str(&format!("**Remediation:** {}\n\n", remediation));
}
}
}
}
let warnings: Vec<_> = results.findings_by_severity(Severity::Warning).collect();
if !warnings.is_empty() {
output.push_str("## Warnings\n\n");
output.push_str("These issues should be addressed.\n\n");
for finding in warnings {
output.push_str(&format!(
"### {} - {}\n\n",
finding.rule_id, finding.message
));
if let Some(location) = &finding.location {
output.push_str(&format!("**Location:** `{}`\n\n", location));
}
if self.detailed {
if let Some(description) = &finding.description {
output.push_str(&format!("{}\n\n", description));
}
if let Some(remediation) = &finding.remediation {
output.push_str(&format!("**Remediation:** {}\n\n", remediation));
}
}
}
}
let info: Vec<_> = results.findings_by_severity(Severity::Info).collect();
if !info.is_empty() && self.detailed {
output.push_str("## Informational\n\n");
output.push_str("These are suggestions for improvement.\n\n");
for finding in info {
output.push_str(&format!("- **{}**: {}\n", finding.rule_id, finding.message));
}
output.push('\n');
}
output.push_str("---\n\n");
output.push_str("*Report generated by [RepoLens](https://github.com/systm-d/repolens)*\n");
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::results::Finding;
fn create_test_results() -> AuditResults {
let mut results = AuditResults::new("test-repo", "opensource");
results.add_finding(
Finding::new("SEC001", "secrets", Severity::Critical, "Secret exposed")
.with_location("src/config.rs:42")
.with_description("A secret was found in the code")
.with_remediation("Move the secret to environment variables"),
);
results.add_finding(
Finding::new("DOC001", "docs", Severity::Warning, "README missing")
.with_location("README.md")
.with_description("The README file is missing")
.with_remediation("Create a README.md file"),
);
results.add_finding(Finding::new(
"INFO001",
"info",
Severity::Info,
"Consider adding tests",
));
results
}
#[test]
fn test_markdown_report_new() {
let report = MarkdownReport::new(false);
assert!(!report.detailed);
}
#[test]
fn test_render_report_simple() {
let report = MarkdownReport::new(false);
let results = create_test_results();
let rendered = report.render_report(&results).unwrap();
assert!(rendered.contains("# RepoLens Audit Report"));
assert!(rendered.contains("**Repository:** test-repo"));
assert!(rendered.contains("**Preset:** opensource"));
assert!(rendered.contains("## Summary"));
assert!(rendered.contains("| Critical | 1 |"));
assert!(rendered.contains("| Warning | 1 |"));
assert!(rendered.contains("## Critical Issues"));
assert!(rendered.contains("SEC001"));
assert!(rendered.contains("## Warnings"));
assert!(rendered.contains("DOC001"));
}
#[test]
fn test_render_report_detailed() {
let report = MarkdownReport::new(true);
let results = create_test_results();
let rendered = report.render_report(&results).unwrap();
assert!(rendered.contains("A secret was found in the code"));
assert!(rendered.contains("**Remediation:**"));
assert!(rendered.contains("Move the secret to environment variables"));
assert!(rendered.contains("## Informational"));
assert!(rendered.contains("Consider adding tests"));
}
#[test]
fn test_render_report_no_critical() {
let report = MarkdownReport::new(false);
let mut results = AuditResults::new("clean-repo", "opensource");
results.add_finding(Finding::new(
"DOC001",
"docs",
Severity::Warning,
"Minor issue",
));
let rendered = report.render_report(&results).unwrap();
assert!(!rendered.contains("## Critical Issues"));
assert!(rendered.contains("## Warnings"));
}
#[test]
fn test_render_report_no_warnings() {
let report = MarkdownReport::new(false);
let mut results = AuditResults::new("clean-repo", "opensource");
results.add_finding(Finding::new(
"SEC001",
"secrets",
Severity::Critical,
"Critical issue",
));
let rendered = report.render_report(&results).unwrap();
assert!(rendered.contains("## Critical Issues"));
assert!(!rendered.contains("## Warnings"));
}
#[test]
fn test_render_report_empty() {
let report = MarkdownReport::new(false);
let results = AuditResults::new("empty-repo", "opensource");
let rendered = report.render_report(&results).unwrap();
assert!(rendered.contains("# RepoLens Audit Report"));
assert!(rendered.contains("| Critical | 0 |"));
assert!(rendered.contains("| Warning | 0 |"));
assert!(!rendered.contains("## Critical Issues"));
assert!(!rendered.contains("## Warnings"));
}
#[test]
fn test_render_report_contains_footer() {
let report = MarkdownReport::new(false);
let results = AuditResults::new("test-repo", "opensource");
let rendered = report.render_report(&results).unwrap();
assert!(rendered.contains("---"));
assert!(rendered.contains("*Report generated by [RepoLens]"));
}
}