checkleft 0.1.0-alpha.8

Experimental repository convention checker; API and behavior may change without notice
Documentation
use std::fs;
use std::path::Path;

use tempfile::tempdir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

use crate::config::{CheckConfigOrigin, ConfigResolver, ConfigResolverOptions};

#[test]
fn resolves_yaml_config_file() {
    let temp = tempdir().expect("create temp dir");
    fs::write(
        temp.path().join("CHECKS.yaml"),
        r#"
checks:
  - id: file-size
    config:
      max_lines: 321
"#,
    )
    .expect("write config file");

    let resolver = ConfigResolver::new(temp.path()).expect("create resolver");
    let checks = resolver
        .resolve_for_file(Path::new("backend/src/lib.rs"))
        .expect("resolve checks");

    assert_eq!(
        checks
            .get("file-size")
            .expect("file-size present")
            .config
            .as_table()
            .expect("file-size config table")
            .get("max_lines")
            .expect("max_lines")
            .as_integer(),
        Some(321)
    );
}

#[test]
fn malformed_yaml_reports_diagnostic_instead_of_failing() {
    let temp = tempdir().expect("create temp dir");
    fs::write(
        temp.path().join("CHECKS.yaml"),
        r#"
checks:
  - id: file-size
    config:
      max_lines: [1, 2
"#,
    )
    .expect("write config file");

    let resolver = ConfigResolver::new(temp.path()).expect("create resolver");
    let checks = resolver
        .resolve_for_file(Path::new("backend/src/lib.rs"))
        .expect("resolve checks");
    let diagnostics: Vec<_> = checks.diagnostics().collect();

    assert_eq!(checks.enabled().count(), 0);
    assert_eq!(diagnostics.len(), 1);
    assert_eq!(diagnostics[0].check_id, "checks-config");
    assert_eq!(diagnostics[0].location.path, Path::new("CHECKS.yaml"));
    assert!(
        diagnostics[0]
            .message
            .contains("failed to parse checks config")
    );
}

#[tokio::test]
async fn merges_external_yaml_before_local_root_yaml() {
    let temp = tempdir().expect("create temp dir");
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/shared/CHECKS.yaml"))
        .respond_with(ResponseTemplate::new(200).set_body_string(
            r#"
checks:
  - id: shared-file-size
    config:
      max_lines: 999
  - id: external-only
"#,
        ))
        .mount(&server)
        .await;

    fs::write(
        temp.path().join("CHECKS.yaml"),
        format!(
            r#"
settings:
  external_checks_url: "{}/shared/CHECKS.yaml"
checks:
  - id: shared-file-size
    config:
      max_lines: 321
  - id: local-only
"#,
            server.uri()
        ),
    )
    .expect("write config file");

    let resolver = ConfigResolver::new_with_options(temp.path(), ConfigResolverOptions::default())
        .await
        .expect("create resolver");
    let checks = resolver
        .resolve_for_file(Path::new("backend/src/lib.rs"))
        .expect("resolve checks");

    assert!(checks.get("external-only").is_some());
    assert!(checks.get("local-only").is_some());
    assert_eq!(
        checks
            .get("shared-file-size")
            .expect("shared-file-size present")
            .config
            .as_table()
            .expect("shared-file-size config table")
            .get("max_lines")
            .expect("max_lines")
            .as_integer(),
        Some(321)
    );
}

#[tokio::test]
async fn supports_cli_external_checks_url_without_root_config() {
    let temp = tempdir().expect("create temp dir");
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/shared/CHECKS.yaml"))
        .respond_with(ResponseTemplate::new(200).set_body_string(
            r#"
checks:
  - id: external-only
"#,
        ))
        .mount(&server)
        .await;

    let resolver = ConfigResolver::new_with_options(
        temp.path(),
        ConfigResolverOptions {
            external_checks_file: None,
            external_checks_url: Some(format!("{}/shared/CHECKS.yaml", server.uri())),
        },
    )
    .await
    .expect("create resolver");
    let checks = resolver
        .resolve_for_file(Path::new("backend/src/lib.rs"))
        .expect("resolve checks");

    assert!(checks.get("external-only").is_some());
}

#[tokio::test]
async fn merges_external_checks_file_before_local_root_yaml() {
    let temp = tempdir().expect("create temp dir");
    let external_path = temp.path().join("shared/CHECKS.yaml");
    fs::create_dir_all(external_path.parent().expect("shared dir")).expect("create shared dir");
    fs::write(
        &external_path,
        r#"
checks:
  - id: shared-file-size
    config:
      max_lines: 999
  - id: external-only
"#,
    )
    .expect("write external config");

    fs::write(
        temp.path().join("CHECKS.yaml"),
        r#"
checks:
  - id: shared-file-size
    config:
      max_lines: 321
  - id: local-only
"#,
    )
    .expect("write local config");

    let resolver = ConfigResolver::new_with_options(
        temp.path(),
        ConfigResolverOptions {
            external_checks_file: Some(external_path.display().to_string()),
            external_checks_url: None,
        },
    )
    .await
    .expect("create resolver");
    let checks = resolver
        .resolve_for_file(Path::new("backend/src/lib.rs"))
        .expect("resolve checks");

    assert!(checks.get("external-only").is_some());
    assert!(checks.get("local-only").is_some());
    assert_eq!(
        checks
            .get("external-only")
            .expect("external-only present")
            .origin,
        CheckConfigOrigin::ExternalFile
    );
    assert_eq!(
        checks
            .get("shared-file-size")
            .expect("shared-file-size present")
            .config
            .as_table()
            .expect("shared-file-size config table")
            .get("max_lines")
            .expect("max_lines")
            .as_integer(),
        Some(321)
    );
}

#[tokio::test]
async fn rejects_file_implementation_from_external_checks_file() {
    let temp = tempdir().expect("create temp dir");
    let external_path = temp.path().join("shared/CHECKS.yaml");
    fs::create_dir_all(external_path.parent().expect("shared dir")).expect("create shared dir");
    fs::write(
        &external_path,
        r#"
checks:
  - id: domain-typo
    check: domain-typo-check
    implementation: checks/domain_typo.check.toml
"#,
    )
    .expect("write external config");

    let resolver = ConfigResolver::new_with_options(
        temp.path(),
        ConfigResolverOptions {
            external_checks_file: Some(external_path.display().to_string()),
            external_checks_url: None,
        },
    )
    .await
    .expect("create resolver");
    let error = resolver
        .resolve_for_file(Path::new("backend/src/lib.rs"))
        .expect_err("resolution must fail");

    assert!(
        error
            .to_string()
            .contains("external checks files may only use `generated:` implementations")
    );
}

#[tokio::test]
async fn fails_when_external_checks_url_returns_404() {
    let temp = tempdir().expect("create temp dir");
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/missing/CHECKS.yaml"))
        .respond_with(ResponseTemplate::new(404))
        .expect(5)
        .mount(&server)
        .await;

    let error = ConfigResolver::new_with_options(
        temp.path(),
        ConfigResolverOptions {
            external_checks_file: None,
            external_checks_url: Some(format!("{}/missing/CHECKS.yaml", server.uri())),
        },
    )
    .await
    .expect_err("resolver must fail");

    assert!(
        error
            .to_string()
            .contains("returned 404 Not Found after 5 attempts")
    );
}