Files
OwncastSentry/tests/test_owncast_client.py
T
LogalDeveloper 9be932c0c7
Audit / Dependencies (push) Failing after 8s
CD / Build (push) Successful in 8s
CI / Formatting (push) Successful in 6s
CI / Linting (push) Successful in 6s
CI / Tests (push) Successful in 11s
CI / Type Checking (push) Successful in 14s
CI / Spelling (push) Successful in 13s
Replaced health checker with Prometheus metrics service.
2026-03-25 08:54:38 -04:00

281 lines
9.6 KiB
Python

# Copyright 2026 Logan Fick
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for the Owncast HTTP client."""
import json
import logging
from typing import TYPE_CHECKING
import pytest
from aioresponses import aioresponses
from owncastsentry.metrics import MetricsService
from owncastsentry.owncast_client import OwncastClient
from tests.conftest import (
VALID_CONFIG_RESPONSE,
VALID_STATUS_RESPONSE,
generate_metrics_output,
)
if TYPE_CHECKING:
from collections.abc import AsyncIterator
@pytest.fixture
async def owncast_client() -> AsyncIterator[OwncastClient]:
"""Create an OwncastClient and close it after the test."""
client = OwncastClient(
logger=logging.getLogger("test"),
version="0.0.0",
metrics=MetricsService(),
)
yield client
await client.close()
class TestGetStreamState:
"""Stream state retrieval from the status API."""
async def test_returns_state_on_success(
self, owncast_client: OwncastClient
) -> None:
"""Return a StreamState with correct fields on a valid 200 response."""
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/status",
body=json.dumps(VALID_STATUS_RESPONSE).encode(),
)
result = await owncast_client.get_stream_state("stream.logal.dev")
assert result is not None
assert result.domain == "stream.logal.dev"
assert (
result.title
== "I think I can do this... Let's start a nuclear reaction - Playing Nucleares!" # noqa: E501
)
assert result.last_connect_time is None
assert result.last_disconnect_time == "2026-03-04T21:05:32-05:00"
async def test_returns_none_on_missing_field(
self, owncast_client: OwncastClient
) -> None:
"""Return None when the response is missing required fields."""
incomplete = {"streamTitle": "Test Stream", "online": True}
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/status",
body=json.dumps(incomplete).encode(),
)
result = await owncast_client.get_stream_state("stream.logal.dev")
assert result is None
async def test_returns_none_on_invalid_json(
self, owncast_client: OwncastClient
) -> None:
"""Return None when the response body is not valid JSON."""
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/status",
body=b"not json",
)
result = await owncast_client.get_stream_state("stream.logal.dev")
assert result is None
async def test_returns_none_on_non_200(self, owncast_client: OwncastClient) -> None:
"""Return None when the response status is not 200."""
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/status",
status=404,
)
result = await owncast_client.get_stream_state("stream.logal.dev")
assert result is None
async def test_returns_none_on_connection_error(
self, owncast_client: OwncastClient
) -> None:
"""Return None when a connection error occurs."""
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/status",
exception=ConnectionError(),
)
result = await owncast_client.get_stream_state("stream.logal.dev")
assert result is None
class TestGetStreamConfig:
"""Stream configuration retrieval from the config API."""
async def test_returns_config_on_success(
self, owncast_client: OwncastClient
) -> None:
"""Return a StreamConfig with correct fields on a valid 200 response."""
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/config",
body=json.dumps(VALID_CONFIG_RESPONSE).encode(),
)
result = await owncast_client.get_stream_config("stream.logal.dev")
assert result is not None
assert result.name == "LogalDeveloper's Live Stream"
assert result.tags == [
"video games",
"chatting",
"casual",
"english",
"streaming",
"owncast",
"variety",
]
async def test_returns_none_on_invalid_json(
self, owncast_client: OwncastClient
) -> None:
"""Return None when the response body is not valid JSON."""
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/config",
body=b"not json",
)
result = await owncast_client.get_stream_config("stream.logal.dev")
assert result is None
async def test_returns_none_on_non_200(self, owncast_client: OwncastClient) -> None:
"""Return None when the response status is not 200."""
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/config",
status=500,
)
result = await owncast_client.get_stream_config("stream.logal.dev")
assert result is None
async def test_returns_none_on_connection_error(
self, owncast_client: OwncastClient
) -> None:
"""Return None when a connection error occurs."""
with aioresponses() as mocked:
mocked.get(
"https://stream.logal.dev/api/config",
exception=ConnectionError(),
)
result = await owncast_client.get_stream_config("stream.logal.dev")
assert result is None
class TestValidateInstance:
"""Owncast instance validation via the status API."""
async def test_returns_true_for_valid_instance(
self, owncast_client: OwncastClient
) -> None:
"""Return True when the status endpoint returns a valid response."""
with aioresponses() as mocked:
mocked.get(
"https://valid.com/api/status",
body=json.dumps(VALID_STATUS_RESPONSE).encode(),
)
result = await owncast_client.validate_instance("valid.com")
assert result is True
async def test_returns_false_for_invalid_instance(
self, owncast_client: OwncastClient
) -> None:
"""Return False when the status endpoint returns a non-200 response."""
with aioresponses() as mocked:
mocked.get(
"https://invalid.com/api/status",
status=404,
)
result = await owncast_client.validate_instance("invalid.com")
assert result is False
class TestResponseTimeMetrics:
"""Response time histogram recording."""
async def test_records_on_success(self) -> None:
"""Record response time on a successful request."""
metrics = MetricsService()
client = OwncastClient(
logger=logging.getLogger("test"),
version="0.0.0",
metrics=metrics,
)
with aioresponses() as mocked:
mocked.get(
"https://example.com/api/status",
body=json.dumps(VALID_STATUS_RESPONSE).encode(),
)
await client.get_stream_state("example.com")
output = generate_metrics_output(metrics)
assert 'owncastsentry_api_response_seconds{domain="example.com"}' in output
await client.close()
async def test_no_observation_on_failure(self) -> None:
"""Do not record response time when request fails."""
metrics = MetricsService()
client = OwncastClient(
logger=logging.getLogger("test"),
version="0.0.0",
metrics=metrics,
)
with aioresponses() as mocked:
mocked.get(
"https://example.com/api/status",
status=500,
)
await client.get_stream_state("example.com")
output = generate_metrics_output(metrics)
assert 'owncastsentry_api_response_seconds{domain="example.com"}' not in output
await client.close()
async def test_no_observation_on_connection_error(self) -> None:
"""Do not record response time on connection error."""
metrics = MetricsService()
client = OwncastClient(
logger=logging.getLogger("test"),
version="0.0.0",
metrics=metrics,
)
with aioresponses() as mocked:
mocked.get(
"https://example.com/api/status",
exception=ConnectionError(),
)
await client.get_stream_state("example.com")
output = generate_metrics_output(metrics)
assert 'owncastsentry_api_response_seconds{domain="example.com"}' not in output
await client.close()
class TestOpenConnectionCount:
"""Open connection count."""
async def test_zero_with_no_requests(self, owncast_client: OwncastClient) -> None:
"""Return zero when no requests have been made."""
assert owncast_client.open_connection_count == 0