feat: auto-refresh calendar subscriptions
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
import socket
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
@@ -9,6 +11,7 @@ from backend.calendar import (
|
||||
parse_ics_events,
|
||||
validate_calendar_url,
|
||||
)
|
||||
from backend.calendar_refresh import refresh_due_subscriptions, refresh_subscription_cache
|
||||
|
||||
ICS = b"""BEGIN:VCALENDAR\r
|
||||
VERSION:2.0\r
|
||||
@@ -234,3 +237,117 @@ def test_subscription_ownership_is_strict(client, monkeypatch):
|
||||
client.post("/api/v1/auth/logout")
|
||||
assert client.patch(f"/api/v1/calendar-subscriptions/{sub['id']}", json={"name": "x"}).status_code == 401
|
||||
assert client.delete(f"/api/v1/calendar-subscriptions/{sub['id']}").status_code == 401
|
||||
|
||||
|
||||
def test_due_enabled_subscriptions_refresh_automatically(client, monkeypatch):
|
||||
client = initialized(client)
|
||||
monkeypatch.setattr(
|
||||
"backend.calendar.validate_calendar_url",
|
||||
lambda url: (url, "93.184.216.34", 443),
|
||||
)
|
||||
calls = []
|
||||
|
||||
def fetch(url, *, etag=None, last_modified=None):
|
||||
calls.append(url)
|
||||
return FetchResult(ICS, None, None, False)
|
||||
|
||||
monkeypatch.setattr("backend.calendar.fetch_calendar", fetch)
|
||||
client.post(
|
||||
"/api/v1/calendar-subscriptions",
|
||||
json={"name": "enabled", "url": "https://example.com/enabled.ics"},
|
||||
)
|
||||
client.post(
|
||||
"/api/v1/calendar-subscriptions",
|
||||
json={"name": "disabled", "url": "https://example.com/disabled.ics", "enabled": False},
|
||||
)
|
||||
|
||||
async def refresh():
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
from backend.db import get_engine
|
||||
|
||||
factory = async_sessionmaker(get_engine(), expire_on_commit=False)
|
||||
async with factory() as db:
|
||||
return await refresh_due_subscriptions(
|
||||
db,
|
||||
now=datetime.now(UTC) + timedelta(minutes=16),
|
||||
refresh_interval=timedelta(minutes=15),
|
||||
)
|
||||
|
||||
assert asyncio.run(refresh()) == 1
|
||||
assert calls == [
|
||||
"https://example.com/enabled.ics",
|
||||
"https://example.com/disabled.ics",
|
||||
"https://example.com/enabled.ics",
|
||||
]
|
||||
|
||||
|
||||
def test_failed_cached_refresh_preserves_last_success_timestamp(monkeypatch):
|
||||
original_refresh = datetime(2026, 9, 20, tzinfo=UTC)
|
||||
row = SimpleNamespace(
|
||||
id="source-1",
|
||||
url="https://example.com/work.ics",
|
||||
name="work",
|
||||
color="#123456",
|
||||
ics_cache=ICS.decode(),
|
||||
etag=None,
|
||||
last_modified=None,
|
||||
refreshed_at=original_refresh,
|
||||
last_error=None,
|
||||
)
|
||||
|
||||
class FakeDb:
|
||||
async def refresh(self, _row):
|
||||
pass
|
||||
|
||||
async def commit(self):
|
||||
pass
|
||||
|
||||
async def rollback(self):
|
||||
pass
|
||||
|
||||
def fail(*args, **kwargs):
|
||||
raise HTTPException(502, "upstream down")
|
||||
|
||||
monkeypatch.setattr("backend.calendar.fetch_calendar", fail)
|
||||
assert asyncio.run(refresh_subscription_cache(FakeDb(), row)) is False
|
||||
assert row.refreshed_at == original_refresh
|
||||
assert row.last_error == "upstream down"
|
||||
|
||||
|
||||
def test_refresh_discards_response_when_url_changes_in_flight(monkeypatch):
|
||||
row = SimpleNamespace(
|
||||
id="source-1",
|
||||
url="https://example.com/old.ics",
|
||||
name="work",
|
||||
color="#123456",
|
||||
ics_cache="old cache",
|
||||
etag=None,
|
||||
last_modified=None,
|
||||
refreshed_at=datetime(2026, 9, 20, tzinfo=UTC),
|
||||
last_error=None,
|
||||
)
|
||||
|
||||
class FakeDb:
|
||||
committed = False
|
||||
|
||||
async def refresh(self, target):
|
||||
target.url = "https://example.com/new.ics"
|
||||
target.ics_cache = None
|
||||
target.refreshed_at = None
|
||||
|
||||
async def commit(self):
|
||||
self.committed = True
|
||||
|
||||
async def rollback(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(
|
||||
"backend.calendar.fetch_calendar",
|
||||
lambda *args, **kwargs: FetchResult(ICS, None, None, False),
|
||||
)
|
||||
db = FakeDb()
|
||||
assert asyncio.run(refresh_subscription_cache(db, row)) is False
|
||||
assert row.url == "https://example.com/new.ics"
|
||||
assert row.ics_cache is None
|
||||
assert db.committed is False
|
||||
|
||||
Reference in New Issue
Block a user