Enabled all Ruff lint rules and resolved findings.
This commit is contained in:
+8
-4
@@ -33,6 +33,10 @@ if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
from pathlib import Path
|
||||
|
||||
from maubot import Plugin
|
||||
from mautrix.util.async_db import UpgradeTable
|
||||
from mautrix.util.config import BaseProxyConfig
|
||||
|
||||
from owncastsentry.metrics import MetricsService
|
||||
from owncastsentry.types import StreamConfig, StreamState
|
||||
|
||||
@@ -72,19 +76,19 @@ def subscription_repo(database: Database) -> SubscriptionRepository:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def maubot_plugin_class():
|
||||
def maubot_plugin_class() -> type[Plugin]:
|
||||
"""Use OwncastSentry as the plugin class for maubot integration tests."""
|
||||
return OwncastSentry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def maubot_plugin_config_class():
|
||||
def maubot_plugin_config_class() -> type[BaseProxyConfig]:
|
||||
"""Use our Config class for maubot integration tests."""
|
||||
return Config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def maubot_upgrade_table():
|
||||
def maubot_upgrade_table() -> UpgradeTable:
|
||||
"""Provide the database upgrade table for maubot integration tests."""
|
||||
return get_upgrade_table()
|
||||
|
||||
@@ -220,7 +224,7 @@ class _StubOwncastClient:
|
||||
self.queried_domains.append(domain)
|
||||
return self.stream_state
|
||||
|
||||
async def get_stream_config(self, domain: str) -> StreamConfig | None:
|
||||
async def get_stream_config(self, _domain: str) -> StreamConfig | None:
|
||||
"""Return the configured stream config."""
|
||||
self.config_call_count += 1
|
||||
return self.stream_config
|
||||
|
||||
+46
-29
@@ -16,12 +16,16 @@
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
import time_machine
|
||||
from aioresponses import aioresponses
|
||||
|
||||
from owncastsentry.commands import (
|
||||
_SECONDS_PER_DAY,
|
||||
_SECONDS_PER_HOUR,
|
||||
_SECONDS_PER_MINUTE,
|
||||
_escape_markdown,
|
||||
_format_duration,
|
||||
_sanitize_for_markdown,
|
||||
@@ -30,6 +34,11 @@ from owncastsentry.owncast_client import _OWNCAST_STATUS_PATH
|
||||
from owncastsentry.types import UNKNOWN_STATUS_THRESHOLD, StreamState
|
||||
from tests.conftest import VALID_STATUS_RESPONSE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from maubot.testing.bot import TestBot
|
||||
|
||||
from owncastsentry import OwncastSentry
|
||||
|
||||
|
||||
class TestEscapeMarkdown:
|
||||
"""Markdown special character escaping."""
|
||||
@@ -78,12 +87,12 @@ class TestFormatDuration:
|
||||
[
|
||||
pytest.param(1, "1 second", id="singular-second"),
|
||||
pytest.param(30, "30 seconds", id="plural-seconds"),
|
||||
pytest.param(60, "1 minute", id="singular-minute"),
|
||||
pytest.param(120, "2 minutes", id="plural-minutes"),
|
||||
pytest.param(3600, "1 hour", id="singular-hour"),
|
||||
pytest.param(7200, "2 hours", id="plural-hours"),
|
||||
pytest.param(86400, "1 day", id="singular-day"),
|
||||
pytest.param(172800, "2 days", id="plural-days"),
|
||||
pytest.param(_SECONDS_PER_MINUTE, "1 minute", id="singular-minute"),
|
||||
pytest.param(2 * _SECONDS_PER_MINUTE, "2 minutes", id="plural-minutes"),
|
||||
pytest.param(_SECONDS_PER_HOUR, "1 hour", id="singular-hour"),
|
||||
pytest.param(2 * _SECONDS_PER_HOUR, "2 hours", id="plural-hours"),
|
||||
pytest.param(_SECONDS_PER_DAY, "1 day", id="singular-day"),
|
||||
pytest.param(2 * _SECONDS_PER_DAY, "2 days", id="plural-days"),
|
||||
],
|
||||
)
|
||||
def test_formats_duration(self, seconds_ago: int, expected: str) -> None:
|
||||
@@ -106,10 +115,11 @@ class TestFormatDuration:
|
||||
assert _format_duration(timestamp, self._NOW) == "unknown duration"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("maubot_plugin")
|
||||
class TestSubscribeCommand:
|
||||
"""Subscribe command end-to-end via maubot."""
|
||||
|
||||
async def test_subscribe_valid_stream(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_subscribe_valid_stream(self, maubot_test_bot: TestBot) -> None:
|
||||
"""Subscribe to a valid Owncast stream."""
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
with aioresponses() as mocked:
|
||||
@@ -125,9 +135,7 @@ class TestSubscribeCommand:
|
||||
"stream.logal.dev goes live."
|
||||
)
|
||||
|
||||
async def test_subscribe_invalid_stream(
|
||||
self, maubot_test_bot, maubot_plugin
|
||||
) -> None:
|
||||
async def test_subscribe_invalid_stream(self, maubot_test_bot: TestBot) -> None:
|
||||
"""Reject subscription to an invalid Owncast instance."""
|
||||
status_url = f"https://invalid.com{_OWNCAST_STATUS_PATH}"
|
||||
with aioresponses() as mocked:
|
||||
@@ -142,9 +150,7 @@ class TestSubscribeCommand:
|
||||
"instance is offline."
|
||||
)
|
||||
|
||||
async def test_subscribe_already_subscribed(
|
||||
self, maubot_test_bot, maubot_plugin
|
||||
) -> None:
|
||||
async def test_subscribe_already_subscribed(self, maubot_test_bot: TestBot) -> None:
|
||||
"""Reject duplicate subscription in the same room."""
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
with aioresponses() as mocked:
|
||||
@@ -163,7 +169,7 @@ class TestSubscribeCommand:
|
||||
)
|
||||
|
||||
async def test_subscribe_existing_stream_new_room(
|
||||
self, maubot_test_bot, maubot_plugin
|
||||
self, maubot_test_bot: TestBot
|
||||
) -> None:
|
||||
"""Skip instance validation when subscribing from a new room."""
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
@@ -189,10 +195,11 @@ class TestSubscribeCommand:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("maubot_plugin")
|
||||
class TestUnsubscribeCommand:
|
||||
"""Unsubscribe command end-to-end via maubot."""
|
||||
|
||||
async def test_unsubscribe_existing(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_unsubscribe_existing(self, maubot_test_bot: TestBot) -> None:
|
||||
"""Unsubscribe from a subscribed stream."""
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
with aioresponses() as mocked:
|
||||
@@ -210,9 +217,7 @@ class TestUnsubscribeCommand:
|
||||
"longer receive notifications for stream.logal.dev."
|
||||
)
|
||||
|
||||
async def test_unsubscribe_not_subscribed(
|
||||
self, maubot_test_bot, maubot_plugin
|
||||
) -> None:
|
||||
async def test_unsubscribe_not_subscribed(self, maubot_test_bot: TestBot) -> None:
|
||||
"""Reject unsubscribe from a non-subscribed stream."""
|
||||
await maubot_test_bot.send("!unsubscribe unknown.com")
|
||||
|
||||
@@ -222,10 +227,11 @@ class TestUnsubscribeCommand:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("maubot_plugin")
|
||||
class TestSubscriptionsCommand:
|
||||
"""Subscriptions listing command end-to-end via maubot."""
|
||||
|
||||
async def test_no_subscriptions(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_no_subscriptions(self, maubot_test_bot: TestBot) -> None:
|
||||
"""Show help text when no subscriptions exist."""
|
||||
await maubot_test_bot.send("!subscriptions")
|
||||
|
||||
@@ -237,7 +243,9 @@ class TestSubscriptionsCommand:
|
||||
)
|
||||
|
||||
@time_machine.travel(datetime(2026, 3, 13, 12, 0, 0, tzinfo=UTC))
|
||||
async def test_shows_online_stream(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_shows_online_stream(
|
||||
self, maubot_test_bot: TestBot, maubot_plugin: OwncastSentry
|
||||
) -> None:
|
||||
"""Show stream details including title and duration."""
|
||||
# Subscribe first
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
@@ -273,7 +281,7 @@ class TestSubscriptionsCommand:
|
||||
|
||||
@time_machine.travel(datetime(2026, 3, 13, 12, 0, 0, tzinfo=UTC))
|
||||
async def test_escapes_markdown_in_stream_name_and_title(
|
||||
self, maubot_test_bot, maubot_plugin
|
||||
self, maubot_test_bot: TestBot, maubot_plugin: OwncastSentry
|
||||
) -> None:
|
||||
"""Render stream name and title as literal text in command output."""
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
@@ -307,7 +315,9 @@ class TestSubscriptionsCommand:
|
||||
assert "Title: `code` > quote #tag" in content.formatted_body
|
||||
|
||||
@time_machine.travel(datetime(2026, 3, 13, 12, 0, 0, tzinfo=UTC))
|
||||
async def test_shows_offline_stream(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_shows_offline_stream(
|
||||
self, maubot_test_bot: TestBot, maubot_plugin: OwncastSentry
|
||||
) -> None:
|
||||
"""Show offline status for non-live streams."""
|
||||
# Subscribe first
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
@@ -340,7 +350,7 @@ class TestSubscriptionsCommand:
|
||||
)
|
||||
|
||||
async def test_shows_offline_stream_without_disconnect_time(
|
||||
self, maubot_test_bot, maubot_plugin
|
||||
self, maubot_test_bot: TestBot
|
||||
) -> None:
|
||||
"""Show offline status without duration before first poll completes."""
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
@@ -364,7 +374,9 @@ class TestSubscriptionsCommand:
|
||||
"instances, use `!unsubscribe <domain>`"
|
||||
)
|
||||
|
||||
async def test_shows_unknown_stream(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_shows_unknown_stream(
|
||||
self, maubot_test_bot: TestBot, maubot_plugin: OwncastSentry
|
||||
) -> None:
|
||||
"""Show unknown status when instance has been unreachable."""
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
with aioresponses() as mocked:
|
||||
@@ -394,7 +406,7 @@ class TestSubscriptionsCommand:
|
||||
|
||||
@time_machine.travel(datetime(2026, 3, 13, 12, 0, 0, tzinfo=UTC))
|
||||
async def test_shows_multiple_subscriptions(
|
||||
self, maubot_test_bot, maubot_plugin
|
||||
self, maubot_test_bot: TestBot, maubot_plugin: OwncastSentry
|
||||
) -> None:
|
||||
"""List subscriptions alphabetically with mixed statuses."""
|
||||
# Subscribe in reverse alphabetical order to verify sorted output
|
||||
@@ -446,10 +458,11 @@ class TestSubscriptionsCommand:
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("maubot_plugin")
|
||||
class TestLiveCommand:
|
||||
"""Live streams listing command end-to-end via maubot."""
|
||||
|
||||
async def test_no_subscriptions(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_no_subscriptions(self, maubot_test_bot: TestBot) -> None:
|
||||
"""Show help text when no subscriptions exist."""
|
||||
await maubot_test_bot.send("!live")
|
||||
|
||||
@@ -460,7 +473,9 @@ class TestLiveCommand:
|
||||
"instance, use `!subscribe <domain>`"
|
||||
)
|
||||
|
||||
async def test_no_live_streams(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_no_live_streams(
|
||||
self, maubot_test_bot: TestBot, maubot_plugin: OwncastSentry
|
||||
) -> None:
|
||||
"""Show 'no live' message when all streams are offline."""
|
||||
# Subscribe first
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
@@ -490,7 +505,9 @@ class TestLiveCommand:
|
||||
)
|
||||
|
||||
@time_machine.travel(datetime(2026, 3, 13, 12, 0, 0, tzinfo=UTC))
|
||||
async def test_shows_live_stream(self, maubot_test_bot, maubot_plugin) -> None:
|
||||
async def test_shows_live_stream(
|
||||
self, maubot_test_bot: TestBot, maubot_plugin: OwncastSentry
|
||||
) -> None:
|
||||
"""Show live stream with title and duration."""
|
||||
# Subscribe first
|
||||
status_url = f"https://stream.logal.dev{_OWNCAST_STATUS_PATH}"
|
||||
@@ -524,7 +541,7 @@ class TestLiveCommand:
|
||||
|
||||
@time_machine.travel(datetime(2026, 3, 13, 12, 0, 0, tzinfo=UTC))
|
||||
async def test_shows_multiple_live_streams(
|
||||
self, maubot_test_bot, maubot_plugin
|
||||
self, maubot_test_bot: TestBot, maubot_plugin: OwncastSentry
|
||||
) -> None:
|
||||
"""List live streams alphabetically with different durations."""
|
||||
# Subscribe in reverse alphabetical order to verify sorted output
|
||||
|
||||
@@ -256,6 +256,7 @@ class TestFormatMessage:
|
||||
title: str,
|
||||
domain: str,
|
||||
tags: list[str],
|
||||
*,
|
||||
title_change: bool,
|
||||
expected: str,
|
||||
subscription_repo: SubscriptionRepository,
|
||||
@@ -264,7 +265,9 @@ class TestFormatMessage:
|
||||
service = _make_service(
|
||||
client=_StubMatrixClient(), subscription_repo=subscription_repo
|
||||
)
|
||||
result = service._format_message(name, title, domain, tags, title_change)
|
||||
result = service._format_message(
|
||||
name, title, domain, tags, title_change=title_change
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class _ChunkedContent:
|
||||
"""Store chunks to return from iter_chunked."""
|
||||
self._chunks = chunks
|
||||
|
||||
async def iter_chunked(self, size: int) -> AsyncIterator[bytes]:
|
||||
async def iter_chunked(self, _size: int) -> AsyncIterator[bytes]:
|
||||
"""Yield chunks using the interface aiohttp exposes."""
|
||||
for chunk in self._chunks:
|
||||
yield chunk
|
||||
|
||||
@@ -28,6 +28,14 @@ from owncastsentry.notification_service import (
|
||||
from owncastsentry.stream_monitor import (
|
||||
_CLEANUP_DELETE_THRESHOLD,
|
||||
_CLEANUP_WARNING_THRESHOLD,
|
||||
_QUERY_EVERY_2_MINUTES_FAILURE_LIMIT,
|
||||
_QUERY_EVERY_2_MINUTES_INTERVAL,
|
||||
_QUERY_EVERY_3_MINUTES_FAILURE_LIMIT,
|
||||
_QUERY_EVERY_3_MINUTES_INTERVAL,
|
||||
_QUERY_EVERY_5_MINUTES_FAILURE_LIMIT,
|
||||
_QUERY_EVERY_5_MINUTES_INTERVAL,
|
||||
_QUERY_EVERY_15_MINUTES_INTERVAL,
|
||||
_QUERY_EVERY_MINUTE_FAILURE_LIMIT,
|
||||
_TEMPORARY_OFFLINE_NOTIFICATION_COOLDOWN,
|
||||
StreamMonitor,
|
||||
_should_query_stream,
|
||||
@@ -122,25 +130,85 @@ class TestShouldQueryStream:
|
||||
[
|
||||
pytest.param(0, True, id="counter-0-always-query"),
|
||||
pytest.param(1, True, id="counter-1-always-query"),
|
||||
pytest.param(4, True, id="counter-4-always-query"),
|
||||
pytest.param(5, False, id="counter-5-skip-odd"),
|
||||
pytest.param(6, True, id="counter-6-query-even"),
|
||||
pytest.param(9, False, id="counter-9-skip-odd"),
|
||||
pytest.param(10, False, id="counter-10-skip-not-mod-3"),
|
||||
pytest.param(12, True, id="counter-12-query-mod-3"),
|
||||
pytest.param(14, False, id="counter-14-skip-not-mod-3"),
|
||||
pytest.param(15, True, id="counter-15-query-mod-5"),
|
||||
pytest.param(16, False, id="counter-16-skip-not-mod-5"),
|
||||
pytest.param(20, True, id="counter-20-query-mod-5"),
|
||||
pytest.param(29, False, id="counter-29-skip-not-mod-5"),
|
||||
pytest.param(30, True, id="counter-30-query-mod-15"),
|
||||
pytest.param(31, False, id="counter-31-skip-not-mod-15"),
|
||||
pytest.param(45, True, id="counter-45-query-mod-15"),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_MINUTE_FAILURE_LIMIT,
|
||||
True,
|
||||
id="last-every-minute-counter",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_MINUTE_FAILURE_LIMIT + 1,
|
||||
False,
|
||||
id="first-two-minute-counter-skips-odd",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_2_MINUTES_INTERVAL * 3,
|
||||
True,
|
||||
id="two-minute-tier-queries-even",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_2_MINUTES_FAILURE_LIMIT,
|
||||
False,
|
||||
id="last-two-minute-counter-skips-odd",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_2_MINUTES_FAILURE_LIMIT + 1,
|
||||
False,
|
||||
id="first-three-minute-counter-skips-non-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_3_MINUTES_INTERVAL * 4,
|
||||
True,
|
||||
id="three-minute-tier-queries-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_3_MINUTES_FAILURE_LIMIT,
|
||||
False,
|
||||
id="last-three-minute-counter-skips-non-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_3_MINUTES_FAILURE_LIMIT + 1,
|
||||
True,
|
||||
id="first-five-minute-counter-queries-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_3_MINUTES_FAILURE_LIMIT + 2,
|
||||
False,
|
||||
id="five-minute-tier-skips-non-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_5_MINUTES_INTERVAL * 4,
|
||||
True,
|
||||
id="five-minute-tier-queries-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_5_MINUTES_FAILURE_LIMIT,
|
||||
False,
|
||||
id="last-five-minute-counter-skips-non-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_5_MINUTES_FAILURE_LIMIT + 1,
|
||||
True,
|
||||
id="first-fifteen-minute-counter-queries-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_5_MINUTES_FAILURE_LIMIT + 2,
|
||||
False,
|
||||
id="fifteen-minute-tier-skips-non-multiple",
|
||||
),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_15_MINUTES_INTERVAL * 3,
|
||||
True,
|
||||
id="fifteen-minute-tier-queries-multiple",
|
||||
),
|
||||
pytest.param(100, False, id="counter-100-skip-not-mod-15"),
|
||||
pytest.param(105, True, id="counter-105-query-mod-15"),
|
||||
pytest.param(
|
||||
_QUERY_EVERY_15_MINUTES_INTERVAL * 7,
|
||||
True,
|
||||
id="fifteen-minute-tier-later-multiple",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_backoff_tiers(self, counter: int, expected: bool) -> None:
|
||||
def test_backoff_tiers(self, counter: int, *, expected: bool) -> None:
|
||||
"""Return the expected query decision for each backoff tier."""
|
||||
assert _should_query_stream(counter) == expected
|
||||
|
||||
|
||||
Reference in New Issue
Block a user