ba199f24bd
- Defer quiet-hours dispatches into new deferred_dispatch table; drain job + periodic catch-up scan re-fire at window end with coalescing on (link, event_type, collection_id). - Add ON DELETE SET NULL migration on event_log_id and partial unique index on (link_id, collection_id, event_type) WHERE status='pending'. - Add release-check provider abstraction (Gitea/GitHub) with SSRF-safe URL validation, settings UI cassette, and scheduled polling. - Replace importlib-only version lookup with version.py helper that prefers the higher of installed metadata vs source pyproject so stale editable dev installs stop misreporting. - Aurora frontend polish: MetaStrip component, ReleaseCassette, EventDetailModal expansion, and i18n additions.
145 lines
5.0 KiB
Python
145 lines
5.0 KiB
Python
"""Tests for the release_check service (interval clamping + status endpoints + persistence)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
def test_parse_interval_hours_clamps_and_defaults() -> None:
|
|
from notify_bridge_server.services.release_check import parse_interval_hours
|
|
|
|
assert parse_interval_hours("12") == 12
|
|
assert parse_interval_hours("") == 12 # default
|
|
assert parse_interval_hours(None) == 12
|
|
assert parse_interval_hours("0") == 1 # clamped to min
|
|
assert parse_interval_hours("9999") == 168 # clamped to max
|
|
assert parse_interval_hours("not-a-number") == 12 # fallback to default
|
|
assert parse_interval_hours("24") == 24
|
|
|
|
|
|
def test_release_endpoint_anonymous_is_rejected(tmp_data_dir) -> None: # noqa: ARG001
|
|
"""GET /api/settings/release requires auth — same as other settings."""
|
|
from notify_bridge_server.main import app
|
|
|
|
with TestClient(app) as client:
|
|
resp = client.get("/api/settings/release")
|
|
# Either 401 (missing token) or 403 (not authenticated) is acceptable.
|
|
assert resp.status_code in (401, 403)
|
|
|
|
|
|
def test_release_force_check_requires_admin(tmp_data_dir) -> None: # noqa: ARG001
|
|
from notify_bridge_server.main import app
|
|
|
|
with TestClient(app) as client:
|
|
resp = client.post("/api/settings/release/check")
|
|
assert resp.status_code in (401, 403)
|
|
|
|
|
|
def test_release_test_requires_admin(tmp_data_dir) -> None: # noqa: ARG001
|
|
from notify_bridge_server.main import app
|
|
|
|
with TestClient(app) as client:
|
|
resp = client.post(
|
|
"/api/settings/release/test",
|
|
json={"provider_kind": "gitea", "provider_url": "https://x.example.com", "provider_repo": "a/b"},
|
|
)
|
|
assert resp.status_code in (401, 403)
|
|
|
|
|
|
# --- Persistence round-trip -------------------------------------------------
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_persist_release_state_round_trip(tmp_data_dir, monkeypatch) -> None: # noqa: ARG001
|
|
"""Write a fake ReleaseInfo, read it back via load_status, assert flags."""
|
|
from notify_bridge_core.release import ReleaseInfo
|
|
from notify_bridge_server.database.engine import init_db
|
|
from notify_bridge_server.services.release_check import (
|
|
load_status,
|
|
persist_release_state,
|
|
)
|
|
|
|
await init_db()
|
|
|
|
info = ReleaseInfo(
|
|
tag="v0.9.0",
|
|
version="0.9.0",
|
|
name="0.9.0 — Aurora",
|
|
body="Release notes",
|
|
url="https://example.com/x/y/releases/tag/v0.9.0",
|
|
published_at="2026-06-01T00:00:00Z",
|
|
prerelease=False,
|
|
draft=False,
|
|
)
|
|
await persist_release_state(
|
|
checked_at="2026-06-01T00:01:00+00:00",
|
|
error=None,
|
|
info=info,
|
|
)
|
|
|
|
# Force the comparator to see an older "current" so update_available
|
|
# comes out True regardless of the actual installed package version.
|
|
monkeypatch.setattr(
|
|
"notify_bridge_server.services.release_check._server_version",
|
|
lambda: "0.7.0",
|
|
)
|
|
status = await load_status()
|
|
assert status.latest == "0.9.0"
|
|
assert status.latest_tag == "v0.9.0"
|
|
assert status.update_available is True
|
|
assert status.error is None
|
|
assert status.latest_body == "Release notes"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_persist_release_state_clears_on_none_info(tmp_data_dir, monkeypatch) -> None: # noqa: ARG001
|
|
"""A persist call with ``info=None`` must blank all the latest-* fields."""
|
|
from notify_bridge_core.release import ReleaseInfo
|
|
from notify_bridge_server.database.engine import init_db
|
|
from notify_bridge_server.services.release_check import (
|
|
load_status,
|
|
persist_release_state,
|
|
)
|
|
|
|
await init_db()
|
|
|
|
# Seed a populated row.
|
|
await persist_release_state(
|
|
checked_at="2026-06-01T00:00:00+00:00",
|
|
error=None,
|
|
info=ReleaseInfo(tag="v9.9.9", version="9.9.9"),
|
|
)
|
|
# Now wipe by passing info=None — mimics the "provider_changed" flow.
|
|
await persist_release_state(
|
|
checked_at="2026-06-01T00:02:00+00:00",
|
|
error="provider_changed",
|
|
info=None,
|
|
)
|
|
monkeypatch.setattr(
|
|
"notify_bridge_server.services.release_check._server_version",
|
|
lambda: "0.7.0",
|
|
)
|
|
status = await load_status()
|
|
assert status.latest is None
|
|
assert status.latest_tag is None
|
|
assert status.update_available is False
|
|
assert status.error == "provider_changed"
|
|
|
|
|
|
# --- Version resolver -------------------------------------------------------
|
|
|
|
|
|
def test_resolve_version_prefers_source_pyproject() -> None:
|
|
"""When pyproject.toml is alongside the source, prefer the higher of (installed, source)."""
|
|
from notify_bridge_server.version import resolve_version
|
|
|
|
v = resolve_version()
|
|
assert v != "0.0.0+unknown"
|
|
# If the editable install is stale (e.g. 0.3.2) but pyproject says 0.7.2,
|
|
# resolve_version must return 0.7.2 (or higher) — the resolver's
|
|
# whole purpose. We test the "not stale" half of the contract here.
|
|
parts = v.split(".")
|
|
assert len(parts) >= 2
|
|
assert parts[0].isdigit()
|