canic-host 0.69.3

Host-side build, install, deployment, and fleet-template library for Canic workspaces
Documentation
use super::super::*;

#[test]
fn promotion_artifact_identity_report_round_trips_through_json() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    let report = promotion_artifact_identity_report("promotion-identity-1", &[input]);

    assert_json_round_trip(&report);
    let encoded = serde_json::to_value(&report).expect("identity report should encode");
    assert_object_keys(
        &encoded,
        &[
            "schema_version",
            "report_id",
            "artifact_identity_report_digest",
            "status",
            "summary",
            "roles",
            "identity_groups",
            "blockers",
        ],
    );
    assert_object_keys(
        &encoded["summary"],
        &[
            "role_count",
            "identity_group_count",
            "shared_identity_group_count",
            "digest_pinned_role_count",
            "source_build_role_count",
            "deferred_identity_role_count",
        ],
    );
    assert!(
        encoded["artifact_identity_report_digest"]
            .as_str()
            .is_some_and(|digest| digest.len() == 64)
    );
    let role = &encoded["roles"][0];
    assert_object_keys(
        role,
        &[
            "role",
            "promotion_level",
            "source_kind",
            "source_locator",
            "identity_kind",
            "digest_pinned",
            "wasm_sha256",
            "wasm_gz_sha256",
            "candid_sha256",
            "canonical_embedded_config_sha256",
        ],
    );
    let group = &encoded["identity_groups"][0];
    assert_object_keys(
        group,
        &[
            "identity_key",
            "identity_kind",
            "roles",
            "source_kinds",
            "source_locators",
            "digest_pinned",
            "wasm_sha256",
            "wasm_gz_sha256",
            "candid_sha256",
            "canonical_embedded_config_sha256",
        ],
    );
}

#[test]
fn promotion_artifact_identity_report_distinguishes_source_kind_from_identity_kind() {
    let mut input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    input.source.kind = RoleArtifactSourceKindV1::LocalWasm;
    input.source.expected_wasm_gz_sha256 = None;

    let report =
        promotion_artifact_identity_report_from_inputs(PromotionArtifactIdentityReportRequest {
            report_id: "promotion-identity-1".to_string(),
            inputs: vec![input],
        })
        .expect("identity report should be produced");

    assert_eq!(report.status, PromotionReadinessStatusV1::Ready);
    assert_eq!(report.roles.len(), 1);
    assert_eq!(
        report.roles[0].source_kind,
        RoleArtifactSourceKindV1::LocalWasm
    );
    assert_eq!(
        report.roles[0].identity_kind,
        PromotionArtifactIdentityKindV1::SealedWasm
    );
    assert!(report.roles[0].digest_pinned);
}

#[test]
fn promotion_artifact_identity_report_groups_roles_by_identity_key() {
    let mut root = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    root.source.kind = RoleArtifactSourceKindV1::LocalWasm;
    root.source.locator = Some("artifacts/root.wasm".to_string());
    root.source.expected_wasm_gz_sha256 = None;

    let mut worker = root.clone();
    worker.role = "worker".to_string();
    worker.source.role = "worker".to_string();
    worker.source.kind = RoleArtifactSourceKindV1::PreviousReceiptArtifact;
    worker.source.locator = Some("receipts/worker.json".to_string());
    worker.source.previous_receipt_kind = Some(PreviousArtifactReceiptKindV1::DeploymentReceipt);

    let report = promotion_artifact_identity_report("promotion-identity-1", &[root, worker]);

    assert_eq!(report.roles.len(), 2);
    assert_eq!(report.identity_groups.len(), 1);
    assert_eq!(report.summary.role_count, 2);
    assert_eq!(report.summary.identity_group_count, 1);
    assert_eq!(report.summary.shared_identity_group_count, 1);
    assert_eq!(report.summary.digest_pinned_role_count, 2);
    let group = &report.identity_groups[0];
    assert_eq!(
        group.identity_kind,
        PromotionArtifactIdentityKindV1::SealedWasm
    );
    assert_eq!(group.roles, vec!["root".to_string(), "worker".to_string()]);
    assert_eq!(
        group.source_kinds,
        vec![
            RoleArtifactSourceKindV1::LocalWasm,
            RoleArtifactSourceKindV1::PreviousReceiptArtifact
        ]
    );
    assert_eq!(
        group.source_locators,
        vec![
            "artifacts/root.wasm".to_string(),
            "receipts/worker.json".to_string()
        ]
    );
    assert_eq!(group.wasm_sha256, Some(sample_sha256("d")));
    assert!(group.identity_key.starts_with("sealed:wasm="));
    validate_promotion_artifact_identity_report(&report)
        .expect("grouped identity report should validate");
}

#[test]
fn promotion_artifact_identity_report_marks_source_build_identity() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SourceBuild);

    let report = promotion_artifact_identity_report("promotion-identity-1", &[input]);

    assert_eq!(report.status, PromotionReadinessStatusV1::Ready);
    assert_eq!(
        report.roles[0].identity_kind,
        PromotionArtifactIdentityKindV1::SourceBuild
    );
    assert_eq!(report.summary.source_build_role_count, 1);
}

#[test]
fn promotion_artifact_identity_report_records_invalid_source_as_blocker() {
    let mut input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    input.source.expected_wasm_sha256 = None;
    input.source.expected_wasm_gz_sha256 = None;

    let report = promotion_artifact_identity_report("promotion-identity-1", &[input]);

    assert_eq!(report.status, PromotionReadinessStatusV1::Blocked);
    assert_eq!(report.blockers.len(), 1);
    assert_eq!(report.blockers[0].code, "promotion_artifact_source_invalid");
    assert_eq!(
        report.roles[0].identity_kind,
        PromotionArtifactIdentityKindV1::Deferred
    );
    assert_eq!(report.summary.deferred_identity_role_count, 1);
    validate_promotion_artifact_identity_report(&report)
        .expect("blocked report should still validate");
}

#[test]
fn promotion_artifact_identity_report_text_reports_passive_summary() {
    let mut input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    input.source.kind = RoleArtifactSourceKindV1::LocalWasm;
    input.source.expected_wasm_gz_sha256 = None;
    let report = promotion_artifact_identity_report("promotion-identity-1", &[input]);

    let text = promotion_artifact_identity_report_text(&report);

    assert!(text.contains("Promotion artifact identity report"));
    assert!(text.contains("mode: passive"));
    assert!(text.contains("status: ready"));
    assert!(text.contains("report_id: promotion-identity-1"));
    assert!(text.contains("artifact_identity_report_digest:"));
    assert!(text.contains("identity_groups: 1"));
    assert!(text.contains("shared_identity_groups: 0"));
    assert!(text.contains("digest_pinned_roles: 1"));
    assert!(text.contains("source_build_roles: 0"));
    assert!(text.contains("deferred_identity_roles: 0"));
    assert!(text.contains("identity groups:"));
    assert!(
        text.contains("root SealedWasm/LocalWasm: identity_kind=SealedWasm digest_pinned=true")
    );
    assert!(text.contains("source_locator: artifacts/root.wasm.gz"));
    assert!(text.contains("wasm_gz_sha256: not recorded"));
}

#[test]
fn promotion_artifact_identity_report_validation_rejects_stale_summary() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    let mut report = promotion_artifact_identity_report("promotion-identity-1", &[input]);
    report.summary.identity_group_count = 2;

    let err = validate_promotion_artifact_identity_report(&report)
        .expect_err("stale summary should fail");

    std::assert_matches!(
        err,
        PromotionArtifactIdentityReportError::SummaryMismatch {
            field: "identity_group_count"
        }
    );
}

#[test]
fn promotion_artifact_identity_report_validation_rejects_stale_digest() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    let mut report = promotion_artifact_identity_report("promotion-identity-1", &[input]);
    report.artifact_identity_report_digest = sample_sha256("9");

    let err = validate_promotion_artifact_identity_report(&report)
        .expect_err("stale identity report digest should fail");

    std::assert_matches!(
        err,
        PromotionArtifactIdentityReportError::LinkageMismatch {
            field: "artifact_identity_report_digest"
        }
    );
}

#[test]
fn promotion_artifact_identity_report_validation_rejects_duplicate_roles() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    let mut report = promotion_artifact_identity_report("promotion-identity-1", &[input]);
    report.roles.push(report.roles[0].clone());

    let err = validate_promotion_artifact_identity_report(&report)
        .expect_err("duplicate role should fail");
    std::assert_matches!(
        err,
        PromotionArtifactIdentityReportError::DuplicateRole { role } if role == "root"
    );
}

#[test]
fn promotion_artifact_identity_report_validation_rejects_status_blocker_mismatch() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    let mut report = promotion_artifact_identity_report("promotion-identity-1", &[input]);
    report.status = PromotionReadinessStatusV1::Blocked;

    let err = validate_promotion_artifact_identity_report(&report)
        .expect_err("status blocker mismatch should fail");
    std::assert_matches!(
        err,
        PromotionArtifactIdentityReportError::StatusBlockerMismatch { .. }
    );
}

#[test]
fn promotion_artifact_identity_report_validation_rejects_bad_digest_shape() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    let mut report = promotion_artifact_identity_report("promotion-identity-1", &[input]);
    report.roles[0].wasm_gz_sha256 = Some("NOT-A-DIGEST".to_string());

    let err =
        validate_promotion_artifact_identity_report(&report).expect_err("bad digest should fail");
    std::assert_matches!(
        err,
        PromotionArtifactIdentityReportError::InvalidSha256Digest {
            field: "wasm_gz_sha256"
        }
    );
}

#[test]
fn promotion_artifact_identity_report_validation_rejects_stale_group_key() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    let mut report = promotion_artifact_identity_report("promotion-identity-1", &[input]);
    report.identity_groups[0].identity_key = "sealed:stale".to_string();

    let err = validate_promotion_artifact_identity_report(&report)
        .expect_err("stale group key should fail");
    std::assert_matches!(
        err,
        PromotionArtifactIdentityReportError::IdentityGroupKeyMismatch { .. }
    );
}

#[test]
fn promotion_artifact_identity_report_validation_rejects_ungrouped_role() {
    let input = sample_role_promotion_input(PromotionArtifactLevelV1::SealedWasm);
    let mut report = promotion_artifact_identity_report("promotion-identity-1", &[input]);
    report.identity_groups.clear();

    let err = validate_promotion_artifact_identity_report(&report)
        .expect_err("ungrouped role should fail");
    std::assert_matches!(
        err,
        PromotionArtifactIdentityReportError::MissingGroupedRole { role } if role == "root"
    );
}