Files
zyrarbot/tests/test_api_surface.py
T
mmacierzanka dadb5f8b0e
Backend / Lint i formatowanie (pull_request) Successful in 15s
Backend / Testy i kontrakt API (pull_request) Successful in 26s
Panel / Lint, typy i build (pull_request) Failing after 36s
Backend / Testowy build obrazu (pull_request) Successful in 1m34s
refactor: track background tasks in application lifecycle and clean up codebase
2026-09-13 16:58:46 +02:00

125 lines
3.9 KiB
Python

"""Przypina powierzchnię API - panel jest osobnym klientem, więc kontrakt musi być stabilny."""
from __future__ import annotations
import tempfile
import unittest
from zyrarbot.application import Application
from zyrarbot.settings import Settings
EXPECTED_ENDPOINTS = {
("get", "/api/status"),
("get", "/api/health"),
("post", "/api/restart"),
("post", "/api/play"),
("post", "/api/play_file"),
("post", "/api/pause"),
("post", "/api/resume"),
("post", "/api/seek"),
("post", "/api/skip"),
("post", "/api/stop"),
("post", "/api/volume"),
("post", "/api/effects/toggle"),
("post", "/api/loop"),
("post", "/api/loop_playlist"),
("post", "/api/loop_cycle"),
("post", "/api/jump"),
("post", "/api/remove"),
("post", "/api/reorder"),
("post", "/api/queue/restore"),
("post", "/api/queue/plays"),
("get", "/api/uploads"),
("post", "/api/upload"),
("delete", "/api/uploads/{filename}"),
("post", "/api/connect"),
("post", "/api/disconnect"),
("post", "/api/notification_channel"),
("post", "/api/default_plays"),
}
class ApiSurfaceTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._workdir = tempfile.TemporaryDirectory()
settings = Settings(
discord_token="test",
config_dir=f"{cls._workdir.name}/config",
uploads_dir=f"{cls._workdir.name}/uploads",
static_dir=f"{cls._workdir.name}/static",
)
cls.spec = Application(settings).web.openapi()
@classmethod
def tearDownClass(cls) -> None:
cls._workdir.cleanup()
def _declared(self) -> set[tuple[str, str]]:
return {
(method, path)
for path, operations in self.spec["paths"].items()
for method in operations
}
def test_all_expected_endpoints_are_declared(self):
self.assertEqual(set(), EXPECTED_ENDPOINTS - self._declared())
def test_no_endpoint_added_without_updating_this_test(self):
# Panel jest odrębnym klientem - nowa trasa powinna trafić tu świadomie.
self.assertEqual(set(), self._declared() - EXPECTED_ENDPOINTS)
def test_openapi_document_is_well_formed(self):
self.assertTrue(self.spec["openapi"].startswith("3."))
self.assertIn("ŻyrarBOT", self.spec["info"]["title"])
def test_restart_endpoint_calls_restart_state(self):
import asyncio
from unittest.mock import MagicMock
from zyrarbot.ui.api.routers.status_router import restart_bot
mock_request = MagicMock()
mock_restart = MagicMock()
mock_request.app.state.restart = mock_restart
result = asyncio.run(restart_bot(mock_request))
self.assertEqual(result, {"status": "restarting"})
mock_restart.assert_called_once()
def test_voice_channels_payload_filters_unconnectable_channels(self):
from unittest.mock import MagicMock
from zyrarbot.ui.api.status_service import StatusService
mock_guild = MagicMock()
mock_guild.id = 123
mock_guild.name = "Test Guild"
ch1 = MagicMock()
ch1.id = 1
ch1.name = "Allowed Voice"
ch1.permissions_for.return_value.connect = True
ch2 = MagicMock()
ch2.id = 2
ch2.name = "Forbidden Voice"
ch2.permissions_for.return_value.connect = False
mock_guild.voice_channels = [ch1, ch2]
mock_guild.me = MagicMock()
mock_guilds_resolver = MagicMock()
mock_guilds_resolver.guilds.return_value = [mock_guild]
service = StatusService(MagicMock(), mock_guilds_resolver)
channels = service._voice_channels_payload(mock_guild)
self.assertEqual(len(channels), 1)
self.assertEqual(channels[0]["channel_id"], "1")
self.assertEqual(channels[0]["channel_name"], "Allowed Voice")
if __name__ == "__main__":
unittest.main()