feat(privacy-host): add signed native calendar contacts and reminders host
Add the owner-only AF_UNIX Reyna CLI privacy host, strict signed-app installation, and typed native routing for Calendar, Contacts, and Reminders.\n\nAdd bounded system-status paths and config-only direct local-service wrappers. Preserve MacMiniMCP pending explicit cutover approval.\n\nApple Notes is intentionally deferred: no native Notes operations, Apple Events declaration, or Automation helper are included; legacy Notes handling remains untouched.
This commit is contained in:
@@ -0,0 +1,774 @@
|
||||
"""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("<plist></plist>")
|
||||
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('<?xml version="1.0"?><plist><dict></dict></plist>')
|
||||
|
||||
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("<plist></plist>")
|
||||
|
||||
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("<plist></plist>")
|
||||
|
||||
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("<plist></plist>")
|
||||
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("<plist></plist>")
|
||||
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("<plist></plist>")
|
||||
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("<plist></plist>")
|
||||
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("<plist></plist>")
|
||||
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}"
|
||||
Reference in New Issue
Block a user