tests/spa/test_inspector_section.py¶
Source from this local checkout, regenerated when the reader rebuilds.
Line links use #L<number>; a GitHub line range opens its first line.
1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 # https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """The ``_server/inspector`` section: mounted by the env var, and by nothing else.16 17 Contract: the mount IS the gate — without ``GNR_ASGI_INSPECTOR`` the section18 does not exist at all; with it the page is HTML and the census is JSON, keyed by19 the code of the front whose pool it watches. And in neither case does the20 inspector touch the hosted site: no connection cookie ever comes back from it.21 22 The front attaches it, on its own startup, so every server here goes through its23 lifespan — the section does not exist before the pool does.24 """25 26 from __future__ import annotations27 28 import json29 from typing import Any30 31 import pytest32 33 from genro_asgi import AsgiServer, ServerApplication34 from genro_asgi.config.builder import AsgiConfigBuilder35 from genro_asgi.lifespan import FatalBootError36 from genro_asgi_multiworker_spa.inspector_section import INSPECTOR_ENV_VAR37 from genro_asgi_multiworker_spa.orchestration import SpaCommander38 from genro_asgi_multiworker_spa.spa_app import SPA_CONNECTION_ID_COOKIE, SpaApplication39 40 from ..conftest import LifespanRunner41 42 43 class InspectorScriptedCommander(SpaCommander):44 """A pool that comes up without processes: no wire, no beat, no worker."""45 46 async def start(self) -> None:47 pass48 49 async def stop(self) -> None:50 pass51 52 53 class InspectorScriptedFront(SpaApplication):54 """The front under test: the real startup, over a pool that launches nothing."""55 56 commander_class = InspectorScriptedCommander57 58 59 def inspector_recipe_for(root) -> type[AsgiConfigBuilder]:60 """A recipe with one spa front and its pool, mounted at the root."""61 62 class FrontConfig(AsgiConfigBuilder):63 def main(self, configuration_root: Any) -> None:64 cfg = configuration_root.configuration()65 front = cfg.applications().application(66 code="site", mount="", app_class=InspectorScriptedFront67 )68 commander = front.orchestration().commander(69 frozen_users_path=str(root / "frozen_users"),70 instance_dir=str(root / "i"),71 )72 commander.groups(default="standard").group(73 name="standard", entry_module="never.launched"74 )75 76 return FrontConfig77 78 79 @pytest.fixture80 async def inspector_server(tmp_path, monkeypatch):81 """A started server whose front attached the inspector on startup."""82 monkeypatch.setenv(INSPECTOR_ENV_VAR, "1")83 server = AsgiServer(config=inspector_recipe_for(tmp_path))84 runner = LifespanRunner(server)85 await runner.startup()86 yield server87 await runner.shutdown()88 89 90 @pytest.fixture91 async def plain_server(tmp_path, monkeypatch):92 """The same server with the variable unset: the section is not there."""93 monkeypatch.delenv(INSPECTOR_ENV_VAR, raising=False)94 server = AsgiServer(config=inspector_recipe_for(tmp_path))95 runner = LifespanRunner(server)96 await runner.startup()97 yield server98 await runner.shutdown()99 100 101 async def test_without_the_env_var_the_page_is_not_there(102 plain_server, http_request, response_status103 ):104 sent = await http_request(plain_server, "/_server/inspector/page")105 106 assert response_status(sent) == 404107 108 109 async def test_the_page_is_html(110 inspector_server, http_request, response_status, response_headers, response_body111 ):112 sent = await http_request(inspector_server, "/_server/inspector/page")113 114 assert response_status(sent) == 200115 assert response_headers(sent)[b"content-type"].startswith(b"text/html")116 assert b"worker-grid" in response_body(sent)117 118 119 async def test_the_census_is_json_under_the_front_code(120 inspector_server, http_request, response_status, response_headers, response_body121 ):122 sent = await http_request(inspector_server, "/_server/inspector/census")123 124 assert response_status(sent) == 200125 assert response_headers(sent)[b"content-type"].startswith(b"application/json")126 census = json.loads(response_body(sent))127 assert set(census) == {"site"}128 assert census["site"]["user_map"] == {}129 130 131 async def test_the_inspector_mints_no_cookie(inspector_server, http_request, response_headers):132 for path in ("/_server/inspector/page", "/_server/inspector/census"):133 headers = await http_request(inspector_server, path)134 135 cookie = response_headers(headers).get(b"set-cookie", b"")136 assert SPA_CONNECTION_ID_COOKIE.encode() not in cookie137 138 139 async def test_the_stream_opens_with_the_census(inspector_server, sse_request):140 connection = await sse_request(inspector_server, "/_server/inspector/stream")141 142 frames = await connection.wait_frames(2)143 await connection.close()144 145 assert b"retry: 2000" in frames[0]146 assert b"event: census" in frames[1]147 148 149 async def test_the_page_carries_its_containers_and_its_endpoints(150 inspector_server, http_request, response_body151 ):152 page = response_body(await http_request(inspector_server, "/_server/inspector/page"))153 154 for container_id in ("commander-panel", "worker-grid", "event-log", "last-read", "toggle-stream"):155 assert f'id="{container_id}"'.encode() in page156 assert b'"/census"' in page157 assert b'"/stream"' in page158 159 160 async def test_no_server_app_leaves_the_inspector_unmounted(inspector_server, caplog):161 """The variable set and no ``_server`` to host it: said once, nothing attached."""162 front = inspector_server.applications["site"]163 del inspector_server.applications["_server"]164 165 with caplog.at_level("WARNING"):166 front.mount_inspector()167 168 assert INSPECTOR_ENV_VAR in caplog.text169 assert "nowhere to go" in caplog.text170 171 172 async def test_a_second_front_on_one_server_does_not_boot(tmp_path, monkeypatch):173 """A server has ONE orchestrated application: the second attach is fatal."""174 monkeypatch.setenv(INSPECTOR_ENV_VAR, "1")175 server = AsgiServer(config=inspector_recipe_for(tmp_path))176 front = server.applications["site"]177 await LifespanRunner(server).startup()178 179 second = InspectorScriptedFront(code="site2", mount="other")180 server.register_application(second)181 182 with pytest.raises(FatalBootError, match="already attached"):183 second.mount_inspector()184 185 server_app = server.applications["_server"]186 assert isinstance(server_app, ServerApplication)187 assert server_app.sections["inspector"].application is front