feat: initial Dropbox tag sync tool
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
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
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
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Tests for tagsync.config: Settings persistence + token storage."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_module():
|
||||
from tagsync import config
|
||||
return importlib.reload(config)
|
||||
|
||||
|
||||
# --- Settings ---------------------------------------------------------------
|
||||
|
||||
|
||||
def test_settings_defaults(config_module):
|
||||
s = config_module.Settings()
|
||||
assert s.local_root == ""
|
||||
assert s.platform == ""
|
||||
assert s.skip_online_only is True
|
||||
assert s.write_sidecars is False
|
||||
assert s.schedule_installed is False
|
||||
assert s.schedule_interval_minutes == 60
|
||||
|
||||
|
||||
def test_settings_roundtrip(config_module, tmp_path):
|
||||
path = tmp_path / "cfg.json"
|
||||
original = config_module.Settings(
|
||||
local_root="/home/u/Dropbox",
|
||||
platform="macos",
|
||||
skip_online_only=False,
|
||||
schedule_installed=True,
|
||||
schedule_interval_minutes=30,
|
||||
)
|
||||
original.save(path)
|
||||
|
||||
loaded = config_module.Settings.load(path)
|
||||
assert loaded == original
|
||||
|
||||
|
||||
def test_settings_load_missing_file_returns_defaults(config_module, tmp_path):
|
||||
loaded = config_module.Settings.load(tmp_path / "nope.json")
|
||||
assert loaded == config_module.Settings()
|
||||
|
||||
|
||||
def test_settings_load_ignores_unknown_keys(config_module, tmp_path):
|
||||
"""Older configs should load even if a new field was removed."""
|
||||
path = tmp_path / "cfg.json"
|
||||
path.write_text(json.dumps({
|
||||
"local_root": "/x",
|
||||
"platform": "macos",
|
||||
"future_field_we_dont_know_about": 42,
|
||||
}))
|
||||
loaded = config_module.Settings.load(path)
|
||||
assert loaded.local_root == "/x"
|
||||
assert loaded.platform == "macos"
|
||||
|
||||
|
||||
# --- Token storage (fallback file path) -------------------------------------
|
||||
|
||||
|
||||
def test_token_fallback_roundtrip(config_module, monkeypatch):
|
||||
"""Force the non-keyring fallback path and verify round-trip."""
|
||||
monkeypatch.setattr(config_module, "_keyring_available", lambda: False)
|
||||
|
||||
assert config_module.load_token() is None
|
||||
|
||||
location = config_module.save_token("sl.test-token-123")
|
||||
assert "plaintext" in location
|
||||
|
||||
assert config_module.load_token() == "sl.test-token-123"
|
||||
|
||||
config_module.clear_token()
|
||||
assert config_module.load_token() is None
|
||||
|
||||
|
||||
def test_token_fallback_file_permissions(config_module, monkeypatch):
|
||||
"""The fallback token file should be mode 0600 where supported."""
|
||||
import os
|
||||
import stat
|
||||
|
||||
monkeypatch.setattr(config_module, "_keyring_available", lambda: False)
|
||||
config_module.save_token("sl.perm-test")
|
||||
|
||||
mode = os.stat(config_module.TOKEN_FALLBACK_FILE).st_mode
|
||||
# Skip on Windows where POSIX modes aren't enforced the same way.
|
||||
if os.name == "posix":
|
||||
assert stat.S_IMODE(mode) == 0o600
|
||||
|
||||
|
||||
@pytest.mark.needs_keyring
|
||||
def test_token_keyring_roundtrip(config_module):
|
||||
"""If keyring is installed, verify we actually use it."""
|
||||
pytest.importorskip("keyring")
|
||||
|
||||
if not config_module._keyring_available():
|
||||
pytest.skip("keyring not importable")
|
||||
|
||||
import keyring
|
||||
# Use the in-memory fallback backend to avoid touching real Keychain.
|
||||
from keyring.backends import fail as _fail # noqa: F401 (ensure package)
|
||||
|
||||
try:
|
||||
import keyring.backends.null
|
||||
keyring.set_keyring(keyring.backends.null.Keyring())
|
||||
except Exception:
|
||||
pytest.skip("no safe keyring backend available for tests")
|
||||
|
||||
# Null keyring swallows set/get — just exercise code paths.
|
||||
config_module.save_token("sl.kr-test")
|
||||
config_module.clear_token()
|
||||
Reference in New Issue
Block a user