"""Tests for Reyna CLI.app bundle builder — deterministic layout, Xcode-owned signing, fail-closed.""" from __future__ import annotations import plistlib import stat from pathlib import Path import pytest def _mock_proc(rc=0, stdout="", stderr=""): from types import SimpleNamespace return SimpleNamespace(returncode=rc, stdout=stdout, stderr=stderr) # ---------------------------------------------------------------------- # Bundle layout & deterministic plist # ---------------------------------------------------------------------- def test_app_bundle_paths_deterministic_repo_local(): from reyna_cli import app_bundle as ab assert ab.BUNDLE_IDENTIFIER == "com.reyna.cli.privacy-host" assert ab.APP_BUNDLE_NAME == "Reyna CLI.app" assert ab.APP_EXECUTABLE_NAME == "ReynaCLIHost" bundle = ab.app_bundle_path() assert "dist" in str(bundle) assert "Reyna CLI.app" in str(bundle) assert bundle.name == "Reyna CLI.app" assert bundle.parent.name == "dist" assert bundle.parent.parent.name == "ReynaCLIHost" def test_build_app_bundle_info_plist_deterministic(): from reyna_cli import app_bundle as ab d1 = ab.build_app_bundle_info_plist_dict() d2 = ab.build_app_bundle_info_plist_dict() assert d1 == d2, "plist dict must be deterministic" assert d1["CFBundleIdentifier"] == "com.reyna.cli.privacy-host" assert d1["CFBundleExecutable"] == "ReynaCLIHost" assert d1["CFBundleName"] == "Reyna CLI" assert d1["CFBundleDisplayName"] == "Reyna CLI" assert d1["CFBundlePackageType"] == "APPL" assert "NSCalendarsFullAccessUsageDescription" in d1 assert "NSContactsUsageDescription" in d1 assert "NSRemindersFullAccessUsageDescription" in d1 assert "NSAppleEventsUsageDescription" not in d1, "Notes deferred — AppleEvents must be forbidden" forbidden = [ "NSRemindersUsageDescription", "NSAppleMusicUsageDescription", "NSNotesUsageDescription", "NSMailUsageDescription", "NSAppleEventsUsageDescription", ] for k in forbidden: assert k not in d1, f"forbidden key {k} present" data = plistlib.dumps(d1, sort_keys=True) loaded = plistlib.loads(data) assert loaded == d1 def test_app_bundle_info_plist_allows_write_only_optional_but_not_forbidden(): from reyna_cli import app_bundle as ab d = ab.build_app_bundle_info_plist_dict() assert "NSCalendarsFullAccessUsageDescription" in d assert "NSContactsUsageDescription" in d assert "NSRemindersFullAccessUsageDescription" in d assert "NSAppleEventsUsageDescription" not in d, "AppleEvents forbidden — Notes deferred" for key in d.keys(): if "UsageDescription" in key and key.startswith("NS"): assert key in { "NSCalendarsFullAccessUsageDescription", "NSCalendarsWriteOnlyAccessUsageDescription", "NSCalendarsUsageDescription", "NSContactsUsageDescription", "NSRemindersFullAccessUsageDescription", }, f"unexpected usage description {key}" def test_app_bundle_info_plist_no_contacts_reminders_notes_mail(): from reyna_cli import app_bundle as ab d = ab.build_app_bundle_info_plist_dict() # Only Calendar, Contacts, Reminders allowed. Notes/Mail/AppleEvents must be rejected (Notes deferred) for bad in ["Notes", "Mail", "AppleEvents"]: for k in d.keys(): if bad.lower() in k.lower() and "UsageDescription" in k: raise AssertionError(f"bundle plist contains forbidden domain {bad} via {k}") for k in d.keys(): low = k.lower() if "notesusage" in low or "mailusage" in low or "appleeventsusage" in low: raise AssertionError(f"forbidden domain key {k}") # ---------------------------------------------------------------------- # Xcode-owned signing – no manual codesign --sign # ---------------------------------------------------------------------- def test_build_xcodebuild_command_static_contract(): from reyna_cli import app_bundle as ab import tempfile repo_root = Path(tempfile.mkdtemp()) / "repo" (repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost" / "Info.plist").write_text("") derived = repo_root / "custom" / "DerivedData" cmd = ab.build_xcodebuild_command(repo_root=repo_root, derived_data_path=derived, disable_code_signing=False) assert isinstance(cmd, list) assert cmd[0] == "xcodebuild" # No shell assert all(isinstance(x, str) for x in cmd) # Must reference project, scheme, configuration, derivedDataPath, build verb assert "-project" in cmd proj_idx = cmd.index("-project") assert str(repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost.xcodeproj") == cmd[proj_idx + 1] assert "-scheme" in cmd assert "Reyna CLI" in cmd assert "-target" not in cmd, "must not use -target when using -derivedDataPath (RC 64)" assert "-configuration" in cmd assert "Release" in cmd assert "-derivedDataPath" in cmd dd_idx = cmd.index("-derivedDataPath") assert cmd[dd_idx + 1] == str(derived) assert "build" in cmd # Must NOT contain manual codesign signing command assert "--sign" not in cmd assert "CODE_SIGNING_ALLOWED=NO" not in cmd cmd_unsigned = ab.build_xcodebuild_command( repo_root=repo_root, derived_data_path=derived, disable_code_signing=True ) assert "CODE_SIGNING_ALLOWED=NO" in cmd_unsigned assert "--sign" not in cmd_unsigned def test_build_xcodebuild_command_no_manual_codesign_in_source(): from reyna_cli import app_bundle as ab src = Path(ab.__file__).read_text() # Source must NOT contain codesign --sign manual invocation (Xcode owns signing) # Allow comments about codesign --verify but not --sign as command construction lines = [l for l in src.splitlines() if "codesign" in l.lower() and "--sign" in l] # Only allowed if inside comment about not doing manual sign, not as actual command list for line in lines: stripped = line.strip() if stripped.startswith("#") or stripped.startswith('"""') or stripped.startswith("'''"): continue # If we ever build ["codesign", "--force", ... "--sign"] that would be violation # Our module only uses codesign --verify and -dv for validation if '"codesign"' in line or "'codesign'" in line or '["codesign"' in line: assert "--sign" not in line or "verify" in line.lower(), f"manual codesign --sign found: {line}" # Strong check: no list containing both codesign and --sign for forced signing assert '["codesign", "--force"' not in src, "manual codesign --sign forbidden; Xcode owns signing" assert src.count('"--sign"') == 0 or 'codesign' not in src.split('"--sign"')[0][-200:].lower() or True # Final guard: validate no manual signing identity handling that invokes codesign --sign # Searching for pattern codesign.*--sign in code (not in comments) — we already checked above def test_app_bundle_no_manual_codesign_sign_invocation(): from reyna_cli import app_bundle as ab src = Path(ab.__file__).read_text() # Must not have ["codesign", "--force", "--options", "runtime", "--timestamp", "--sign", identity # The old builder used this; new builder must not assert "codesign" in src.lower() # verify still allowed # Ensure we never build a codesign --sign command array forbidden_snippets = [ '"--force",\n "--options",\n "runtime"', 'sign_cmd = [', ] for snippet in forbidden_snippets: if snippet in src: # If present, ensure it's not constructing --sign command ctx = src[src.index(snippet) - 200 : src.index(snippet) + 400] if snippet in src else "" assert "--sign" not in ctx or "verify" in ctx.lower(), f"found manual sign cmd near {snippet}: {ctx[:500]}" def test_build_app_bundle_uses_xcodebuild_and_copies_product(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" pkg_dir = repo_root / "native" / "ReynaCLIHost" pkg_dir.mkdir(parents=True) # Create required xcodeproj dir and info plist source (pkg_dir / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (pkg_dir / "ReynaCLIHost.xcodeproj" / "project.pbxproj").write_text("// dummy") (pkg_dir / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (pkg_dir / "ReynaCLIHost" / "Info.plist").write_text('') derived = repo_root / "native" / "ReynaCLIHost" / "build" / "DerivedData" built_app = derived / "Build" / "Products" / "Release" / "Reyna CLI.app" (built_app / "Contents" / "MacOS").mkdir(parents=True) (built_app / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"fakebinarycontent") with open(built_app / "Contents" / "Info.plist", "wb") as f: plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) calls = [] class Proc: def __init__(self, rc=0, stdout="", stderr=""): self.returncode = rc self.stdout = stdout self.stderr = stderr def runner(args, cwd=None, **kwargs): assert isinstance(args, list), "must use arg array" calls.append(list(args)) if args and args[0] == "xcodebuild": # simulate successful build - ensure product already exists return Proc(rc=0, stdout="BUILD SUCCEEDED", stderr="") if args[:3] == ["codesign", "--verify", "--deep"]: return Proc(rc=0) if args[:2] == ["codesign", "-dv"]: return Proc(rc=0, stdout="", stderr="TeamIdentifier=TEAM123\nAuthority=Apple Development: Foo (TEAM123)\n") return Proc(rc=0) result = ab.build_app_bundle(repo_root=repo_root, runner=runner, disable_code_signing=False) assert result["ok"] is True assert "xcodebuild" in str(result.get("build_command", [])).lower() or any(c[0] == "xcodebuild" for c in calls) # Ensure xcodebuild invocation used safe arg array with project/scheme/derivedDataPath xb_calls = [c for c in calls if c and c[0] == "xcodebuild"] assert len(xb_calls) == 1 xb = xb_calls[0] assert "-project" in xb assert "-scheme" in xb assert "-target" not in xb assert "Reyna CLI" in xb assert "-derivedDataPath" in xb assert "build" in xb assert "--sign" not in xb # No manual codesign --sign sign_calls = [c for c in calls if c[0] == "codesign" and "--sign" in c] assert len(sign_calls) == 0, f"manual codesign --sign must not occur, got {sign_calls}" # Product copied to dist dist_app = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app" assert dist_app.exists() assert (dist_app / "Contents" / "MacOS" / "ReynaCLIHost").exists() def test_build_app_bundle_fails_if_xcodebuild_fails(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" pkg_dir = repo_root / "native" / "ReynaCLIHost" (pkg_dir / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (pkg_dir / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (pkg_dir / "ReynaCLIHost" / "Info.plist").write_text("") def runner(args, cwd=None, **kwargs): if args and args[0] == "xcodebuild": return _mock_proc(rc=1, stderr="BUILD FAILED") return _mock_proc(rc=0) res = ab.build_app_bundle(repo_root=repo_root, runner=runner) assert res["ok"] is False assert "xcodebuild" in res["error"].lower() def test_build_app_bundle_fails_if_product_missing_after_build(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" pkg_dir = repo_root / "native" / "ReynaCLIHost" (pkg_dir / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (pkg_dir / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (pkg_dir / "ReynaCLIHost" / "Info.plist").write_text("") def runner(args, cwd=None, **kwargs): if args and args[0] == "xcodebuild": return _mock_proc(rc=0) return _mock_proc(rc=0) res = ab.build_app_bundle(repo_root=repo_root, runner=runner) assert res["ok"] is False assert "product not found" in res["error"].lower() def test_build_app_bundle_unsigned_mode_allows_validation_failure(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" pkg_dir = repo_root / "native" / "ReynaCLIHost" (pkg_dir / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (pkg_dir / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (pkg_dir / "ReynaCLIHost" / "Info.plist").write_text("") derived = repo_root / "native" / "ReynaCLIHost" / "build" / "DerivedData" built_app = derived / "Build" / "Products" / "Release" / "Reyna CLI.app" (built_app / "Contents" / "MacOS").mkdir(parents=True) (built_app / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"bin") with open(built_app / "Contents" / "Info.plist", "wb") as f: plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) def runner(args, cwd=None, **kwargs): if args and args[0] == "xcodebuild": return _mock_proc(rc=0) if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=1, stderr="code object is not signed at all") if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="Signature=adhoc\nTeamIdentifier=not set\n") return _mock_proc(rc=0) res = ab.build_app_bundle(repo_root=repo_root, runner=runner, disable_code_signing=True) # Unsigned build should succeed even if validation fails (for testing) assert res["ok"] is True assert res.get("unsigned_build") is True # But validation indicates not verified assert res["validation"]["signature_verified"] is False def test_build_app_bundle_validation_detects_ad_hoc_signed(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" pkg_dir = repo_root / "native" / "ReynaCLIHost" (pkg_dir / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (pkg_dir / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (pkg_dir / "ReynaCLIHost" / "Info.plist").write_text("") derived = repo_root / "native" / "ReynaCLIHost" / "build" / "DerivedData" built_app = derived / "Build" / "Products" / "Release" / "Reyna CLI.app" (built_app / "Contents" / "MacOS").mkdir(parents=True) (built_app / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"bin") with open(built_app / "Contents" / "Info.plist", "wb") as f: plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) def runner(args, cwd=None, **kwargs): if args and args[0] == "xcodebuild": return _mock_proc(rc=0) if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=0) if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stdout="", stderr="Signature=adhoc\nTeamIdentifier=not set\n") return _mock_proc(rc=0) res = ab.build_app_bundle(repo_root=repo_root, runner=runner, disable_code_signing=False) assert res["ok"] is False assert "ad-hoc" in (res.get("error", "") + str(res.get("validation", {}))).lower() def test_build_app_bundle_verify_failure_fail_closed(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" pkg_dir = repo_root / "native" / "ReynaCLIHost" (pkg_dir / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (pkg_dir / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (pkg_dir / "ReynaCLIHost" / "Info.plist").write_text("") derived = repo_root / "native" / "ReynaCLIHost" / "build" / "DerivedData" built_app = derived / "Build" / "Products" / "Release" / "Reyna CLI.app" (built_app / "Contents" / "MacOS").mkdir(parents=True) (built_app / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"bin") with open(built_app / "Contents" / "Info.plist", "wb") as f: plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) def runner(args, cwd=None, **kwargs): if args and args[0] == "xcodebuild": return _mock_proc(rc=0) if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=1, stderr="code failed to satisfy") if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="TeamIdentifier=TEAM123\n") return _mock_proc(rc=0) res = ab.build_app_bundle(repo_root=repo_root, runner=runner, disable_code_signing=False) assert res["ok"] is False assert "verify" in res["error"].lower() or "validation" in res["error"].lower() # ---------------------------------------------------------------------- # Validation – same as before, plus xcodeproj contract # ---------------------------------------------------------------------- def test_validate_app_bundle_layout_and_signature(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" bundle = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app" contents = bundle / "Contents" macos = contents / "MacOS" macos.mkdir(parents=True) exe = macos / "ReynaCLIHost" exe.write_bytes(b"binary") exe.chmod(0o755) plist_dict = ab.build_app_bundle_info_plist_dict() with open(contents / "Info.plist", "wb") as f: plistlib.dump(plist_dict, f) def runner_ok(args, cwd=None, **kwargs): if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=0) if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="Executable=...\nIdentifier=com.reyna.cli.privacy-host\nFormat=app bundle\nAuthority=Apple Development: Foo (TEAM123)\nTeamIdentifier=TEAM123\n") return _mock_proc(rc=0) res = ab.validate_app_bundle(repo_root=repo_root, runner=runner_ok) assert res["ok"] is True assert res["bundle_exists"] is True assert res["executable_exists"] is True assert res["info_plist_exists"] is True assert res["bundle_identifier"] == "com.reyna.cli.privacy-host" assert res["bundle_identifier_matches"] is True assert res["signature_verified"] is True assert res["is_ad_hoc"] is False def test_validate_app_bundle_accepts_only_calendar_usage(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" bundle = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app" contents = bundle / "Contents" macos = contents / "MacOS" macos.mkdir(parents=True) (macos / "ReynaCLIHost").write_bytes(b"x") plist_dict = ab.build_app_bundle_info_plist_dict() plist_dict["NSRemindersUsageDescription"] = "Should not be allowed" with open(contents / "Info.plist", "wb") as f: plistlib.dump(plist_dict, f) def runner(args, cwd=None, **kwargs): if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=0) if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="TeamIdentifier=TEAM123\n") return _mock_proc(rc=0) res = ab.validate_app_bundle(repo_root=repo_root, runner=runner) assert res["ok"] is False assert any("reminders" in e.lower() or "forbidden" in e.lower() or "unexpected" in e.lower() for e in res["errors"]) def test_validate_app_bundle_fails_if_ad_hoc(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" bundle = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app" contents = bundle / "Contents" macos = contents / "MacOS" macos.mkdir(parents=True) (macos / "ReynaCLIHost").write_bytes(b"x") with open(contents / "Info.plist", "wb") as f: plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) def runner(args, cwd=None, **kwargs): if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=0) if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="Signature=adhoc\nTeamIdentifier=not set\n") return _mock_proc(rc=0) res = ab.validate_app_bundle(repo_root=repo_root, runner=runner) assert res["ok"] is False assert res["is_ad_hoc"] is True assert res["signature_verified"] is False def test_validate_app_bundle_fails_if_verify_fails(tmp_path): from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" bundle = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app" (bundle / "Contents" / "MacOS").mkdir(parents=True) (bundle / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"x") with open(bundle / "Contents" / "Info.plist", "wb") as f: plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) def runner(args, cwd=None, **kwargs): if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=1, stderr="main executable failed strict validation") if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="TeamIdentifier=TEAM123\n") return _mock_proc(rc=0) res = ab.validate_app_bundle(repo_root=repo_root, runner=runner) assert res["ok"] is False assert res["signature_verified"] is False def test_xcodeproject_static_contract(tmp_path): """Verify xcodeproject and Info.plist static contract exist.""" from reyna_cli import app_bundle as ab repo_root = Path(ab.__file__).resolve().parents[2] proj = repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost.xcodeproj" / "project.pbxproj" assert proj.exists(), f"xcodeproj missing at {proj}" src = proj.read_text() assert "com.reyna.cli.privacy-host" in src assert "Reyna CLI" in src assert "CODE_SIGN_STYLE = Automatic" in src info_src = repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost" / "Info.plist" assert info_src.exists() with open(info_src, "rb") as f: d = plistlib.load(f) assert d["CFBundleIdentifier"] == "com.reyna.cli.privacy-host" assert d["CFBundleExecutable"] == "ReynaCLIHost" def test_build_app_bundle_no_secret_logging(tmp_path): """Result must not contain secret identity in clear.""" from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" pkg_dir = repo_root / "native" / "ReynaCLIHost" (pkg_dir / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (pkg_dir / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (pkg_dir / "ReynaCLIHost" / "Info.plist").write_text("") derived = repo_root / "native" / "ReynaCLIHost" / "build" / "DerivedData" built_app = derived / "Build" / "Products" / "Release" / "Reyna CLI.app" (built_app / "Contents" / "MacOS").mkdir(parents=True) (built_app / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"bin") with open(built_app / "Contents" / "Info.plist", "wb") as f: plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) def runner(args, cwd=None, **kwargs): if args and args[0] == "xcodebuild": return _mock_proc(rc=0) if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=0) if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="TeamIdentifier=TEAM123\n") return _mock_proc(rc=0) secret_identity = "Apple Development: Very Secret Name (TEAM999)" res = ab.build_app_bundle(signing_identity=secret_identity, repo_root=repo_root, runner=runner) assert res["ok"] is True res_str = str(res) # secret team must not leak (Automatic Signing means we don't use identity at all) assert "TEAM999" not in res_str assert secret_identity not in res_str def test_lifecycle_refuses_invalid_unverified_bundle(tmp_path): from reyna_cli import privacy_host as ph_mod from reyna_cli import app_bundle as ab repo_root = tmp_path / "repo" repo_root.mkdir() (repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost.xcodeproj").mkdir(parents=True) (repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost").mkdir(parents=True, exist_ok=True) (repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost" / "Info.plist").write_text("") derived = repo_root / "native" / "ReynaCLIHost" / "build" / "DerivedData" built_app = derived / "Build" / "Products" / "Release" / "Reyna CLI.app" (built_app / "Contents" / "MacOS").mkdir(parents=True) (built_app / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"bin") with open(built_app / "Contents" / "Info.plist", "wb") as f: plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) sock = tmp_path / "priv" / "reyna-cli.sock" plist = tmp_path / "LaunchAgents" / "com.reyna.cli.privacy-host.plist" logd = tmp_path / "Logs" def fake_runner(args, cwd=None, **kwargs): if args and args[0] == "xcodebuild": return _mock_proc(rc=0) if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=1, stderr="verify failed") if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="TeamIdentifier=TEAM123\n") if args[0] == "launchctl": return _mock_proc(rc=0) return _mock_proc(rc=0) result = ph_mod.install_privacy_host_service( runner=fake_runner, uid=501, repo_root=repo_root, socket_path=sock, plist_path=plist, log_dir=logd, signing_identity="Test ID" ) assert result["ok"] is False assert "verify" in result["error"].lower() or "validation" in result["error"].lower() def test_lifecycle_start_refuses_unverified_bundle(tmp_path): from reyna_cli import privacy_host as ph_mod repo_root = tmp_path / "repo" repo_root.mkdir() (repo_root / "native" / "ReynaCLIHost").mkdir(parents=True) plist = tmp_path / "LaunchAgents" / "com.reyna.cli.privacy-host.plist" plist.parent.mkdir(parents=True) import plistlib as _plist plist.write_bytes(_plist.dumps({"Label": ph_mod.PRIVACY_HOST_LABEL})) def fake_runner(args, cwd=None, **kwargs): if args[:3] == ["codesign", "--verify", "--deep"]: return _mock_proc(rc=1, stderr="verify failed") if args[:2] == ["codesign", "-dv"]: return _mock_proc(rc=0, stderr="Signature=adhoc\nTeamIdentifier=not set\n") return _mock_proc(rc=0) bundle = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app" (bundle / "Contents" / "MacOS").mkdir(parents=True) (bundle / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"x") with open(bundle / "Contents" / "Info.plist", "wb") as f: from reyna_cli import app_bundle as ab _plist.dump(ab.build_app_bundle_info_plist_dict(), f) res = ph_mod.start_privacy_host_service(runner=fake_runner, uid=501, plist_path=plist, repo_root=repo_root) assert res["ok"] is False assert "bundle" in res["error"].lower() def test_status_outputs_app_bundle_path_and_verification_state(tmp_path): from reyna_cli import privacy_host as ph_mod, app_bundle as ab repo_root = tmp_path / "repo" (repo_root / "native" / "ReynaCLIHost").mkdir(parents=True) bundle = repo_root / "native" / "ReynaCLIHost" / "dist" / "Reyna CLI.app" (bundle / "Contents" / "MacOS").mkdir(parents=True) (bundle / "Contents" / "MacOS" / "ReynaCLIHost").write_bytes(b"x") with open(bundle / "Contents" / "Info.plist", "wb") as f: import plistlib plistlib.dump(ab.build_app_bundle_info_plist_dict(), f) sock = tmp_path / "sock" / "reyna-cli.sock" plist_path = tmp_path / "LaunchAgents" / "com.reyna.cli.privacy-host.plist" plist_path.parent.mkdir(parents=True) import plistlib plist_path.write_bytes(plistlib.dumps({"Label": ph_mod.PRIVACY_HOST_LABEL})) class Proc: returncode = 0 stdout = "pid = 1234\n" stderr = "" def runner(args, cwd=None, **kwargs): if isinstance(args, list) and args and args[0] == "codesign" and "--verify" in args: return Proc() if isinstance(args, list) and args[:2] == ["codesign", "-dv"]: class P: returncode = 0 stdout = "" stderr = "TeamIdentifier=TEAM123\nAuthority=Apple Development: Foo (TEAM123)\n" return P() if isinstance(args, list) and args[0] == "launchctl": return Proc() return Proc() status = ph_mod.privacy_host_service_status( runner=runner, uid=501, plist_path_override=plist_path, socket_path_override=sock, repo_root_override=repo_root ) assert status["ok"] is True assert "app_bundle_path" in status assert "bundle_identifier" in status assert status["bundle_identifier"] == "com.reyna.cli.privacy-host" assert status["bundle_identifier_expected"] == "com.reyna.cli.privacy-host" assert "signature_verified" in status assert "bundle_exists" in status assert status["app_bundle_path_expected"] == str(bundle) # ---------------------------------------------------------------------- # Contacts migration readiness – Xcode linkage + deterministic nil date # ---------------------------------------------------------------------- def test_xcode_contacts_source_files_and_framework_linked(): from reyna_cli import app_bundle as ab repo_root = Path(ab.__file__).resolve().parents[2] proj = repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost.xcodeproj" / "project.pbxproj" assert proj.exists() src = proj.read_text() # Contacts source files must be in project assert "ContactsProvider.swift" in src, "ContactsProvider.swift missing from pbxproj" assert "ContactsAuthorizationProvider.swift" in src, "ContactsAuthorizationProvider.swift missing" # Must be in Sources build phase assert "ContactsProvider.swift in Sources" in src assert "ContactsAuthorizationProvider.swift in Sources" in src # Must be in ReynaCLIHostCore group # Find the core group and check its children include both assert "ReynaCLIHostCore" in src # Framework must be linked assert "Contacts.framework" in src, "Contacts.framework missing from pbxproj" assert "Contacts.framework in Frameworks" in src, "Contacts.framework not in Frameworks phase" # Also EventKit still present assert "EventKit.framework" in src def test_package_swift_links_contacts_framework(): from reyna_cli import app_bundle as ab repo_root = Path(ab.__file__).resolve().parents[2] pkg = repo_root / "native" / "ReynaCLIHost" / "Package.swift" assert pkg.exists() content = pkg.read_text() assert "Contacts" in content assert 'linkedFramework("Contacts")' in content # Should still link EventKit assert 'linkedFramework("EventKit")' in content def test_contacts_provider_nil_modification_date_deterministic(): from reyna_cli import app_bundle as ab repo_root = Path(ab.__file__).resolve().parents[2] provider = repo_root / "native" / "ReynaCLIHost" / "Sources" / "ReynaCLIHostCore" / "ContactsProvider.swift" assert provider.exists() src = provider.read_text() # Must not use iso.string(from: Date()) as fallback – nondeterministic # Count occurrences that use current Date as fallback assert "iso.string(from: Date())" not in src, "ContactsProvider must not fallback to current Date() – nondeterministic" # Ensure the replacement is deterministic (empty string) # Both search and read providers should have deterministic fallback assert '?? ""' in src or 'modifiedStr = ""' in src or "= \"\"" in src def test_xcode_info_plist_and_generated_plist_alignment_calendar_plus_contacts(): from reyna_cli import app_bundle as ab import plistlib repo_root = Path(ab.__file__).resolve().parents[2] xcode_plist = repo_root / "native" / "ReynaCLIHost" / "ReynaCLIHost" / "Info.plist" assert xcode_plist.exists() with open(xcode_plist, "rb") as f: xcode_d = plistlib.load(f) gen_d = ab.build_app_bundle_info_plist_dict() # Both must have Calendar, Contacts, Reminders usage descriptions for key in ["NSCalendarsFullAccessUsageDescription", "NSContactsUsageDescription", "NSRemindersFullAccessUsageDescription"]: assert key in xcode_d, f"Xcode Info.plist missing {key}" assert key in gen_d, f"generated plist missing {key}" # Bundle identity alignment assert xcode_d["CFBundleIdentifier"] == gen_d["CFBundleIdentifier"] == "com.reyna.cli.privacy-host" assert xcode_d["CFBundleExecutable"] == gen_d["CFBundleExecutable"] == "ReynaCLIHost" # Security: no Notes/Mail/AppleEvents in either (Notes deferred) for d, label in [(xcode_d, "Xcode"), (gen_d, "generated")]: for bad in ["Notes", "Mail", "AppleEvents"]: for k in d.keys(): if bad.lower() in k.lower() and "UsageDescription" in k: raise AssertionError(f"{label} plist contains forbidden domain {bad} via {k}") for k in d.keys(): low = k.lower() if "notesusage" in low or "mailusage" in low or "appleeventsusage" in low: raise AssertionError(f"{label} plist contains forbidden domain via {k}") # Allowed set only: Calendar, Contacts, Reminders (Notes deferred, no AppleEvents) allowed = { "NSCalendarsFullAccessUsageDescription", "NSCalendarsWriteOnlyAccessUsageDescription", "NSCalendarsUsageDescription", "NSContactsUsageDescription", "NSRemindersFullAccessUsageDescription", } for d, label in [(xcode_d, "Xcode"), (gen_d, "generated")]: for k in d.keys(): if k.startswith("NS") and "UsageDescription" in k: assert k in allowed, f"{label} plist has unexpected usage key {k}"