canic-host 0.99.5

Host-side App build, Fleet install, deployment, and release-set library for Canic workspaces
Documentation
use super::*;
use crate::test_support::temp_dir;
use std::fs;

#[test]
fn catalog_reads_network_scoped_fleet_rows_in_canonical_order() {
    let root = fixture("list");
    let network = CanonicalNetworkId::public_ic();
    write_catalog(
        &root,
        network,
        vec![
            entry(network, 1, "alpha", "shop", "staging", "aaaaa-aa"),
            entry(network, 2, "zeta", "shop", "production", "2vxsx-fae"),
        ],
    );

    let report = build_fleet_catalog_report(&request(&root, "staging")).expect("Fleet catalog");

    assert_eq!(report.canonical_network_id, network);
    assert_eq!(report.environment, "staging");
    assert_eq!(
        report
            .entries
            .iter()
            .map(|entry| entry.fleet_name.as_str())
            .collect::<Vec<_>>(),
        vec!["alpha", "zeta"]
    );
    assert_eq!(report.entries[0].app.as_str(), "shop");
    assert_eq!(report.entries[0].environment, "staging");
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn environment_aliases_read_the_same_canonical_network_catalog() {
    let root = fixture("aliases");
    let network = CanonicalNetworkId::public_ic();
    write_catalog(
        &root,
        network,
        vec![entry(
            network,
            1,
            "shop-production",
            "shop",
            "production",
            "aaaaa-aa",
        )],
    );

    let staging = build_fleet_catalog_report(&request(&root, "staging")).expect("staging catalog");
    let production =
        build_fleet_catalog_report(&request(&root, "production")).expect("production catalog");

    assert_eq!(
        staging.canonical_network_id,
        production.canonical_network_id
    );
    assert_eq!(staging.entries, production.entries);
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn missing_catalog_is_empty_and_never_reads_legacy_deployment_state() {
    let root = fixture("no-legacy-fallback");
    let legacy = root.join(".canic/staging/deployments/shop.json");
    fs::create_dir_all(legacy.parent().expect("legacy parent")).expect("legacy directory");
    fs::write(&legacy, b"{\"deployment_name\":\"shop\"}").expect("legacy state");

    let report = build_fleet_catalog_report(&request(&root, "staging")).expect("empty catalog");

    assert!(report.entries.is_empty());
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn catalog_rejects_wrong_network_unsorted_names_and_duplicate_ids() {
    let root = fixture("invalid");
    let network = CanonicalNetworkId::public_ic();
    let path = fleet_catalog_path(&root, network);

    write_catalog_record(
        &path,
        FleetCatalogRecord {
            schema_version: FLEET_CATALOG_SCHEMA_VERSION,
            canonical_network_id: FleetId::from_generated_bytes([8; 32])
                .to_string()
                .parse()
                .expect("network-shaped text"),
            entries: Vec::new(),
        },
    );
    assert!(matches!(
        build_fleet_catalog_report(&request(&root, "staging")),
        Err(FleetCatalogError::Invalid { .. })
    ));

    write_catalog(
        &root,
        network,
        vec![
            entry(network, 1, "zeta", "shop", "staging", "aaaaa-aa"),
            entry(network, 2, "alpha", "shop", "staging", "2vxsx-fae"),
        ],
    );
    assert!(matches!(
        build_fleet_catalog_report(&request(&root, "staging")),
        Err(FleetCatalogError::Invalid { .. })
    ));

    write_catalog(
        &root,
        network,
        vec![
            entry(network, 1, "alpha", "shop", "staging", "aaaaa-aa"),
            entry(network, 1, "zeta", "shop", "staging", "2vxsx-fae"),
        ],
    );
    assert!(matches!(
        build_fleet_catalog_report(&request(&root, "staging")),
        Err(FleetCatalogError::Invalid { .. })
    ));
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn catalog_rejects_malformed_unknown_field_and_invalid_identity_rows() {
    let root = fixture("malformed");
    let network = CanonicalNetworkId::public_ic();
    let path = fleet_catalog_path(&root, network);
    fs::create_dir_all(path.parent().expect("catalog parent")).expect("catalog directory");
    fs::write(&path, b"{not-json").expect("malformed catalog");
    assert!(matches!(
        build_fleet_catalog_report(&request(&root, "staging")),
        Err(FleetCatalogError::Decode { .. })
    ));

    let mut value = serde_json::to_value(FleetCatalogRecord {
        schema_version: FLEET_CATALOG_SCHEMA_VERSION,
        canonical_network_id: network,
        entries: vec![entry(
            network,
            1,
            "shop-production",
            "bad/app",
            "staging",
            "aaaaa-aa",
        )],
    })
    .expect("catalog value");
    value
        .as_object_mut()
        .expect("catalog object")
        .insert("legacy".to_string(), serde_json::Value::Bool(true));
    fs::write(&path, serde_json::to_vec(&value).expect("catalog JSON")).expect("unknown field");
    assert!(matches!(
        build_fleet_catalog_report(&request(&root, "staging")),
        Err(FleetCatalogError::Decode { .. })
    ));

    write_catalog(
        &root,
        network,
        vec![entry(
            network,
            1,
            "shop-production",
            "bad/app",
            "staging",
            "aaaaa-aa",
        )],
    );
    assert!(matches!(
        build_fleet_catalog_report(&request(&root, "staging")),
        Err(FleetCatalogError::Invalid { .. })
    ));
    fs::remove_dir_all(root).expect("remove fixture");
}

#[cfg(unix)]
#[test]
fn catalog_rejects_symlinked_authority() {
    use std::os::unix::fs::symlink;

    let root = fixture("symlink");
    let network = CanonicalNetworkId::public_ic();
    let path = fleet_catalog_path(&root, network);
    fs::create_dir_all(path.parent().expect("catalog parent")).expect("catalog directory");
    let target = root.join("catalog-target.json");
    write_catalog_record(
        &target,
        FleetCatalogRecord {
            schema_version: FLEET_CATALOG_SCHEMA_VERSION,
            canonical_network_id: network,
            entries: Vec::new(),
        },
    );
    symlink(&target, &path).expect("catalog symlink");

    assert!(matches!(
        build_fleet_catalog_report(&request(&root, "staging")),
        Err(FleetCatalogError::NotRegular { .. })
    ));
    fs::remove_dir_all(root).expect("remove fixture");
}

#[cfg(unix)]
#[test]
fn catalog_rejects_special_file_authority() {
    use rustix::fs::{CWD, Mode, mkfifoat};

    let root = fixture("special-file");
    let network = CanonicalNetworkId::public_ic();
    let path = fleet_catalog_path(&root, network);
    fs::create_dir_all(path.parent().expect("catalog parent")).expect("catalog directory");
    mkfifoat(CWD, &path, Mode::from_raw_mode(0o600)).expect("catalog FIFO");

    assert!(matches!(
        build_fleet_catalog_report(&request(&root, "staging")),
        Err(FleetCatalogError::NotRegular { .. })
    ));
    fs::remove_file(path).expect("remove FIFO");
    fs::remove_dir_all(root).expect("remove fixture");
}

#[test]
fn catalog_inspect_and_text_use_fleet_identity_terms() {
    let root = fixture("inspect");
    let network = CanonicalNetworkId::public_ic();
    write_catalog(
        &root,
        network,
        vec![entry(
            network,
            9,
            "shop-production",
            "shop",
            "production",
            "aaaaa-aa",
        )],
    );

    let report = inspect_fleet_catalog_report(&request(&root, "staging"), "shop-production")
        .expect("inspect Fleet");
    let text = fleet_catalog_report_text(&report);

    assert_eq!(report.entries.len(), 1);
    assert!(text.contains("Fleet catalog:"));
    assert!(text.contains("fleet_id:"));
    assert!(text.contains("app: shop"));
    assert!(!text.contains("deployment target"));
    assert!(matches!(
        inspect_fleet_catalog_report(&request(&root, "staging"), "unknown"),
        Err(FleetCatalogError::UnknownFleet { .. })
    ));
    fs::remove_dir_all(root).expect("remove fixture");
}

fn fixture(name: &str) -> PathBuf {
    let root = temp_dir(&format!("canic-fleet-catalog-{name}"));
    fs::create_dir_all(&root).expect("create project root");
    fs::write(
        root.join("icp.yaml"),
        "environments:\n  - name: staging\n    network: ic\n  - name: production\n    network: ic\n",
    )
    .expect("write ICP config");
    root
}

fn request(root: &Path, environment: &str) -> FleetCatalogRequest {
    FleetCatalogRequest {
        project_root: root.to_path_buf(),
        environment: environment.to_string(),
        generated_at: "unix:54".to_string(),
    }
}

fn write_catalog(root: &Path, network: CanonicalNetworkId, entries: Vec<FleetCatalogEntryV1>) {
    write_catalog_record(
        &fleet_catalog_path(root, network),
        FleetCatalogRecord {
            schema_version: FLEET_CATALOG_SCHEMA_VERSION,
            canonical_network_id: network,
            entries,
        },
    );
}

fn write_catalog_record(path: &Path, catalog: FleetCatalogRecord) {
    fs::create_dir_all(path.parent().expect("catalog parent")).expect("catalog directory");
    fs::write(
        path,
        serde_json::to_vec_pretty(&catalog).expect("catalog JSON"),
    )
    .expect("write catalog");
}

fn entry(
    network: CanonicalNetworkId,
    id_byte: u8,
    fleet_name: &str,
    app: &str,
    environment: &str,
    root_principal: &str,
) -> FleetCatalogEntryV1 {
    FleetCatalogEntryV1 {
        canonical_network_id: network,
        fleet_id: FleetId::from_generated_bytes([id_byte; 32]),
        fleet_name: fleet_name.parse().expect("Fleet name"),
        app: AppId::from(app),
        environment: environment.to_string(),
        deployed_at_unix_secs: 54,
        root_principal: root_principal.to_string(),
        root_verification: FleetCatalogRootVerificationV1::Verified,
    }
}