secretspec 0.20.0

A declarative interface for every secret provider.
Documentation
use secretspec::{Profile, Secret, SecretSpecError, Spec};

fn test_spec() -> Spec {
    Spec::builder("schema-test")
        .secret("SHARED_TOKEN", Secret::required("Shared token"))
        .secret("OPTIONAL_TOKEN", Secret::optional("Optional token"))
        .profile(
            "production",
            Profile::new().secret("PRODUCTION_TOKEN", Secret::required("Production token")),
        )
        .build()
        .unwrap()
}

#[test]
fn public_api_emits_union_and_profile_schemas() {
    let spec = test_spec();

    let union: serde_json::Value = serde_json::from_str(&spec.schema_json(None).unwrap()).unwrap();
    assert_eq!(union["title"], "SecretSpec");
    assert_eq!(union["additionalProperties"], false);
    assert_eq!(union["properties"]["SHARED_TOKEN"]["type"], "string");
    assert_eq!(
        union["properties"]["OPTIONAL_TOKEN"]["type"],
        serde_json::json!(["string", "null"])
    );
    assert!(union["properties"]["PRODUCTION_TOKEN"].is_object());

    let production: serde_json::Value =
        serde_json::from_str(&spec.schema_json(Some("production")).unwrap()).unwrap();
    assert_eq!(production["title"], "ProductionSecrets");
    assert!(production["properties"]["SHARED_TOKEN"].is_object());
    assert!(production["properties"]["PRODUCTION_TOKEN"].is_object());
    assert!(production["properties"]["OPTIONAL_TOKEN"].is_object());
}

#[test]
fn public_api_reports_an_unknown_profile() {
    let error = test_spec().schema_json(Some("missing")).unwrap_err();
    match error {
        SecretSpecError::InvalidProfile(message) => {
            assert!(message.contains("missing"));
            assert!(message.contains("production"));
        }
        other => panic!("expected InvalidProfile, got {other:?}"),
    }
}

#[cfg(feature = "cli")]
#[test]
fn cli_schema_matches_the_public_api() {
    use std::fs;
    use std::process::Command;

    let project = tempfile::tempdir().unwrap();
    let path = project.path().join("secretspec.toml");
    fs::write(
        &path,
        r#"[project]
name = "schema-test"
revision = "1.0"

[profiles.default]
SHARED_TOKEN = { description = "Shared token", required = true }

[profiles.production]
PRODUCTION_TOKEN = { description = "Production token", required = true }
"#,
    )
    .unwrap();

    let expected = Spec::try_from(path.as_path())
        .unwrap()
        .schema_json(Some("production"))
        .unwrap();
    let output = Command::new(env!("CARGO_BIN_EXE_secretspec"))
        .args([
            "--file",
            path.to_str().unwrap(),
            "schema",
            "--profile",
            "production",
        ])
        .output()
        .unwrap();

    assert!(
        output.status.success(),
        "schema command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert_eq!(String::from_utf8(output.stdout).unwrap(), expected);
}