ab5560a623
- BUG 1: Replace isinstance(active_remote_id, int) guard with _lookup_remote_by_device_id() so bell-clear fires correctly when active_remote_id is a UUID string (new format). Old code silently skipped the POST for all non-integer active_remote_id values. Regression test: test_poll_cycle_fires_federation_bell_clear_for_remote_session_with_uuid - GAP 4: Add _resolveActiveView(activeView, views) helper in app.js. Falls back to 'all' when active_view references a deleted/unknown view name. Ready for Phase 2 view-switching code to call at read time. - ISSUE 6: _createDeviceSelect now uses remotes[i].device_id || String(i) for option values instead of always String(i), so session creation routes receive correct device_id. - ISSUE 9: identity.json written with indent=2 + trailing newline, consistent with settings.json and state.json formatting conventions. - ISSUE 11: Fix state.py module docstring — 'tmux-web muxplex' → 'muxplex'. - ISSUE 12: Settings panel hidden_sessions checkboxes now use s.sessionKey || s.name as the checkbox value (and checked state) so remote sessions are stored in device_id:name format, consistent with getVisibleSessions() lookups.
176 lines
6.2 KiB
Python
176 lines
6.2 KiB
Python
"""
|
|
Tests for muxplex/identity.py — Device Identity Module.
|
|
|
|
Each muxplex instance gets a persistent device_id (UUID v4) stored in
|
|
~/.config/muxplex/identity.json (outside federation sync boundary).
|
|
"""
|
|
|
|
import json
|
|
import uuid
|
|
|
|
from muxplex.identity import load_device_id, reset_device_id
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _is_valid_uuid4(value: str) -> bool:
|
|
"""Return True if value is a valid UUID v4 string."""
|
|
try:
|
|
parsed = uuid.UUID(value)
|
|
return parsed.version == 4
|
|
except (ValueError, AttributeError):
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# load_device_id tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_load_creates_file_when_absent(tmp_path, monkeypatch):
|
|
"""load_device_id() creates identity.json when it does not exist."""
|
|
identity_path = tmp_path / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
device_id = load_device_id()
|
|
|
|
assert identity_path.exists(), "identity.json should be created"
|
|
assert _is_valid_uuid4(device_id), f"Expected UUID v4, got: {device_id}"
|
|
data = json.loads(identity_path.read_text())
|
|
assert data["device_id"] == device_id
|
|
|
|
|
|
def test_load_returns_same_id_on_repeated_calls(tmp_path, monkeypatch):
|
|
"""load_device_id() returns the same ID on repeated calls (idempotent)."""
|
|
identity_path = tmp_path / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
id_first = load_device_id()
|
|
id_second = load_device_id()
|
|
id_third = load_device_id()
|
|
|
|
assert id_first == id_second == id_third, "Repeated calls must return the same ID"
|
|
|
|
|
|
def test_load_reads_existing_file(tmp_path, monkeypatch):
|
|
"""load_device_id() reads the existing device_id from identity.json."""
|
|
identity_path = tmp_path / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
known_id = str(uuid.uuid4())
|
|
identity_path.write_text(json.dumps({"device_id": known_id}))
|
|
|
|
result = load_device_id()
|
|
|
|
assert result == known_id, "Should return the pre-existing device_id"
|
|
|
|
|
|
def test_load_creates_parent_dirs(tmp_path, monkeypatch):
|
|
"""load_device_id() creates parent directories if they do not exist."""
|
|
identity_path = tmp_path / "nested" / "dirs" / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
device_id = load_device_id()
|
|
|
|
assert identity_path.exists(), "identity.json should be created in nested dirs"
|
|
assert _is_valid_uuid4(device_id)
|
|
|
|
|
|
def test_load_regenerates_on_corrupt_json(tmp_path, monkeypatch):
|
|
"""load_device_id() regenerates device_id when identity.json contains corrupt JSON."""
|
|
identity_path = tmp_path / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
identity_path.write_text("this is not valid json {{{")
|
|
|
|
device_id = load_device_id()
|
|
|
|
assert _is_valid_uuid4(device_id), (
|
|
f"Expected UUID v4 after regeneration, got: {device_id}"
|
|
)
|
|
data = json.loads(identity_path.read_text())
|
|
assert data["device_id"] == device_id, (
|
|
"File should be rewritten with the new device_id"
|
|
)
|
|
|
|
|
|
def test_load_regenerates_on_missing_key(tmp_path, monkeypatch):
|
|
"""load_device_id() regenerates device_id when identity.json lacks the device_id key."""
|
|
identity_path = tmp_path / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
identity_path.write_text(json.dumps({"other_key": "some_value"}))
|
|
|
|
device_id = load_device_id()
|
|
|
|
assert _is_valid_uuid4(device_id), (
|
|
f"Expected UUID v4 after regeneration, got: {device_id}"
|
|
)
|
|
data = json.loads(identity_path.read_text())
|
|
assert data["device_id"] == device_id, (
|
|
"File should be rewritten with the new device_id"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# reset_device_id tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_reset_generates_new_id(tmp_path, monkeypatch):
|
|
"""reset_device_id() generates a new device_id different from the existing one."""
|
|
identity_path = tmp_path / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
# Establish an initial ID
|
|
original_id = load_device_id()
|
|
|
|
# Reset should produce a new, different ID
|
|
new_id = reset_device_id()
|
|
|
|
assert _is_valid_uuid4(new_id), f"Expected UUID v4 from reset, got: {new_id}"
|
|
assert new_id != original_id, (
|
|
"reset_device_id() should return a different ID than the original"
|
|
)
|
|
|
|
# Verify the file now contains the new ID
|
|
data = json.loads(identity_path.read_text())
|
|
assert data["device_id"] == new_id, "File should contain the newly reset device_id"
|
|
|
|
|
|
def test_reset_creates_parent_dirs(tmp_path, monkeypatch):
|
|
"""reset_device_id() creates parent directories if they do not exist."""
|
|
identity_path = tmp_path / "no" / "parent" / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
new_id = reset_device_id()
|
|
|
|
assert identity_path.exists(), "identity.json should be created in new directories"
|
|
assert _is_valid_uuid4(new_id)
|
|
data = json.loads(identity_path.read_text())
|
|
assert data["device_id"] == new_id
|
|
|
|
|
|
def test_identity_json_is_human_readable(tmp_path, monkeypatch):
|
|
"""identity.json must be written with indentation for human readability.
|
|
|
|
Consistent with settings.py and state.py which both write indented JSON.
|
|
"""
|
|
identity_path = tmp_path / "identity.json"
|
|
monkeypatch.setattr("muxplex.identity.IDENTITY_PATH", identity_path)
|
|
|
|
load_device_id()
|
|
|
|
raw = identity_path.read_text()
|
|
# Human-readable JSON has newlines; compact JSON does not
|
|
assert "\n" in raw, (
|
|
"identity.json must be written with indent=2 for human readability "
|
|
"(consistent with settings.json and state.json formatting)"
|
|
)
|
|
# Must still be valid JSON
|
|
data = json.loads(raw)
|
|
assert "device_id" in data
|