snapshot of https://github.com/ActivityWatch/aw-client.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / Test on ubuntu-latest (push) Successful in 2m4s
Examples / Test examples (ubuntu-latest, 3.10) (push) Successful in 2m16s
Lint / lint (push) Successful in 1m25s
Build / Test on macOS-latest (push) Has been cancelled
Build / Test on windows-latest (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
Some checks failed
Build / Test on ubuntu-latest (push) Successful in 2m4s
Examples / Test examples (ubuntu-latest, 3.10) (push) Successful in 2m16s
Lint / lint (push) Successful in 1m25s
Build / Test on macOS-latest (push) Has been cancelled
Build / Test on windows-latest (push) Has been cancelled
CodeQL / Analyze (python) (push) Failing after 1s
This commit is contained in:
12
tests/conftest.py
Normal file
12
tests/conftest.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_aw_profile_after_test():
|
||||
"""export_profile() writes os.environ directly; monkeypatch.delenv does
|
||||
not record an undo when the var was already unset, so later tests would
|
||||
inherit a leftover profile."""
|
||||
yield
|
||||
os.environ.pop("AW_PROFILE", None)
|
||||
104
tests/test_auth.py
Normal file
104
tests/test_auth.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from aw_client import ActivityWatchClient
|
||||
from aw_client.config import load_local_server_api_key
|
||||
from aw_client import client as client_module
|
||||
|
||||
|
||||
class DummyResponse:
|
||||
def __init__(self, data: Optional[Dict[str, Any]] = None):
|
||||
self._data = data or {}
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
return None
|
||||
|
||||
def json(self) -> Dict[str, Any]:
|
||||
return self._data
|
||||
|
||||
|
||||
def write_server_config(tmp_path, filename: str, content: str) -> None:
|
||||
config_dir = tmp_path / "activitywatch" / "aw-server-rust"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
(config_dir / filename).write_text(content)
|
||||
|
||||
|
||||
def test_load_local_server_api_key_matches_port(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"aw_client.config._user_config_dir",
|
||||
lambda appname: str(tmp_path / appname),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"aw_client.config._user_data_dir",
|
||||
lambda appname: str(tmp_path / "data" / appname),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"aw_client.config._user_cache_dir",
|
||||
lambda appname: str(tmp_path / "cache" / appname),
|
||||
)
|
||||
write_server_config(
|
||||
tmp_path,
|
||||
"config.toml",
|
||||
'port = 5601\n\n[auth]\napi_key = "secret123"\n',
|
||||
)
|
||||
write_server_config(
|
||||
tmp_path,
|
||||
"config-testing.toml",
|
||||
'port = 5666\n\n[auth]\napi_key = "testing-secret"\n',
|
||||
)
|
||||
|
||||
assert load_local_server_api_key("127.0.0.1", 5601) == "secret123"
|
||||
assert (
|
||||
load_local_server_api_key("localhost", "5666", profile="testing")
|
||||
== "testing-secret"
|
||||
)
|
||||
assert load_local_server_api_key("::1", 5601) == "secret123"
|
||||
assert load_local_server_api_key("127.0.0.1", 5600) is None
|
||||
assert load_local_server_api_key("example.com", 5601) is None
|
||||
# Default profile must not pick up the testing server's key.
|
||||
assert load_local_server_api_key("localhost", "5666") is None
|
||||
|
||||
|
||||
def test_client_sends_authorization_header_for_local_server(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
||||
write_server_config(
|
||||
tmp_path,
|
||||
"config.toml",
|
||||
'port = 5600\n\n[auth]\napi_key = "secret123"\n',
|
||||
)
|
||||
monkeypatch.setattr(client_module, "SingleInstance", lambda name: object())
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_get(url, params=None, headers=None):
|
||||
captured["url"] = url
|
||||
captured["headers"] = headers
|
||||
return DummyResponse({"hostname": "test-host", "testing": False})
|
||||
|
||||
monkeypatch.setattr(client_module.req, "get", fake_get)
|
||||
|
||||
client = ActivityWatchClient("test-client", host="127.0.0.1", port=5600)
|
||||
assert client.get_info()["hostname"] == "test-host"
|
||||
assert captured["url"] == "http://127.0.0.1:5600/api/0/info"
|
||||
assert captured["headers"]["Authorization"] == "Bearer secret123"
|
||||
|
||||
|
||||
def test_client_skips_authorization_header_for_remote_server(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
|
||||
write_server_config(
|
||||
tmp_path,
|
||||
"config.toml",
|
||||
'port = 5600\n\n[auth]\napi_key = "secret123"\n',
|
||||
)
|
||||
monkeypatch.setattr(client_module, "SingleInstance", lambda name: object())
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_get(url, params=None, headers=None):
|
||||
captured["headers"] = headers
|
||||
return DummyResponse({"hostname": "remote-host", "testing": False})
|
||||
|
||||
monkeypatch.setattr(client_module.req, "get", fake_get)
|
||||
|
||||
client = ActivityWatchClient("test-client", host="aw.example.com", port=5600)
|
||||
assert client.get_info()["hostname"] == "remote-host"
|
||||
assert "Authorization" not in captured["headers"]
|
||||
130
tests/test_client.py
Executable file
130
tests/test_client.py
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
import warnings
|
||||
from random import random
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
import pytest
|
||||
|
||||
from aw_core.models import Event
|
||||
from aw_client import ActivityWatchClient
|
||||
|
||||
|
||||
def create_unique_event():
|
||||
return Event(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
duration=timedelta(),
|
||||
data={"label": str(random())},
|
||||
)
|
||||
|
||||
|
||||
def test_full():
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
client_name = "aw-test-client"
|
||||
bucket_name = "test-bucket"
|
||||
bucket_etype = "test"
|
||||
|
||||
# Test context manager
|
||||
with ActivityWatchClient(client_name, testing=True) as client:
|
||||
time.sleep(1)
|
||||
|
||||
# Check that client name is set correctly
|
||||
assert client.client_name == client_name
|
||||
|
||||
# Delete bucket before creating it, and handle error if it doesn't already exist
|
||||
try:
|
||||
client.delete_bucket(bucket_name)
|
||||
except HTTPError:
|
||||
pass
|
||||
|
||||
# Create bucket
|
||||
client.create_bucket(bucket_name, bucket_etype)
|
||||
|
||||
# Check bucket
|
||||
buckets = client.get_buckets()
|
||||
assert bucket_name in buckets
|
||||
assert bucket_name == buckets[bucket_name]["id"]
|
||||
assert bucket_etype == buckets[bucket_name]["type"]
|
||||
|
||||
# Insert events
|
||||
e1 = create_unique_event()
|
||||
e2 = create_unique_event()
|
||||
e3 = create_unique_event()
|
||||
events = [e1, e2, e3]
|
||||
client.insert_events(bucket_name, events)
|
||||
|
||||
# Get events
|
||||
fetched_events = client.get_events(bucket_name, limit=len(events))
|
||||
|
||||
# Assert events
|
||||
assert [(e.timestamp, e.duration, e.data) for e in fetched_events] == [
|
||||
(e.timestamp, e.duration, e.data) for e in events
|
||||
]
|
||||
|
||||
# Check eventcount
|
||||
eventcount = client.get_eventcount(bucket_name)
|
||||
assert eventcount == len(events)
|
||||
|
||||
result = client.query(
|
||||
f'RETURN = query_bucket("{bucket_name}");',
|
||||
timeperiods=[(now - timedelta(hours=1), now + timedelta(hours=1))],
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert len(result[0]) == 3
|
||||
|
||||
# Get single event
|
||||
e = client.get_event(bucket_name, fetched_events[1].id)
|
||||
assert e.id == fetched_events[1].id
|
||||
|
||||
# Delete single event
|
||||
client.delete_event(bucket_name, fetched_events[1].id)
|
||||
assert client.get_event(bucket_name, fetched_events[1].id) is None
|
||||
|
||||
# Test exception raising
|
||||
with pytest.raises(ValueError):
|
||||
# timeperiod end time does not have a timezone set
|
||||
result = client.query(
|
||||
f'RETURN = query_bucket("{bucket_name}");',
|
||||
timeperiods=[(now - timedelta(hours=1), datetime.now())],
|
||||
)
|
||||
|
||||
# Create and delete an event: check that it no longer exists
|
||||
e_del = create_unique_event()
|
||||
client.insert_event(bucket_name, e_del)
|
||||
fetched_events = client.get_events(bucket_name)
|
||||
assert (e_del.timestamp, e_del.duration, e_del.data) in [
|
||||
(e.timestamp, e.duration, e.data) for e in fetched_events
|
||||
]
|
||||
|
||||
e_del_fetched = [e for e in fetched_events if e.data == e_del.data][0]
|
||||
client.delete_event(bucket_name, e_del_fetched)
|
||||
fetched_events = client.get_events(bucket_name)
|
||||
assert (e_del.timestamp, e_del.duration, e_del.data) not in [
|
||||
(e.timestamp, e.duration, e.data) for e in fetched_events
|
||||
]
|
||||
|
||||
# Delete bucket
|
||||
client.delete_bucket(bucket_name)
|
||||
|
||||
|
||||
def test_queued_usage_warns_once_before_connect():
|
||||
client = ActivityWatchClient(f"aw-test-client-{random()}", testing=True)
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
client.create_bucket("test-bucket", "test", queued=True)
|
||||
client.heartbeat(
|
||||
"test-bucket",
|
||||
create_unique_event(),
|
||||
pulsetime=1,
|
||||
queued=True,
|
||||
)
|
||||
|
||||
queue_warnings = [
|
||||
warning
|
||||
for warning in caught
|
||||
if "connect()" in str(warning.message) or "with client:" in str(warning.message)
|
||||
]
|
||||
assert len(queue_warnings) == 1
|
||||
56
tests/test_failqueue.py
Executable file
56
tests/test_failqueue.py
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
import time
|
||||
from pprint import pprint
|
||||
from random import randint
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from aw_core.models import Event
|
||||
from aw_client import ActivityWatchClient
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def create_unique_event():
|
||||
return Event(timestamp=now, data={"label": str(randint(0, 10000))})
|
||||
|
||||
|
||||
def test_failqueue():
|
||||
client_name = "aw-test-client-" + str(randint(0, 10000))
|
||||
bucket_id = "test-bucket-" + str(randint(0, 10000))
|
||||
bucket_etype = "test"
|
||||
|
||||
input("Make sure aw-server isn't running, then press enter > ")
|
||||
client1 = ActivityWatchClient(client_name, testing=True)
|
||||
client1.create_bucket(bucket_id, bucket_etype, queued=True)
|
||||
|
||||
print("Creating events")
|
||||
events = [create_unique_event() for _ in range(3)]
|
||||
for i, e in enumerate(events):
|
||||
e.timestamp += timedelta(seconds=i)
|
||||
client1.heartbeat(bucket_id, e, pulsetime=1, queued=True)
|
||||
|
||||
print("Trying to send events (should fail)")
|
||||
with client1:
|
||||
time.sleep(1)
|
||||
|
||||
input("Start aw-server with --testing, then press enter > ")
|
||||
|
||||
client2 = ActivityWatchClient(client_name, testing=True)
|
||||
client2.create_bucket(bucket_id, bucket_etype, queued=True)
|
||||
|
||||
# Here the previously queued events should be sent
|
||||
with client2:
|
||||
time.sleep(1)
|
||||
|
||||
print("Getting events")
|
||||
recv_events = client2.get_events(bucket_id)
|
||||
|
||||
print("Asserting latest event")
|
||||
pprint(recv_events)
|
||||
pprint(recv_events[0].data["label"])
|
||||
pprint(events[2].data["label"])
|
||||
assert recv_events[0].data["label"] == events[2].data["label"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_failqueue()
|
||||
94
tests/test_profile.py
Normal file
94
tests/test_profile.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Unit tests for profile resolution."""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from aw_client.profile import (
|
||||
DEFAULT_PROFILE,
|
||||
TESTING_PROFILE,
|
||||
export_profile,
|
||||
is_testing,
|
||||
profile_from_env,
|
||||
profile_suffix,
|
||||
resolve_profile,
|
||||
validate_profile,
|
||||
)
|
||||
|
||||
|
||||
class TestResolveProfile:
|
||||
def test_no_flags_gives_default(self):
|
||||
assert resolve_profile(None, False) == DEFAULT_PROFILE
|
||||
|
||||
def test_testing_flag_is_an_alias(self):
|
||||
assert resolve_profile(None, True) == TESTING_PROFILE
|
||||
|
||||
def test_explicit_profile_wins(self):
|
||||
assert resolve_profile("research", False) == "research"
|
||||
|
||||
def test_testing_with_matching_profile_is_allowed(self):
|
||||
assert resolve_profile("testing", True) == TESTING_PROFILE
|
||||
|
||||
def test_testing_with_conflicting_profile_raises(self):
|
||||
with pytest.raises(ValueError, match="conflicts"):
|
||||
resolve_profile("research", True)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name", ["Research", "with space", "a" * 33, "", "../escape", "-leading"]
|
||||
)
|
||||
def test_invalid_names_rejected(self, name):
|
||||
with pytest.raises(ValueError):
|
||||
validate_profile(name)
|
||||
|
||||
@pytest.mark.parametrize("name", ["research", "aw2", "my_profile", "my-profile"])
|
||||
def test_valid_names_accepted(self, name):
|
||||
assert validate_profile(name) == name
|
||||
|
||||
|
||||
class TestProfileSuffix:
|
||||
def test_default_has_no_suffix(self):
|
||||
assert profile_suffix(DEFAULT_PROFILE) == ""
|
||||
|
||||
def test_testing_keeps_legacy_suffix(self):
|
||||
assert profile_suffix(TESTING_PROFILE) == "-testing"
|
||||
|
||||
def test_custom_profile_suffix(self):
|
||||
assert profile_suffix("research") == "-research"
|
||||
|
||||
def test_suffixes_are_disjoint(self):
|
||||
suffixes = {profile_suffix(p) for p in ("default", "testing", "research")}
|
||||
assert len(suffixes) == 3
|
||||
|
||||
|
||||
class TestIsTesting:
|
||||
def test_only_testing_profile_is_testing(self):
|
||||
assert is_testing(TESTING_PROFILE)
|
||||
assert not is_testing(DEFAULT_PROFILE)
|
||||
assert not is_testing("research")
|
||||
|
||||
|
||||
class TestExportProfile:
|
||||
def test_exported_for_child_processes(self, monkeypatch):
|
||||
monkeypatch.delenv("AW_PROFILE", raising=False)
|
||||
export_profile("research")
|
||||
assert os.environ["AW_PROFILE"] == "research"
|
||||
|
||||
def test_default_unsets_env_so_aw_core_keeps_bare_root(self, monkeypatch):
|
||||
monkeypatch.setenv("AW_PROFILE", "research")
|
||||
export_profile(DEFAULT_PROFILE)
|
||||
assert "AW_PROFILE" not in os.environ
|
||||
|
||||
|
||||
class TestProfileFromEnv:
|
||||
def test_defaults_when_unset(self, monkeypatch):
|
||||
monkeypatch.delenv("AW_PROFILE", raising=False)
|
||||
assert profile_from_env(False) == DEFAULT_PROFILE
|
||||
assert profile_from_env(True) == TESTING_PROFILE
|
||||
|
||||
def test_reads_exported_profile(self, monkeypatch):
|
||||
monkeypatch.setenv("AW_PROFILE", "research")
|
||||
assert profile_from_env(False) == "research"
|
||||
|
||||
def test_ignores_invalid_env_value(self, monkeypatch):
|
||||
monkeypatch.setenv("AW_PROFILE", "Not A Profile")
|
||||
assert profile_from_env(False) == DEFAULT_PROFILE
|
||||
294
tests/test_profile_config.py
Normal file
294
tests/test_profile_config.py
Normal file
@@ -0,0 +1,294 @@
|
||||
"""Client constructor, config sections, persistqueue, and rust api-key lookup."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from aw_client import ActivityWatchClient
|
||||
from aw_client import client as client_module
|
||||
from aw_client.config import load_local_server_api_key, rust_server_config_candidates
|
||||
from aw_client.profile import DEFAULT_PROFILE, TESTING_PROFILE
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_dirs(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
|
||||
monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "data"))
|
||||
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / "cache"))
|
||||
monkeypatch.delenv("AW_PROFILE", raising=False)
|
||||
monkeypatch.setattr(client_module, "SingleInstance", lambda name: object())
|
||||
return tmp_path
|
||||
|
||||
|
||||
class TestClientConstructor:
|
||||
def test_testing_alias_uses_testing_port_and_exports_env(self, isolated_dirs):
|
||||
client = ActivityWatchClient("t", testing=True)
|
||||
assert client.profile == TESTING_PROFILE
|
||||
assert client.testing is True
|
||||
assert client.server_address.endswith(":5666")
|
||||
assert os.environ["AW_PROFILE"] == "testing"
|
||||
|
||||
def test_explicit_profile_testing_is_the_same(self, isolated_dirs):
|
||||
client = ActivityWatchClient("t", profile="testing")
|
||||
assert client.profile == TESTING_PROFILE
|
||||
assert client.testing is True
|
||||
assert client.server_address.endswith(":5666")
|
||||
|
||||
def test_default_unsets_env_and_uses_5600(self, isolated_dirs, monkeypatch):
|
||||
monkeypatch.setenv("AW_PROFILE", "research")
|
||||
client = ActivityWatchClient("t", profile="default")
|
||||
assert client.profile == DEFAULT_PROFILE
|
||||
assert client.testing is False
|
||||
assert client.server_address.endswith(":5600")
|
||||
assert "AW_PROFILE" not in os.environ
|
||||
|
||||
def test_named_profile_falls_back_to_server_section(self, isolated_dirs, caplog):
|
||||
with caplog.at_level(logging.WARNING, logger="aw_client.client"):
|
||||
client = ActivityWatchClient("t", profile="research")
|
||||
assert client.profile == "research"
|
||||
assert client.testing is False
|
||||
assert client.server_address.endswith(":5600")
|
||||
assert os.environ["AW_PROFILE"] == "research"
|
||||
assert any(
|
||||
"falling back to [server]" in rec.message
|
||||
and "may collide with the default instance" in rec.message
|
||||
for rec in caplog.records
|
||||
)
|
||||
|
||||
def test_env_from_launcher_is_used_when_no_flags(self, isolated_dirs, monkeypatch):
|
||||
monkeypatch.setenv("AW_PROFILE", "research")
|
||||
client = ActivityWatchClient("t")
|
||||
assert client.profile == "research"
|
||||
assert os.environ["AW_PROFILE"] == "research"
|
||||
|
||||
def test_explicit_testing_overrides_stale_env(self, isolated_dirs, monkeypatch):
|
||||
monkeypatch.setenv("AW_PROFILE", "research")
|
||||
client = ActivityWatchClient("t", testing=True)
|
||||
assert client.profile == TESTING_PROFILE
|
||||
assert os.environ["AW_PROFILE"] == "testing"
|
||||
|
||||
def test_host_and_port_kwargs_still_win(self, isolated_dirs):
|
||||
client = ActivityWatchClient(
|
||||
"t", profile="research", host="127.0.0.1", port=5667
|
||||
)
|
||||
assert client.server_address == "http://127.0.0.1:5667"
|
||||
|
||||
def test_conflicting_flags_raise(self, isolated_dirs):
|
||||
with pytest.raises(ValueError, match="conflicts"):
|
||||
ActivityWatchClient("t", testing=True, profile="research")
|
||||
|
||||
|
||||
class TestPersistqueueSuffix:
|
||||
def test_testing_keeps_legacy_suffix(self, isolated_dirs):
|
||||
client = ActivityWatchClient("aw-test-client", testing=True)
|
||||
assert "aw-test-client-testing." in client.request_queue.persistqueue_path
|
||||
|
||||
def test_named_profile_suffix_is_disjoint(self, isolated_dirs):
|
||||
testing = ActivityWatchClient("aw-test-client", testing=True)
|
||||
research = ActivityWatchClient("aw-test-client", profile="research")
|
||||
assert (
|
||||
testing.request_queue.persistqueue_path
|
||||
!= research.request_queue.persistqueue_path
|
||||
)
|
||||
assert "aw-test-client-research." in research.request_queue.persistqueue_path
|
||||
|
||||
def test_reconnect_preserves_named_profile_queue_path(
|
||||
self, isolated_dirs, monkeypatch
|
||||
):
|
||||
def profile_data_dir(module):
|
||||
profile = os.environ.get("AW_PROFILE")
|
||||
root = "activitywatch" if profile is None else f"activitywatch-{profile}"
|
||||
return str(isolated_dirs / root / module)
|
||||
|
||||
monkeypatch.setattr(client_module, "get_data_dir", profile_data_dir)
|
||||
research = ActivityWatchClient("aw-test-client", profile="research")
|
||||
original_path = research.request_queue.persistqueue_path
|
||||
monkeypatch.setattr(research.request_queue, "_try_connect", lambda: True)
|
||||
research.request_queue.connected = True
|
||||
research.connect()
|
||||
|
||||
default = ActivityWatchClient("other-client", profile="default")
|
||||
assert default.request_queue.persistqueue_path != original_path
|
||||
research.disconnect()
|
||||
|
||||
assert research.request_queue.persistqueue_path == original_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_platform_dirs(tmp_path, monkeypatch):
|
||||
"""Point rust-config lookup at a tmp tree. platformdirs on macOS/Windows
|
||||
ignores XDG_* even when set, so patch the wrappers rather than env vars.
|
||||
"""
|
||||
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_client.config._user_data_dir", lambda appname: _join(data, appname)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"aw_client.config._user_config_dir",
|
||||
lambda appname: _join(config, appname),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"aw_client.config._user_cache_dir",
|
||||
lambda appname: _join(cache, appname),
|
||||
)
|
||||
monkeypatch.delenv("AW_PROFILE", raising=False)
|
||||
return {"data": data, "config": config, "cache": cache, "root": tmp_path}
|
||||
|
||||
|
||||
def _write_rust_config(
|
||||
config_root: Path, appname: str, filename: str, content: str
|
||||
) -> Path:
|
||||
rust_dir = config_root / appname / "aw-server-rust"
|
||||
rust_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = rust_dir / filename
|
||||
path.write_text(content)
|
||||
return path
|
||||
|
||||
|
||||
def test_load_local_server_api_key_named_profile(fake_platform_dirs):
|
||||
config = fake_platform_dirs["config"]
|
||||
_write_rust_config(
|
||||
config,
|
||||
"activitywatch-research",
|
||||
"config.toml",
|
||||
'port = 5667\n\n[auth]\napi_key = "research-secret"\n',
|
||||
)
|
||||
_write_rust_config(
|
||||
config,
|
||||
"activitywatch",
|
||||
"config.toml",
|
||||
'port = 5600\n\n[auth]\napi_key = "default-secret"\n',
|
||||
)
|
||||
# Suffixed names in the isolated root must not be read — dir isolation
|
||||
# is the point (ActivityWatch/activitywatch#1399).
|
||||
_write_rust_config(
|
||||
config,
|
||||
"activitywatch-research",
|
||||
"config-research.toml",
|
||||
'port = 5667\n\n[auth]\napi_key = "suffixed-must-be-ignored"\n',
|
||||
)
|
||||
assert (
|
||||
load_local_server_api_key("127.0.0.1", 5667, profile="research")
|
||||
== "research-secret"
|
||||
)
|
||||
assert load_local_server_api_key("127.0.0.1", 5600, profile="default") == (
|
||||
"default-secret"
|
||||
)
|
||||
assert load_local_server_api_key("127.0.0.1", 5600, profile="research") is None
|
||||
|
||||
|
||||
class TestTestingRootApiKeyLookup:
|
||||
"""Rust API-key lookup follows the #1399 testing-root rule."""
|
||||
|
||||
def test_fresh_setup_reads_bare_config_in_new_root(self, fake_platform_dirs):
|
||||
_write_rust_config(
|
||||
fake_platform_dirs["config"],
|
||||
"activitywatch-testing",
|
||||
"config.toml",
|
||||
'port = 5666\n\n[auth]\napi_key = "new-root-secret"\n',
|
||||
)
|
||||
assert (
|
||||
load_local_server_api_key("127.0.0.1", 5666, profile="testing")
|
||||
== "new-root-secret"
|
||||
)
|
||||
paths = [p for p, _ in rust_server_config_candidates("testing")]
|
||||
assert paths[0].endswith(
|
||||
os.path.join("activitywatch-testing", "aw-server-rust", "config.toml")
|
||||
)
|
||||
|
||||
def test_legacy_artifacts_keep_suffixed_shared_root(self, fake_platform_dirs):
|
||||
_write_rust_config(
|
||||
fake_platform_dirs["config"],
|
||||
"activitywatch",
|
||||
"config-testing.toml",
|
||||
'port = 5666\n\n[auth]\napi_key = "legacy-secret"\n',
|
||||
)
|
||||
assert (
|
||||
load_local_server_api_key("127.0.0.1", 5666, profile="testing")
|
||||
== "legacy-secret"
|
||||
)
|
||||
paths = [p for p, _ in rust_server_config_candidates("testing")]
|
||||
assert paths[0].endswith(
|
||||
os.path.join("activitywatch", "aw-server-rust", "config-testing.toml")
|
||||
)
|
||||
|
||||
def test_new_root_wins_over_legacy_artifacts(self, fake_platform_dirs):
|
||||
_write_rust_config(
|
||||
fake_platform_dirs["config"],
|
||||
"activitywatch",
|
||||
"config-testing.toml",
|
||||
'port = 5666\n\n[auth]\napi_key = "legacy-secret"\n',
|
||||
)
|
||||
_write_rust_config(
|
||||
fake_platform_dirs["config"],
|
||||
"activitywatch-testing",
|
||||
"config.toml",
|
||||
'port = 5666\n\n[auth]\napi_key = "new-root-secret"\n',
|
||||
)
|
||||
assert (
|
||||
load_local_server_api_key("127.0.0.1", 5666, profile="testing")
|
||||
== "new-root-secret"
|
||||
)
|
||||
|
||||
def test_empty_new_root_still_finds_legacy_key(self, fake_platform_dirs):
|
||||
"""Python creating activitywatch-testing/ must not hide rust's legacy key."""
|
||||
(fake_platform_dirs["config"] / "activitywatch-testing").mkdir(parents=True)
|
||||
_write_rust_config(
|
||||
fake_platform_dirs["config"],
|
||||
"activitywatch",
|
||||
"config-testing.toml",
|
||||
'port = 5666\n\n[auth]\napi_key = "legacy-secret"\n',
|
||||
)
|
||||
assert (
|
||||
load_local_server_api_key("127.0.0.1", 5666, profile="testing")
|
||||
== "legacy-secret"
|
||||
)
|
||||
|
||||
|
||||
class TestCliPortOverride:
|
||||
def test_explicit_port_5600_is_not_discarded(self, monkeypatch):
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, **kwargs):
|
||||
captured.update(kwargs)
|
||||
|
||||
def get_buckets(self):
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr("aw_client.cli.aw_client.ActivityWatchClient", FakeClient)
|
||||
from click.testing import CliRunner
|
||||
|
||||
from aw_client.cli import main
|
||||
|
||||
result = CliRunner().invoke(main, ["--port", "5600", "buckets"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert captured["port"] == 5600
|
||||
|
||||
def test_omitted_port_lets_profile_config_win(self, monkeypatch):
|
||||
captured = {}
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, **kwargs):
|
||||
captured.update(kwargs)
|
||||
|
||||
def get_buckets(self):
|
||||
return {}
|
||||
|
||||
monkeypatch.setattr("aw_client.cli.aw_client.ActivityWatchClient", FakeClient)
|
||||
from click.testing import CliRunner
|
||||
|
||||
from aw_client.cli import main
|
||||
|
||||
result = CliRunner().invoke(main, ["--testing", "buckets"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert captured["port"] is None
|
||||
assert captured["testing"] is True
|
||||
90
tests/test_requestqueue.py
Normal file
90
tests/test_requestqueue.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Tests the often-buggy request queue.
|
||||
|
||||
WARNING: A shitload of mocking ahead
|
||||
|
||||
It is said about testing that it makes you able to refactorize
|
||||
with confidence, and I need some of that right now.
|
||||
"""
|
||||
|
||||
from time import sleep
|
||||
from logging import basicConfig, DEBUG
|
||||
|
||||
basicConfig(level=DEBUG)
|
||||
|
||||
import requests
|
||||
from aw_client.client import RequestQueue
|
||||
|
||||
|
||||
class MockClient:
|
||||
client_name = "Mock"
|
||||
|
||||
def __init__(self):
|
||||
self.testing = True
|
||||
self.create_bucket_calls = []
|
||||
|
||||
def get_buckets(self, *args, **kwargs):
|
||||
print("Called get_buckets")
|
||||
return [{"id": "test", "name": "Test"}]
|
||||
|
||||
def create_bucket(self, *args, **kwargs):
|
||||
self.create_bucket_calls.append((args, kwargs))
|
||||
print("Called create_bucket")
|
||||
|
||||
def _post(self, *args, **kwargs):
|
||||
print(args, kwargs)
|
||||
return requests.Response()
|
||||
|
||||
|
||||
def test_basic():
|
||||
client = MockClient()
|
||||
rq = RequestQueue(client) # type: ignore
|
||||
|
||||
# Mockeypatching
|
||||
rq._try_connect = lambda: True # type: ignore
|
||||
rq.connected = True
|
||||
|
||||
rq.start()
|
||||
rq.add_request("/api/0/buckets/test/heartbeat", {})
|
||||
sleep(1)
|
||||
rq.stop()
|
||||
rq.join()
|
||||
|
||||
|
||||
def test_complex():
|
||||
client = MockClient()
|
||||
rq = RequestQueue(client) # type: ignore
|
||||
|
||||
# Mockeypatching
|
||||
rq._try_connect = lambda: False # type: ignore
|
||||
|
||||
rq.start()
|
||||
sleep(1)
|
||||
rq.stop()
|
||||
rq.join()
|
||||
|
||||
|
||||
def test_register_bucket_creates_immediately_when_connected():
|
||||
client = MockClient()
|
||||
rq = RequestQueue(client) # type: ignore
|
||||
rq.connected = True
|
||||
|
||||
rq.register_bucket("test-bucket", "test-type")
|
||||
|
||||
assert client.create_bucket_calls == [(("test-bucket", "test-type"), {})]
|
||||
|
||||
|
||||
def test_register_bucket_marks_queue_disconnected_on_create_failure():
|
||||
class FailingClient(MockClient):
|
||||
def create_bucket(self, *args, **kwargs):
|
||||
super().create_bucket(*args, **kwargs)
|
||||
raise requests.exceptions.ConnectionError()
|
||||
|
||||
client = FailingClient()
|
||||
rq = RequestQueue(client) # type: ignore
|
||||
rq.connected = True
|
||||
|
||||
rq.register_bucket("test-bucket", "test-type")
|
||||
|
||||
assert rq.connected is False
|
||||
assert client.create_bucket_calls == [(("test-bucket", "test-type"), {})]
|
||||
Reference in New Issue
Block a user