Release / release (push) Successful in 53s
- open_store_readonly(): stat() PermissionError (non-group user on the 2750 store dir) now maps to StoreFault so status/TUI degrade instead of crashing with a traceback. - init_store(): chmod db + -wal/-shm group rw after WAL setup — SQLite WAL readers need write access to sidecars even for mode=ro opens. - Store dir 2750 → 2770 (tmpfiles + make install) and UMask=002 on the collect unit so root-created files stay group-accessible. - rpm %post upgrade path re-runs systemd-tmpfiles --create to correct placement modes on existing machines. Bump to 0.3.3.
80 lines
3.0 KiB
Python
80 lines
3.0 KiB
Python
"""Group access to the observation store — regression coverage for issue #54.
|
|
|
|
Two defects: (1) a non-group user's stat() on the store directory raised
|
|
PermissionError straight through open_store_readonly(), crashing status/TUI
|
|
instead of degrading to the Store fault view; (2) even group members could
|
|
not open the WAL-mode store because root-created sidecars lacked group write
|
|
and the store directory lacked group execute-then-write.
|
|
"""
|
|
import sqlite3
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from fenris.store import DEFAULT_STORE_PATH, init_store
|
|
from fenris.status import StoreFault, open_store_readonly
|
|
|
|
|
|
def test_stat_permission_error_becomes_store_fault(monkeypatch, tmp_path):
|
|
"""stat() denied (non-group user on a 2750 dir) → StoreFault, not crash."""
|
|
store = tmp_path / "observations.db"
|
|
store.write_bytes(b"")
|
|
|
|
import pathlib
|
|
|
|
def denied(self, follow_symlinks=True):
|
|
raise PermissionError(13, "Permission denied")
|
|
|
|
monkeypatch.setattr(pathlib.Path, "exists", denied)
|
|
with pytest.raises(StoreFault):
|
|
open_store_readonly(store)
|
|
|
|
|
|
def test_connect_failure_becomes_store_fault(tmp_path):
|
|
"""sqlite failures stay wrapped as StoreFault (existing contract)."""
|
|
garbage = tmp_path / "observations.db"
|
|
garbage.write_bytes(b"not a database" * 100)
|
|
with pytest.raises(StoreFault):
|
|
open_store_readonly(garbage)
|
|
|
|
|
|
def test_init_store_leaves_files_group_writable(tmp_path):
|
|
"""Root-created stores must stay readable by WAL readers: db and sidecars
|
|
need group write after init_store (issue #54)."""
|
|
store = tmp_path / "observations.db"
|
|
conn = init_store(store)
|
|
try:
|
|
assert (store.stat().st_mode & 0o060) == 0o060, "db not group rw"
|
|
wal = store.with_name(store.name + "-wal")
|
|
shm = store.with_name(store.name + "-shm")
|
|
if wal.exists():
|
|
assert (wal.stat().st_mode & 0o060) == 0o060, "wal not group rw"
|
|
if shm.exists():
|
|
assert (shm.stat().st_mode & 0o060) == 0o060, "shm not group rw"
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_readonly_open_works_after_init_store(tmp_path):
|
|
"""The shipped read path opens a store created by init_store."""
|
|
store = tmp_path / "observations.db"
|
|
writer = init_store(store)
|
|
writer.execute("INSERT INTO monitoring_periods (started_at) VALUES ('2026-01-01T00:00:00+00:00')")
|
|
writer.commit()
|
|
conn = open_store_readonly(store)
|
|
assert conn is not None
|
|
conn.close()
|
|
writer.close()
|
|
|
|
|
|
def test_packaging_ships_group_access():
|
|
"""tmpfiles must create the store dir group-writable; collect unit must
|
|
keep the umask loose so root-created sidecars stay group-accessible."""
|
|
repo = Path(__file__).resolve().parent.parent
|
|
assert "2770" in (repo / "packaging" / "tmpfiles.d" / "fenris.conf").read_text()
|
|
assert "2750" not in (repo / "packaging" / "tmpfiles.d" / "fenris.conf").read_text()
|
|
assert "UMask=002" in (repo / "units" / "fenris-collect.service").read_text()
|