feat: add tests, update configs, fix state.py return, update gitignore

This commit is contained in:
2026-06-11 23:00:34 +02:00
parent 64d078f457
commit 0f390ff1e1
12 changed files with 305 additions and 15 deletions
+68
View File
@@ -0,0 +1,68 @@
import os
import sys
import pytest
import tempfile
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
@pytest.fixture
def ics_sample():
"""Return a sample ICS string with 2 events."""
return """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test
BEGIN:VEVENT
UID:evt-001@test.com
DTSTAMP:20240101T000000Z
DTSTART:20240101T100000Z
DTEND:20240101T110000Z
SUMMARY:Event One
END:VEVENT
BEGIN:VEVENT
UID:evt-002@test.com
DTSTAMP:20240101T000000Z
DTSTART:20240102T100000Z
DTEND:20240102T110000Z
SUMMARY:Event Two
END:VEVENT
END:VCALENDAR"""
@pytest.fixture
def ics_sample_modified():
"""Return ICS with one modified event and one new event."""
return """BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Test
BEGIN:VEVENT
UID:evt-001@test.com
DTSTAMP:20240101T000000Z
DTSTART:20240101T100000Z
DTEND:20240101T120000Z
SUMMARY:Event One Modified
END:VEVENT
BEGIN:VEVENT
UID:evt-003@test.com
DTSTAMP:20240101T000000Z
DTSTART:20240103T100000Z
DTEND:20240103T110000Z
SUMMARY:Event Three
END:VEVENT
END:VCALENDAR"""
@pytest.fixture
def tmp_db():
"""Return a temp database path."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
path = f.name
yield path
if os.path.exists(path):
os.unlink(path)
@pytest.fixture(autouse=True)
def env_vars(monkeypatch):
"""Set up required env vars for config tests."""
monkeypatch.setenv("ICS_URL", "https://example.com/cal.ics")
monkeypatch.setenv("BAIKAL_URL", "https://baikal.com/dav.php/calendars/user/cal/")
monkeypatch.setenv("BAIKAL_USER", "user")
monkeypatch.setenv("BAIKAL_PASS", "pass")
monkeypatch.setenv("SYNC_FREQUENCY", "5")
+49
View File
@@ -0,0 +1,49 @@
import os
import pytest
from config import validate, HEADERS, Config
class TestValidate:
def test_valid_config(self, env_vars):
cfg = validate()
assert isinstance(cfg, Config)
assert cfg.ics_url == "https://example.com/cal.ics"
assert cfg.baikal_url == "https://baikal.com/dav.php/calendars/user/cal/"
assert cfg.baikal_user == "user"
assert cfg.baikal_pass == "pass"
assert cfg.sync_frequency == 5
def test_default_frequency(self, monkeypatch, env_vars):
monkeypatch.delenv("SYNC_FREQUENCY", raising=False)
cfg = validate()
assert cfg.sync_frequency == 5
def test_missing_ics_url(self, monkeypatch, env_vars):
monkeypatch.delenv("ICS_URL", raising=False)
with pytest.raises(ValueError, match="ICS_URL"):
validate()
def test_missing_all_required(self, monkeypatch, env_vars):
for var in ["ICS_URL", "BAIKAL_URL", "BAIKAL_USER", "BAIKAL_PASS"]:
monkeypatch.delenv(var, raising=False)
with pytest.raises(ValueError):
validate()
def test_invalid_frequency(self, monkeypatch, env_vars):
monkeypatch.setenv("SYNC_FREQUENCY", "-1")
with pytest.raises(ValueError, match="SYNC_FREQUENCY"):
validate()
def test_frequency_zero(self, monkeypatch, env_vars):
monkeypatch.setenv("SYNC_FREQUENCY", "0")
with pytest.raises(ValueError, match="SYNC_FREQUENCY"):
validate()
def test_frequency_string(self, monkeypatch, env_vars):
monkeypatch.setenv("SYNC_FREQUENCY", "abc")
with pytest.raises(ValueError, match="SYNC_FREQUENCY"):
validate()
class TestHeaders:
def test_headers_exist(self):
assert "User-Agent" in HEADERS
assert "Unraid-Sync" in HEADERS["User-Agent"]
+73
View File
@@ -0,0 +1,73 @@
from diff import parse_ics_events, compute_diff, parse_ics_events_with_data
class TestParseIcsEvents:
def test_parse_basic(self, ics_sample):
result = parse_ics_events(ics_sample)
assert "evt-001@test.com" in result
assert "evt-002@test.com" in result
assert len(result) == 2
def test_parse_no_uid(self):
ics = """BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTAMP:20240101T000000Z
DTSTART:20240101T100000Z
SUMMARY:No UID
END:VEVENT
END:VCALENDAR"""
result = parse_ics_events(ics)
assert len(result) == 0
def test_hashes_are_sha256(self, ics_sample):
result = parse_ics_events(ics_sample)
for uid, h in result.items():
assert len(h) == 64
int(h, 16)
def test_same_event_same_hash(self, ics_sample):
r1 = parse_ics_events(ics_sample)
r2 = parse_ics_events(ics_sample)
for uid in r1:
assert r1[uid] == r2[uid]
def test_modified_event_different_hash(self, ics_sample, ics_sample_modified):
r1 = parse_ics_events(ics_sample)
r2 = parse_ics_events(ics_sample_modified)
assert r1["evt-001@test.com"] != r2["evt-001@test.com"]
class TestComputeDiff:
def test_no_changes(self, ics_sample):
ics_uids = parse_ics_events(ics_sample)
deltas = compute_diff(ics_uids, ics_uids)
assert len(deltas["to_add"]) == 0
assert len(deltas["to_update"]) == 0
assert len(deltas["to_delete"]) == 0
def test_add_events(self, ics_sample, ics_sample_modified):
original = parse_ics_events(ics_sample)
modified = parse_ics_events(ics_sample_modified)
deltas = compute_diff(modified, original)
assert ("evt-003@test.com", modified["evt-003@test.com"]) in deltas["to_add"]
def test_delete_events(self, ics_sample, ics_sample_modified):
original = parse_ics_events(ics_sample)
modified = parse_ics_events(ics_sample_modified)
deltas = compute_diff(modified, original)
assert "evt-002@test.com" in deltas["to_delete"]
def test_update_events(self, ics_sample, ics_sample_modified):
original = parse_ics_events(ics_sample)
modified = parse_ics_events(ics_sample_modified)
deltas = compute_diff(modified, original)
uid, new_hash = deltas["to_update"][0]
assert uid == "evt-001@test.com"
assert new_hash == modified["evt-001@test.com"]
class TestParseIcsEventsWithData:
def test_returns_bytes(self, ics_sample):
result = parse_ics_events_with_data(ics_sample)
assert len(result) == 2
for uid, data in result.items():
assert isinstance(data, bytes)
assert b"VEVENT" in data
+71
View File
@@ -0,0 +1,71 @@
from state import SyncState
class TestSyncState:
def test_create_tables(self, tmp_db):
state = SyncState(tmp_db)
assert True
state.close()
def test_upsert_and_get(self, tmp_db):
state = SyncState(tmp_db)
state.upsert_event("uid-1", "hash-abc")
assert state.get_event_hash("uid-1") == "hash-abc"
state.close()
def test_get_uids(self, tmp_db):
state = SyncState(tmp_db)
state.upsert_event("uid-1", "h1")
state.upsert_event("uid-2", "h2")
uids = state.get_event_uids()
assert "uid-1" in uids
assert "uid-2" in uids
assert len(uids) == 2
state.close()
def test_delete_event(self, tmp_db):
state = SyncState(tmp_db)
state.upsert_event("uid-1", "h1")
state.delete_event("uid-1")
assert state.get_event_hash("uid-1") is None
state.close()
def test_clear_events(self, tmp_db):
state = SyncState(tmp_db)
state.upsert_event("uid-1", "h1")
state.upsert_event("uid-2", "h2")
count = state.clear_events()
assert count == 2
assert len(state.get_event_uids()) == 0
state.close()
def test_ics_cache(self, tmp_db):
state = SyncState(tmp_db)
state.set_ics_cache("hash-xyz", "etag-123")
h, e, _ = state.get_ics_cache()
assert h == "hash-xyz"
assert e == "etag-123"
state.clear_ics_cache()
h, e, _ = state.get_ics_cache()
assert h is None
assert e is None
state.close()
def test_snapshot_and_restore(self, tmp_db):
state = SyncState(tmp_db)
state.upsert_event("uid-1", "h1")
state.upsert_event("uid-2", "h2")
snap = state.snapshot()
assert "uid-1" in snap["uids"]
state.clear_events()
assert len(state.get_event_uids()) == 0
state.restore_snapshot(snap)
assert len(state.get_event_uids()) == 2
assert state.get_event_hash("uid-1") == "h1"
state.close()
def test_empty_state(self, tmp_db):
state = SyncState(tmp_db)
assert len(state.get_event_uids()) == 0
h, e, _ = state.get_ics_cache()
assert h is None
state.close()