Files
upstream-aw-core/tests/test_dirs.py
upstream-mirror 8757f2fd5e
Some checks failed
Build / Test on ubuntu-latest, py-3.8 (push) Successful in 4m21s
Lint / lint (push) Successful in 25s
Build / Test on macOS-latest, py-3.8 (push) Has been cancelled
Build / Test on windows-latest, py-3.8 (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
snapshot of https://github.com/ActivityWatch/aw-core.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:17:07 +05:00

202 lines
7.6 KiB
Python

"""Tests for profile isolation via AW_PROFILE in aw_core.dirs.
Three profiles (default / testing / research) must yield completely disjoint
directory roots so that an ActivityWatch research build cannot read from or
write to a participant's personal datastore.
The ``testing`` profile additionally follows the new-root-plus-legacy-fallback
rule from ActivityWatch/activitywatch#1399.
"""
import os
from pathlib import Path
from unittest.mock import patch
import pytest
from aw_core.dirs import (
_get_appname,
get_cache_dir,
get_config_dir,
get_data_dir,
legacy_testing_suffix,
using_legacy_testing_root,
)
from . import context # noqa: F401
@pytest.fixture
def fake_dirs(tmp_path, monkeypatch):
"""Point platformdirs at a tmp tree so tests never touch the real home."""
data = tmp_path / "data"
config = tmp_path / "config"
cache = tmp_path / "cache"
def _join(root: Path, appname: str) -> str:
return str(root / appname)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_data_dir",
lambda appname, *a, **k: _join(data, appname),
)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_config_dir",
lambda appname, *a, **k: _join(config, appname),
)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_cache_dir",
lambda appname, *a, **k: _join(cache, appname),
)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_cache_path",
lambda appname, *a, **k: cache / appname,
)
monkeypatch.setattr(
"aw_core.dirs.platformdirs.user_log_dir",
lambda appname, *a, **k: str(cache / appname / "log"),
)
monkeypatch.delenv("AW_PROFILE", raising=False)
return {"data": data, "config": config, "cache": cache, "root": tmp_path}
def _plant_legacy_testing_db(fake_dirs) -> Path:
aw_server = fake_dirs["data"] / "activitywatch" / "aw-server"
aw_server.mkdir(parents=True)
marker = aw_server / "peewee-sqlite-testing.v2.db"
marker.write_text("")
return marker
# ---------------------------------------------------------------------------
# _get_appname
# ---------------------------------------------------------------------------
class TestGetAppname:
def test_unset_returns_bare_name(self, fake_dirs):
"""AW_PROFILE absent → bare 'activitywatch', identical to legacy."""
with patch.dict(os.environ, {"AW_PROFILE": ""}):
assert _get_appname() == "activitywatch"
def test_empty_string_returns_bare_name(self, fake_dirs):
"""AW_PROFILE='' is treated the same as unset."""
with patch.dict(os.environ, {"AW_PROFILE": ""}):
assert _get_appname() == "activitywatch"
def test_testing_profile_fresh_uses_new_root(self, fake_dirs):
with patch.dict(os.environ, {"AW_PROFILE": "testing"}):
assert _get_appname() == "activitywatch-testing"
def test_research_profile(self, fake_dirs):
with patch.dict(os.environ, {"AW_PROFILE": "research"}):
assert _get_appname() == "activitywatch-research"
def test_arbitrary_profile(self, fake_dirs):
with patch.dict(os.environ, {"AW_PROFILE": "myproject"}):
assert _get_appname() == "activitywatch-myproject"
# ---------------------------------------------------------------------------
# testing-root fallback (ActivityWatch/activitywatch#1399)
# ---------------------------------------------------------------------------
class TestTestingRootFallback:
def test_fresh_setup_uses_new_root(self, fake_dirs):
with patch.dict(os.environ, {"AW_PROFILE": "testing"}):
assert using_legacy_testing_root() is False
assert _get_appname() == "activitywatch-testing"
assert legacy_testing_suffix(True) == ""
data = get_data_dir()
assert "activitywatch-testing" in data
assert (fake_dirs["data"] / "activitywatch-testing").is_dir()
def test_legacy_artifacts_keep_shared_root(self, fake_dirs):
_plant_legacy_testing_db(fake_dirs)
with patch.dict(os.environ, {"AW_PROFILE": "testing"}):
assert using_legacy_testing_root() is True
assert _get_appname() == "activitywatch"
assert legacy_testing_suffix(True) == "-testing"
data = get_data_dir()
assert "activitywatch-testing" not in data
assert data.endswith("activitywatch") or data.endswith("activitywatch/")
assert not (fake_dirs["data"] / "activitywatch-testing").exists()
def test_new_root_wins_over_legacy_artifacts(self, fake_dirs):
_plant_legacy_testing_db(fake_dirs)
(fake_dirs["data"] / "activitywatch-testing").mkdir()
with patch.dict(os.environ, {"AW_PROFILE": "testing"}):
assert using_legacy_testing_root() is False
assert _get_appname() == "activitywatch-testing"
assert legacy_testing_suffix(True) == ""
def test_config_testing_toml_is_a_legacy_marker(self, fake_dirs):
cfg = fake_dirs["config"] / "activitywatch"
cfg.mkdir(parents=True)
(cfg / "config-testing.toml").write_text("")
with patch.dict(os.environ, {"AW_PROFILE": "testing"}):
assert using_legacy_testing_root() is True
assert _get_appname() == "activitywatch"
def test_testing_without_aw_profile_keeps_filename_suffix(self, fake_dirs):
"""``testing=True`` with no profile still shares the default root."""
assert legacy_testing_suffix(True) == "-testing"
assert legacy_testing_suffix(False) == ""
def test_named_profile_never_suffixes_filenames(self, fake_dirs):
with patch.dict(os.environ, {"AW_PROFILE": "research"}):
assert legacy_testing_suffix(True) == ""
assert legacy_testing_suffix(False) == ""
# ---------------------------------------------------------------------------
# Directory isolation
# ---------------------------------------------------------------------------
def _data_dir(profile: str) -> str:
"""Return get_data_dir() under the given profile (empty string = default)."""
with patch.dict(os.environ, {"AW_PROFILE": profile}):
return get_data_dir()
class TestDirsIsolation:
"""Three profiles must yield fully disjoint directory roots."""
def test_default_has_no_profile_suffix(self, fake_dirs):
d = _data_dir("")
assert "activitywatch" in d
# The bare appname must not contain a dash after "activitywatch"
assert "activitywatch-" not in d
def test_testing_suffix_present(self, fake_dirs):
assert "activitywatch-testing" in _data_dir("testing")
def test_research_suffix_present(self, fake_dirs):
assert "activitywatch-research" in _data_dir("research")
def test_three_profiles_are_disjoint(self, fake_dirs):
"""default, testing, research all produce distinct, non-nested paths."""
default = _data_dir("")
testing = _data_dir("testing")
research = _data_dir("research")
dirs = {default, testing, research}
assert len(dirs) == 3, f"Profiles are not disjoint: {dirs}"
for a in dirs:
for b in dirs:
if a != b:
assert not a.startswith(b + os.sep), f"{a!r} is a subpath of {b!r}"
def test_config_dir_isolated(self, fake_dirs):
with patch.dict(os.environ, {"AW_PROFILE": "research"}):
cfg = get_config_dir()
assert "activitywatch-research" in cfg
def test_cache_dir_isolated(self, fake_dirs):
with patch.dict(os.environ, {"AW_PROFILE": "testing"}):
cache = get_cache_dir()
assert "activitywatch-testing" in cache