verifyos-cli 0.2.1

A pure Rust CLI tool to scan Apple app bundles for App Store rejection risks before submission.
Documentation
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
use verifyos_cli::parsers::plist_reader::InfoPlist;
use verifyos_cli::rules::core::{AppStoreRule, ArtifactContext, RuleStatus};
use verifyos_cli::rules::info_plist::LSApplicationQueriesSchemesAuditRule;
use verifyos_cli::rules::info_plist::UIRequiredDeviceCapabilitiesAuditRule;
use verifyos_cli::rules::permissions::CameraUsageDescriptionRule;
use verifyos_cli::rules::privacy::MissingPrivacyManifestRule;
use verifyos_cli::rules::signing::EmbeddedCodeSignatureTeamRule;

fn get_fixture_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join("TestApp.app")
}

#[test]
fn test_privacy_manifest_rule_passes() {
    let app_path = get_fixture_path();
    let context = ArtifactContext {
        app_bundle_path: &app_path,
        info_plist: None,
    };

    let rule = MissingPrivacyManifestRule;
    let result = rule.evaluate(&context).expect("Rule should evaluate");
    assert_eq!(result.status, RuleStatus::Pass);
}

#[test]
fn test_privacy_manifest_rule_fails() {
    let app_path = PathBuf::from("does_not_exist.app");
    let context = ArtifactContext {
        app_bundle_path: &app_path,
        info_plist: None,
    };

    let rule = MissingPrivacyManifestRule;
    let result = rule.evaluate(&context).expect("Rule should evaluate");
    assert_eq!(result.status, RuleStatus::Fail);
}

#[test]
fn test_camera_usage_rule_passes() {
    let app_path = get_fixture_path();
    let plist_path = app_path.join("Info.plist");
    let plist = InfoPlist::from_file(&plist_path).unwrap();

    let context = ArtifactContext {
        app_bundle_path: &app_path,
        info_plist: Some(&plist),
    };

    let rule = CameraUsageDescriptionRule;
    let result = rule.evaluate(&context).expect("Rule should evaluate");
    assert_eq!(result.status, RuleStatus::Pass);
}

#[test]
fn test_embedded_team_rule_skips_without_executable() {
    let app_path = get_fixture_path();
    let plist_path = app_path.join("Info.plist");
    let plist = InfoPlist::from_file(&plist_path).unwrap();

    let context = ArtifactContext {
        app_bundle_path: &app_path,
        info_plist: Some(&plist),
    };

    let rule = EmbeddedCodeSignatureTeamRule;
    let result = rule.evaluate(&context).expect("Rule should evaluate");
    assert_eq!(result.status, RuleStatus::Skip);
}

#[test]
fn test_lsapplicationqueries_schemes_passes() {
    let mut dict = plist::Dictionary::new();
    dict.insert(
        "LSApplicationQueriesSchemes".to_string(),
        plist::Value::Array(vec![
            plist::Value::String("fb".to_string()),
            plist::Value::String("twitter".to_string()),
        ]),
    );

    let plist = InfoPlist::from_dictionary(dict);
    let app_path = PathBuf::from("does_not_exist.app");
    let context = ArtifactContext {
        app_bundle_path: &app_path,
        info_plist: Some(&plist),
    };

    let rule = LSApplicationQueriesSchemesAuditRule;
    let result = rule.evaluate(&context).expect("Rule should evaluate");
    assert_eq!(result.status, RuleStatus::Pass);
}

#[test]
fn test_lsapplicationqueries_schemes_fails_on_duplicates() {
    let mut dict = plist::Dictionary::new();
    dict.insert(
        "LSApplicationQueriesSchemes".to_string(),
        plist::Value::Array(vec![
            plist::Value::String("fb".to_string()),
            plist::Value::String("fb".to_string()),
            plist::Value::String("prefs".to_string()),
        ]),
    );

    let plist = InfoPlist::from_dictionary(dict);
    let app_path = PathBuf::from("does_not_exist.app");
    let context = ArtifactContext {
        app_bundle_path: &app_path,
        info_plist: Some(&plist),
    };

    let rule = LSApplicationQueriesSchemesAuditRule;
    let result = rule.evaluate(&context).expect("Rule should evaluate");
    assert_eq!(result.status, RuleStatus::Fail);
}

#[test]
fn test_device_capabilities_audit_fails_on_missing_usage() {
    let dir = tempdir().expect("temp dir");
    let app_dir = dir.path().join("TestApp.app");
    fs::create_dir_all(&app_dir).expect("create app dir");

    let executable_path = app_dir.join("TestApp");
    fs::write(&executable_path, b"no usage signatures").expect("write executable");

    let mut dict = plist::Dictionary::new();
    dict.insert(
        "UIRequiredDeviceCapabilities".to_string(),
        plist::Value::Array(vec![plist::Value::String("camera".to_string())]),
    );
    let plist = InfoPlist::from_dictionary(dict);

    let context = ArtifactContext {
        app_bundle_path: &app_dir,
        info_plist: Some(&plist),
    };

    let rule = UIRequiredDeviceCapabilitiesAuditRule;
    let result = rule.evaluate(&context).expect("Rule should evaluate");
    assert_eq!(result.status, RuleStatus::Fail);
}

#[test]
fn test_device_capabilities_audit_passes_on_usage() {
    let dir = tempdir().expect("temp dir");
    let app_dir = dir.path().join("TestApp.app");
    fs::create_dir_all(&app_dir).expect("create app dir");

    let executable_path = app_dir.join("TestApp");
    fs::write(&executable_path, b"AVCaptureDevice").expect("write executable");

    let mut dict = plist::Dictionary::new();
    dict.insert(
        "UIRequiredDeviceCapabilities".to_string(),
        plist::Value::Array(vec![plist::Value::String("camera".to_string())]),
    );
    let plist = InfoPlist::from_dictionary(dict);

    let context = ArtifactContext {
        app_bundle_path: &app_dir,
        info_plist: Some(&plist),
    };

    let rule = UIRequiredDeviceCapabilitiesAuditRule;
    let result = rule.evaluate(&context).expect("Rule should evaluate");
    assert_eq!(result.status, RuleStatus::Pass);
}