Files
dropbox_helper/tests/test_platform_macos.py
T
jared e363020080
CI / Tests (Linux, Python 3.12) (push) Failing after 4m51s
CI / Tests (Linux, Python 3.11) (push) Failing after 5m19s
CI / Tests (Linux, Python 3.9) (push) Failing after 5m3s
CI / Tests (macOS) (push) Has been cancelled
CI / Tests (Windows) (push) Has been cancelled
feat: initial Dropbox tag sync tool
Cross-platform CLI that mirrors Dropbox web tags to OS-native tags:
- macOS: Finder xattrs (content-preserving, no re-upload)
- Windows: XMP:Subject via ExifTool (re-upload expected once per file)

Includes an interactive wizard that auto-detects the OS and Dropbox
folder, walks the user through read-only token creation, previews
tagged files, applies tags, and installs a background scheduler
(launchd on macOS, Task Scheduler on Windows).

Ships with:
- 47-test suite (pytest) covering config, state DB, run_sync, writers
- Gitea Actions CI for Linux / macOS / Windows with coverage reporting
2026-04-20 14:16:50 -04:00

141 lines
4.3 KiB
Python

"""Tests for tagsync.platform_macos.
The plist encoding test runs anywhere. The xattr round-trip runs only on
systems where the extended-attribute syscalls are available (macOS and
most Linux filesystems).
"""
from __future__ import annotations
import os
import plistlib
import sys
from pathlib import Path
import pytest
from tagsync.platform_macos import FINDER_TAGS_XATTR, MacWriter
def test_mac_writer_supports_all_file_types():
w = MacWriter()
for path in ("a.jpg", "b.zip", "c.txt", "noextension"):
assert w.supports(Path(path))
def test_mac_writer_is_online_only_is_false():
w = MacWriter()
assert w.is_online_only(Path("/any/file")) is False
def test_plist_encoding_matches_finder_format():
"""Finder expects a binary plist of a string array."""
from tagsync.platform_macos import plistlib # re-export
tags = ["alpha", "beta", "gamma"]
encoded = plistlib.dumps(tags, fmt=plistlib.FMT_BINARY)
# Binary plists start with 'bplist'.
assert encoded.startswith(b"bplist")
decoded = plistlib.loads(encoded)
assert decoded == tags
def _xattr_supported(path: Path) -> bool:
"""Can we setxattr / getxattr on files in this directory?"""
if not hasattr(os, "setxattr") or not hasattr(os, "getxattr"):
return False
try:
# Use a neutral namespace; on Linux, user.* always works on ext4.
os.setxattr(path, "user.tagsync_probe", b"ok")
val = os.getxattr(path, "user.tagsync_probe")
os.removexattr(path, "user.tagsync_probe")
return val == b"ok"
except OSError:
return False
@pytest.fixture
def xattr_capable_file(tmp_path):
f = tmp_path / "target"
f.touch()
if not _xattr_supported(f):
pytest.skip("filesystem does not support extended attributes")
return f
def test_write_and_read_tags_via_xattr(xattr_capable_file):
"""Round-trip the actual xattr the script uses in production.
On Linux, the 'com.apple.metadata:*' namespace may not be allowed,
so this test uses the MacWriter only on Darwin.
"""
if sys.platform != "darwin":
pytest.skip("Finder xattr namespace only writable on macOS")
w = MacWriter()
w.write_tags(xattr_capable_file, ["alpha", "beta"])
raw = os.getxattr(xattr_capable_file, FINDER_TAGS_XATTR)
assert plistlib.loads(raw) == ["alpha", "beta"]
def test_clear_tags_via_xattr(xattr_capable_file):
if sys.platform != "darwin":
pytest.skip("Finder xattr namespace only writable on macOS")
w = MacWriter()
w.write_tags(xattr_capable_file, ["alpha"])
w.write_tags(xattr_capable_file, [])
with pytest.raises(OSError):
os.getxattr(xattr_capable_file, FINDER_TAGS_XATTR)
def test_clear_tags_when_none_set_is_noop(tmp_path):
"""Removing a non-existent xattr should not raise."""
if sys.platform != "darwin":
pytest.skip("requires macOS xattr semantics")
f = tmp_path / "fresh"
f.touch()
MacWriter().write_tags(f, []) # should not raise
# --- launchd plist generation -----------------------------------------------
def test_install_schedule_writes_plist(tmp_path, monkeypatch):
"""We don't want to actually call launchctl; intercept it and verify
the plist contents."""
import tagsync.platform_macos as pm
calls = []
class FakeResult:
returncode = 0
stderr = ""
stdout = ""
monkeypatch.setattr(pm.subprocess, "run", lambda args, **kw: calls.append(args) or FakeResult())
# Redirect plist + log paths into tmp to avoid touching real HOME.
fake_plist = tmp_path / "agent.plist"
fake_log = tmp_path / "log"
monkeypatch.setattr(pm, "LAUNCHD_PLIST", fake_plist)
monkeypatch.setattr(pm, "LOG_PATH", fake_log)
entry = tmp_path / "tagsync.py"
entry.write_text("# placeholder")
result_path = pm.install_schedule(entry, interval_minutes=30)
assert result_path == fake_plist
content = fake_plist.read_text()
assert "StartInterval" in content
assert "1800" in content # 30 min -> 1800 sec
assert str(entry) in content
assert "launchctl" in " ".join(" ".join(c) for c in calls)
def test_uninstall_schedule_when_not_installed(tmp_path, monkeypatch):
import tagsync.platform_macos as pm
monkeypatch.setattr(pm, "LAUNCHD_PLIST", tmp_path / "missing.plist")
assert pm.uninstall_schedule() is False