Files
musicseerr/backend/tests/infrastructure/test_degradation.py
T
2026-04-03 15:53:00 +01:00

127 lines
4.3 KiB
Python

"""Tests for DegradationContext and contextvar lifecycle."""
import asyncio
import pytest
from infrastructure.degradation import (
DegradationContext,
clear_degradation_context,
get_degradation_context,
init_degradation_context,
try_get_degradation_context,
)
from infrastructure.integration_result import IntegrationResult
class TestDegradationContext:
def test_empty_context(self):
ctx = DegradationContext()
assert ctx.summary() == {}
assert ctx.has_degradation() is False
assert ctx.degraded_summary() == {}
def test_record_ok(self):
ctx = DegradationContext()
ctx.record(IntegrationResult.ok(data=[1], source="musicbrainz"))
assert ctx.summary() == {"musicbrainz": "ok"}
assert ctx.has_degradation() is False
def test_record_error(self):
ctx = DegradationContext()
ctx.record(IntegrationResult.error(source="jellyfin", msg="timeout"))
assert ctx.summary() == {"jellyfin": "error"}
assert ctx.has_degradation() is True
assert ctx.degraded_summary() == {"jellyfin": "error"}
def test_record_degraded(self):
ctx = DegradationContext()
ctx.record(
IntegrationResult.degraded(data=[], source="audiodb", msg="rate limit")
)
assert ctx.summary() == {"audiodb": "degraded"}
assert ctx.has_degradation() is True
def test_worst_status_wins(self):
ctx = DegradationContext()
ctx.record(IntegrationResult.ok(data=[], source="musicbrainz"))
ctx.record(IntegrationResult.degraded(data=[], source="musicbrainz", msg="slow"))
assert ctx.summary() == {"musicbrainz": "degraded"}
def test_error_beats_degraded(self):
ctx = DegradationContext()
ctx.record(
IntegrationResult.degraded(data=[], source="musicbrainz", msg="slow")
)
ctx.record(IntegrationResult.error(source="musicbrainz", msg="503"))
assert ctx.summary() == {"musicbrainz": "error"}
def test_cannot_downgrade(self):
ctx = DegradationContext()
ctx.record(IntegrationResult.error(source="jellyfin", msg="down"))
ctx.record(IntegrationResult.ok(data=[1], source="jellyfin"))
assert ctx.summary() == {"jellyfin": "error"}
def test_multiple_sources(self):
ctx = DegradationContext()
ctx.record(IntegrationResult.ok(data=[], source="musicbrainz"))
ctx.record(IntegrationResult.error(source="jellyfin", msg="down"))
ctx.record(
IntegrationResult.degraded(data={}, source="audiodb", msg="slow")
)
assert ctx.summary() == {
"musicbrainz": "ok",
"jellyfin": "error",
"audiodb": "degraded",
}
assert ctx.degraded_summary() == {
"jellyfin": "error",
"audiodb": "degraded",
}
class TestContextVarLifecycle:
def test_no_context_raises(self):
clear_degradation_context()
with pytest.raises(RuntimeError, match="outside a request scope"):
get_degradation_context()
def test_try_get_returns_none_outside(self):
clear_degradation_context()
assert try_get_degradation_context() is None
def test_init_and_get(self):
ctx = init_degradation_context()
assert get_degradation_context() is ctx
clear_degradation_context()
def test_clear_removes_context(self):
init_degradation_context()
clear_degradation_context()
assert try_get_degradation_context() is None
@pytest.mark.asyncio
async def test_isolated_across_tasks(self):
"""Context in one asyncio task must not leak into another."""
results: dict[str, bool] = {}
async def task_a():
init_degradation_context()
ctx = get_degradation_context()
ctx.record(IntegrationResult.error(source="a", msg="fail"))
await asyncio.sleep(0.01)
results["a_has_degradation"] = get_degradation_context().has_degradation()
clear_degradation_context()
async def task_b():
await asyncio.sleep(0.005)
results["b_is_none"] = try_get_degradation_context() is None
await asyncio.gather(task_a(), task_b())
assert results["a_has_degradation"] is True
assert results["b_is_none"] is True