"""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()