scrybe 2.1.0

Local-first meeting recording, transcription, and notes for macOS.
use std::cell::RefCell;

use super::*;

#[derive(Default)]
struct FakeRunner {
    calls: RefCell<Vec<Vec<String>>>,
    identities: String,
    fail_candidate_verify: bool,
}

impl FakeRunner {
    fn healthy() -> Self {
        Self {
            calls: RefCell::default(),
            identities: format!("1) HASH \"{PROJECT_SIGNING_IDENTITY}\"\n"),
            fail_candidate_verify: false,
        }
    }

    fn calls(&self) -> Vec<Vec<String>> {
        self.calls.borrow().clone()
    }
}

impl CommandRunner for FakeRunner {
    fn output(&self, program: &OsStr, args: &[OsString]) -> Result<CommandOutput> {
        let mut call = vec![program.to_string_lossy().into_owned()];
        call.extend(args.iter().map(|arg| arg.to_string_lossy().into_owned()));
        self.calls.borrow_mut().push(call.clone());

        if call.first().is_some_and(|value| value == "security") {
            return Ok(CommandOutput {
                success: true,
                stdout: self.identities.as_bytes().to_vec(),
                stderr: Vec::new(),
            });
        }
        if call.first().is_some_and(|value| value == "codesign") {
            let verifying = call.iter().any(|value| value == "--verify");
            let candidate = call
                .last()
                .is_some_and(|value| value.contains(".scrybe-install-"));
            let success = !(verifying && candidate && self.fail_candidate_verify);
            return Ok(CommandOutput {
                success,
                stdout: Vec::new(),
                stderr: if success {
                    Vec::new()
                } else {
                    b"invalid candidate".to_vec()
                },
            });
        }
        anyhow::bail!("unexpected command: {}", call.join(" "))
    }
}

fn write_bundle(path: &Path) {
    fs::create_dir_all(path.join("Contents/MacOS")).unwrap();
    fs::create_dir_all(path.join("Contents/Resources")).unwrap();
    fs::write(
        path.join("Contents/Info.plist"),
        render_plist(env!("CARGO_PKG_VERSION")),
    )
    .unwrap();
    fs::write(path.join("Contents/MacOS/scrybe"), b"binary").unwrap();
    fs::write(
        path.join("Contents/Resources/entitlements.plist"),
        ENTITLEMENTS,
    )
    .unwrap();
}

#[test]
fn reports_missing_invalid_stale_and_ready_states() {
    let temp = tempfile::tempdir().unwrap();
    let bundle = temp.path().join(BUNDLE_FILE_NAME);
    let runner = FakeRunner::healthy();

    assert_eq!(
        inspect_bundle_with(&bundle, env!("CARGO_PKG_VERSION"), &runner),
        BundleState::Missing
    );

    fs::create_dir(&bundle).unwrap();
    assert!(matches!(
        inspect_bundle_with(&bundle, env!("CARGO_PKG_VERSION"), &runner),
        BundleState::Invalid { .. }
    ));

    fs::remove_dir(&bundle).unwrap();
    write_bundle(&bundle);
    assert!(matches!(
        inspect_bundle_with(&bundle, "99.0.0", &runner),
        BundleState::Stale { .. }
    ));
    assert_eq!(
        inspect_bundle_with(&bundle, env!("CARGO_PKG_VERSION"), &runner),
        BundleState::Ready
    );
}

#[test]
fn inspection_never_executes_the_bundled_binary() {
    let temp = tempfile::tempdir().unwrap();
    let bundle = temp.path().join(BUNDLE_FILE_NAME);
    write_bundle(&bundle);
    let runner = FakeRunner::healthy();

    assert_eq!(
        inspect_bundle_with(&bundle, env!("CARGO_PKG_VERSION"), &runner),
        BundleState::Ready
    );
    assert!(runner
        .calls()
        .iter()
        .all(|call| call.first().is_some_and(|program| program == "codesign")));
}

#[test]
fn identity_resolution_never_selects_an_unrelated_identity() {
    let runner = FakeRunner {
        identities: "1) HASH \"Developer ID Application: Someone Else\"\n".to_string(),
        ..FakeRunner::healthy()
    };

    let error = resolve_signing_identity_with(None, &runner).unwrap_err();

    assert!(error.to_string().contains(PROJECT_SIGNING_IDENTITY));
    assert!(!error.to_string().contains("Someone Else"));
}

#[test]
fn explicit_identity_takes_precedence_over_project_default() {
    let runner = FakeRunner {
        identities: format!("1) HASH \"{PROJECT_SIGNING_IDENTITY}\"\n2) HASH \"explicit-local\"\n"),
        ..FakeRunner::healthy()
    };

    assert_eq!(
        resolve_signing_identity_with(Some("explicit-local"), &runner).unwrap(),
        "explicit-local"
    );
}

#[test]
fn failed_candidate_verification_preserves_existing_bundle() {
    let temp = tempfile::tempdir().unwrap();
    let binary = temp.path().join("source-scrybe");
    let destination = temp.path().join(BUNDLE_FILE_NAME);
    fs::write(&binary, b"new binary").unwrap();
    fs::create_dir(&destination).unwrap();
    fs::write(destination.join("sentinel"), b"existing bundle").unwrap();
    let runner = FakeRunner {
        fail_candidate_verify: true,
        ..FakeRunner::healthy()
    };

    let error =
        install_bundle_with(&binary, &destination, PROJECT_SIGNING_IDENTITY, &runner).unwrap_err();

    assert!(error.to_string().contains("verifying bundle candidate"));
    assert_eq!(
        fs::read(destination.join("sentinel")).unwrap(),
        b"existing bundle"
    );
    assert_eq!(
        fs::read_dir(temp.path())
            .unwrap()
            .filter_map(Result::ok)
            .filter(|entry| entry
                .file_name()
                .to_string_lossy()
                .contains(".scrybe-install-"))
            .count(),
        0
    );
}

#[test]
fn verified_candidate_replaces_existing_bundle_after_verification() {
    let temp = tempfile::tempdir().unwrap();
    let binary = temp.path().join("source-scrybe");
    let destination = temp.path().join(BUNDLE_FILE_NAME);
    fs::write(&binary, b"new binary").unwrap();
    fs::create_dir(&destination).unwrap();
    fs::write(destination.join("sentinel"), b"existing bundle").unwrap();
    let runner = FakeRunner::healthy();

    install_bundle_with(&binary, &destination, PROJECT_SIGNING_IDENTITY, &runner).unwrap();

    assert_eq!(
        fs::read(destination.join("Contents/MacOS/scrybe")).unwrap(),
        b"new binary"
    );
    assert!(!destination.join("sentinel").exists());
    let calls = runner.calls();
    let sign_index = calls
        .iter()
        .position(|call| call.iter().any(|arg| arg == "--force"))
        .unwrap();
    let verify_index = calls
        .iter()
        .position(|call| call.iter().any(|arg| arg == "--verify"))
        .unwrap();
    assert!(sign_index < verify_index);
}

#[test]
fn rendered_plist_has_no_unexpanded_version_marker() {
    let plist = render_plist("1.3.2");

    assert!(plist.contains("<string>1.3.2</string>"));
    assert!(!plist.contains("{{VERSION}}"));
}