Skip to content

Commit 94143d4

Browse files
committed
test: add remaining WebServer tests
Ref: #162
1 parent 118f0f3 commit 94143d4

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

tests/questionpy_sdk/webserver/test_webserver.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
# (c) Technische Universität Berlin, innoCampus <[email protected]>
44

55
import asyncio
6+
import logging
7+
from collections.abc import Iterator
68
from pathlib import Path
9+
from unittest.mock import AsyncMock, Mock
710

811
import pytest
12+
from aiohttp import web
913

1014
from questionpy import Attempt, NeedsManualScoringError, Package, Question, QuestionTypeWrapper
1115
from questionpy.form import FormModel
1216
from questionpy_common.api.qtype import QuestionTypeInterface
1317
from questionpy_common.constants import DIST_DIR
18+
from questionpy_common.manifest import Bcp47LanguageTag, Manifest
1419
from questionpy_sdk.package.builder import DirPackageBuilder
1520
from questionpy_sdk.package.source import PackageSource
1621
from questionpy_sdk.webserver.server import WebServer
@@ -23,6 +28,150 @@
2328
)
2429

2530

31+
@pytest.fixture
32+
def mock_worker_pool(monkeypatch: pytest.MonkeyPatch, mock_worker: AsyncMock) -> Iterator[tuple[Mock, AsyncMock]]:
33+
with monkeypatch.context() as mp:
34+
mock_get_worker_context = Mock()
35+
mock_get_worker_context.return_value = AsyncMock(
36+
__aenter__=AsyncMock(return_value=mock_worker),
37+
__aexit__=AsyncMock(return_value=None),
38+
)
39+
40+
mock_worker_pool_instance = AsyncMock(get_worker=mock_get_worker_context)
41+
mock_worker_pool_cls = Mock()
42+
mock_worker_pool_cls.return_value = AsyncMock(
43+
__aenter__=AsyncMock(return_value=mock_worker_pool_instance),
44+
__aexit__=AsyncMock(return_value=None),
45+
)
46+
47+
mp.setattr("questionpy_sdk.webserver.server.WorkerPool", mock_worker_pool_cls)
48+
yield mock_worker_pool_cls, mock_worker_pool_instance
49+
50+
51+
@pytest.fixture
52+
def mock_worker() -> AsyncMock:
53+
mock_worker = AsyncMock()
54+
manifest = Manifest(
55+
short_name="my_short_name",
56+
version="7.3.1",
57+
api_version="9.4",
58+
author="Testy McTestface",
59+
languages=[Bcp47LanguageTag("en")],
60+
)
61+
mock_worker.get_manifest = AsyncMock(return_value=manifest)
62+
return mock_worker
63+
64+
65+
@pytest.fixture
66+
def mock_web_components(monkeypatch: pytest.MonkeyPatch) -> Iterator[tuple[Mock, AsyncMock]]:
67+
with monkeypatch.context() as mp:
68+
mock_app_runner = AsyncMock()
69+
mp.setattr("questionpy_sdk.webserver.server.web.AppRunner", Mock(return_value=mock_app_runner))
70+
71+
mock_tcp_site = Mock()
72+
mock_tcp_site.return_value.start = AsyncMock()
73+
mp.setattr("questionpy_sdk.webserver.server.web.TCPSite", mock_tcp_site)
74+
75+
yield mock_app_runner, mock_tcp_site
76+
77+
78+
@pytest.fixture
79+
def mock_state_manager(monkeypatch: pytest.MonkeyPatch) -> Iterator[Mock]:
80+
with monkeypatch.context() as mp:
81+
mock_state_manager_cls = Mock()
82+
mp.setattr("questionpy_sdk.webserver.server.StateManager", mock_state_manager_cls)
83+
yield mock_state_manager_cls
84+
85+
86+
async def test_webserver_startup(
87+
mock_worker_pool: tuple[Mock, AsyncMock],
88+
mock_worker: AsyncMock,
89+
mock_web_components: tuple[Mock, AsyncMock],
90+
mock_state_manager: Mock,
91+
) -> None:
92+
mock_worker_pool_cls, mock_worker_pool_instance = mock_worker_pool
93+
mock_app_runner, mock_tcp_site = mock_web_components
94+
95+
package_location = Mock()
96+
state_storage_path = Path("/tmp/storage")
97+
98+
async with WebServer(package_location, state_storage_path):
99+
mock_worker_pool_cls.assert_called_once()
100+
101+
mock_worker_pool_instance.get_worker.assert_called_once_with(package_location, 0, None)
102+
mock_worker.get_manifest.assert_awaited_once()
103+
104+
expected_path = state_storage_path / "local-my_short_name-7.3.1"
105+
mock_state_manager.assert_called_once_with(expected_path)
106+
107+
mock_app_runner.setup.assert_awaited_once()
108+
mock_tcp_site.return_value.start.assert_awaited_once()
109+
110+
111+
async def test_webserver_shutdown(
112+
mock_worker_pool: tuple[Mock, AsyncMock], mock_web_components: tuple[Mock, AsyncMock]
113+
) -> None:
114+
_, mock_worker_pool_instance = mock_worker_pool
115+
mock_app_runner, _ = mock_web_components
116+
117+
async with WebServer(Mock(), Path("/tmp")):
118+
pass
119+
120+
mock_app_runner.cleanup.assert_awaited_once()
121+
mock_worker_pool_instance.__aexit__.assert_awaited_once()
122+
123+
124+
def test_create_webapp_frontend_routes(monkeypatch: pytest.MonkeyPatch) -> None:
125+
with monkeypatch.context() as mp:
126+
mp.setattr("questionpy_sdk.webserver.server.USE_VITE_DEV_SERVER", False)
127+
128+
routes = web.RouteTableDef()
129+
130+
@routes.get("/")
131+
async def some_route(r: web.Request) -> web.Response: # noqa: RUF029
132+
return web.Response()
133+
134+
mp.setattr("questionpy_sdk.webserver.server.frontend_routes", routes)
135+
136+
server = WebServer(Mock(), Path("/tmp"))
137+
app = server._create_webapp()
138+
139+
assert some_route in (route.handler for route in app.router.routes())
140+
141+
142+
def test_create_webapp_vite_middleware(monkeypatch: pytest.MonkeyPatch) -> None:
143+
with monkeypatch.context() as mp:
144+
mp.setattr("questionpy_sdk.webserver.server.USE_VITE_DEV_SERVER", True)
145+
mock_middleware = Mock()
146+
mp.setattr("questionpy_sdk.webserver.middlewares.vite_dev.vite_devserver_middleware", mock_middleware)
147+
148+
server = WebServer(Mock(), Path("/tmp"))
149+
app = server._create_webapp()
150+
151+
assert mock_middleware in app.middlewares
152+
153+
154+
def test_print_status_logs_urls(caplog: pytest.LogCaptureFixture) -> None:
155+
with caplog.at_level(logging.INFO):
156+
server = WebServer(Mock(), Path("/tmp"))
157+
server._runner = Mock()
158+
server._runner.addresses = [("127.0.0.1", 8080), ("::1", 8080, 0, 0)]
159+
160+
server._print_status()
161+
162+
assert "http://127.0.0.1:8080" in caplog.text
163+
assert "http://[::1]:8080" in caplog.text
164+
165+
166+
def test_print_status_raises_on_invalid_address() -> None:
167+
server = WebServer(Mock(), Path("/tmp"))
168+
server._runner = Mock()
169+
server._runner.addresses = [("invalid",)]
170+
171+
with pytest.raises(ValueError, match="Unknown address format"):
172+
server._print_status()
173+
174+
26175
def _pkg_init(package: Package) -> QuestionTypeInterface:
27176
class PackageForm(FormModel):
28177
pass

0 commit comments

Comments
 (0)