lenso-cli 0.5.1

Authoring CLI for Lenso Plugins and App intent.
Documentation
use super::*;
use lenso_app_plan::authoring::{PluginInstanceId, PluginRootInstance};
use serde_json::json;

fn descriptor(id: &str) -> PluginDescriptor {
    PluginDescriptor::new(id, "1.0.0", "store")
        .with_configuration_schema(json!({"type":"object"}))
        .with_configuration_defaults(json!({"limit": 4, "network": {"hosts": []}}))
}

fn policy(release: PluginDescriptor) -> SlotAdmission {
    SlotAdmission {
        slot: "store".into(),
        max_instances: 1,
        releases: vec![AdmittedRelease {
            descriptor: release,
            manifest_digest: format!("sha256:{}", "a".repeat(64)),
        }],
        configuration_schema: Some(json!({"type":"object", "properties": {
            "limit": {"type":"integer", "maximum":8},
            "network": {"type":"object", "properties": {"hosts": {"type":"array", "items":{"enum":["api.example.com"]}}}}
        }})),
    }
}

fn snapshot(descriptor: &PluginDescriptor, configurations: &[(&str, Value)]) -> PluginRootSnapshot {
    PluginRootSnapshot::new(
        [descriptor.clone()],
        configurations.iter().map(|(name, configuration)| {
            PluginRootInstance::new(descriptor.plugin_id(), *name)
                .with_configuration(configuration.clone())
        }),
        [],
    )
}

#[test]
fn extension_is_inactive_until_enabled_and_counts_only_selected_instances() {
    let release = descriptor("company.store");
    let host = GeneratedHostBuild::lower_with_admission(
        "company.app",
        vec![],
        vec![HostSlot::many("store")],
        vec![policy(release.clone())],
    )
    .unwrap();
    assert_eq!(
        host.resolve(&snapshot(&release, &[]))
            .unwrap()
            .instances()
            .len(),
        0
    );
    assert_eq!(
        host.resolve(&snapshot(&release, &[("a", json!({}))]))
            .unwrap()
            .instances()
            .len(),
        1
    );
    assert!(
        host.resolve(&snapshot(&release, &[("a", json!({})), ("b", json!({}))]))
            .unwrap_err()
            .to_string()
            .contains("maxInstances")
    );
    let disabled = PluginRootSnapshot::new(
        [release],
        [
            PluginRootInstance::new("company.store", "a"),
            PluginRootInstance::new("company.store", "b"),
        ],
        [PluginInstanceId::new("company.store", "b")],
    );
    assert_eq!(host.resolve(&disabled).unwrap().instances().len(), 1);
}

#[test]
fn ceilings_check_merged_values_and_never_echo_sensitive_values() {
    let release = descriptor("company.store");
    let host = GeneratedHostBuild::lower_with_admission(
        "company.app",
        vec![],
        vec![HostSlot::many("store")],
        vec![policy(release.clone())],
    )
    .unwrap();
    assert!(
        host.resolve(&snapshot(
            &release,
            &[(
                "a",
                json!({"limit":8,"network":{"hosts":["api.example.com"]}})
            )]
        ))
        .is_ok()
    );
    for configuration in [
        json!({"limit":9}),
        json!({"network":{"hosts":["do-not-echo-this-secret"]}}),
    ] {
        let error = host
            .resolve(&snapshot(&release, &[("a", configuration)]))
            .unwrap_err()
            .to_string();
        assert!(error.contains("configuration ceiling"));
        assert!(!error.contains("do-not-echo-this-secret"));
    }
    let unsafe_defaults = release.with_configuration_defaults(json!({"limit": 20}));
    let host = GeneratedHostBuild::lower_with_admission(
        "company.app",
        vec![],
        vec![HostSlot::many("store")],
        vec![policy(unsafe_defaults.clone())],
    )
    .unwrap();
    assert!(
        host.resolve(&snapshot(&unsafe_defaults, &[("a", json!({}))]))
            .is_err()
    );
    assert!(
        host.resolve(&snapshot(&unsafe_defaults, &[("a", json!({"limit":4}))]))
            .is_ok()
    );
}

#[test]
fn explicit_replacement_uses_the_shared_resolver_without_counting_the_retired_default() {
    let default = || HostPluginInput {
        descriptor: descriptor("company.original"),
        instance: "default".into(),
        configuration: json!({}),
        source: "app.ts:1".into(),
    };
    let replacement = descriptor("company.replacement");
    let candidate = snapshot(&replacement, &[("default", json!({}))]);
    let closed = GeneratedHostBuild::lower_with_admission(
        "company.app",
        vec![default()],
        vec![HostSlot::one("store")],
        vec![policy(replacement.clone())],
    )
    .unwrap();
    assert!(closed.resolve(&candidate).is_err());
    let open = GeneratedHostBuild::lower_with_admission(
        "company.app",
        vec![default()],
        vec![HostSlot::one("store").replaceable()],
        vec![policy(replacement)],
    )
    .unwrap();
    let resolved = open.resolve(&candidate).unwrap();
    assert_eq!(resolved.instances().len(), 1);
    assert_eq!(
        resolved.instances()[0].id().plugin_id(),
        "company.replacement"
    );
}

#[test]
fn malformed_and_remote_schemas_fail_even_in_an_inactive_branch() {
    for schema in [
        json!({"maximun":8}),
        json!({"if":false,"then":{"$ref":"https://example.com/schema"}}),
        json!({"type":"not-a-type"}),
    ] {
        let mut rule = policy(descriptor("company.store"));
        rule.configuration_schema = Some(schema);
        assert!(
            GeneratedHostBuild::lower_with_admission(
                "company.app",
                vec![],
                vec![HostSlot::many("store")],
                vec![rule]
            )
            .is_err()
        );
    }
}

#[test]
fn changed_ceiling_invalidates_a_reviewed_configuration_proposal() {
    let root = tempfile::tempdir().unwrap();
    std::fs::create_dir(root.path().join(".lenso")).unwrap();
    let mut host = GeneratedHostBuild::lower_with_admission(
        "company.app",
        vec![HostPluginInput {
            descriptor: descriptor("company.store"),
            instance: "default".into(),
            configuration: json!({}),
            source: "app.ts:1".into(),
        }],
        vec![HostSlot::one("store")],
        vec![policy(descriptor("company.store"))],
    )
    .unwrap();
    let publish = |host: &GeneratedHostBuild| {
        std::fs::write(
            root.path().join(HOST_BUILD),
            serde_json::to_vec(host).unwrap(),
        )
        .unwrap();
    };
    publish(&host);
    let revision = crate::inspect_plugin_root(root.path())
        .unwrap()
        .revision()
        .clone();
    let proposal = crate::propose_instance_configuration(
        root.path(),
        &revision,
        "company.store",
        "default",
        b"limit = 8\n",
    )
    .unwrap();
    host.admissions[0].configuration_schema = Some(json!({"properties":{"limit":{"maximum":5}}}));
    publish(&host);
    assert!(crate::publish_instance_configuration(root.path(), &proposal).is_err());
    assert!(
        !root
            .path()
            .join("plugins/company.store/default.toml")
            .exists()
    );
}