Files
shelfmark/tests/core/test_system_routes.py
T
splitsec2 d1e959758d refactor: move the system routes into core/system_routes.py (#1624)
The system routes are the next group from the plan in Discussion #1536:
`/api/config`, `/api/health`, `/api/debug`, `/api/restart` and the
routes that serve the frontend (`/`, the SPA catch-all, `/assets/...`,
the logo, the favicons and `theme-init.js`) move from `main.py` to
`core/system_routes.py`, registered with `register_system_routes(app,
login_required, ...)`. `main.py` is about 250 lines shorter.

Same two-commit layout as before. The first commit adds 14 tests for the
frontend-serving routes and the `/api/config` error paths, which had
none, and they pass on current `main`. The second commit is the move,
and no existing test changed.

The helpers only these routes use moved with them (`admin_required`,
built on the `login_required` that is passed in, plus `_base_href` and
`_serve_index_html`). What `main.py` still owns, the base path, the
frontend folder, the auth mode and the session user id, is passed in as
callables evaluated per request, so the tests that patch them on `main`
still work. The debug routes are only registered when debug is on, as
before, with the stop-GUI and run-script hooks handed in.

The thing to look at is the SPA catch-all, which is now registered
earlier in `main.py` than it used to be (the old comment said it must be
last). Werkzeug picks the most specific rule whatever the registration
order, and I checked it: all 76 rule and method pairs in the URL map
still match their own endpoint, on `main` and on this branch, and an
unknown `/api/...` path still gets the JSON 404.

The Flask URL map is identical before and after, all 77 rules, and with
debug on (79). The full suite, ruff and basedpyright are clean. I also
ran the app from this branch: health, config, the logo, the favicon and
the theme script answer as before, a client-side path like
`/library/books` gets the index page, an unknown `/api/` path gets the
JSON 404, and the page loads in a browser. I did not exercise
`/api/restart` or a real debug script run, since nothing tests them.
2026-11-01 14:12:11 -07:00

121 lines
3.6 KiB
Python

"""Tests for the frontend-serving routes and the gaps in /api/config coverage."""
from __future__ import annotations
import importlib
from pathlib import Path
from unittest.mock import patch
import pytest
_INDEX = '<html><head><base href="/" data-shelfmark-base /></head><body>app</body></html>'
@pytest.fixture(scope="module")
def main_module():
"""Import `shelfmark.main` with background startup disabled."""
with patch("shelfmark.download.orchestrator.start"):
import shelfmark.main as main
importlib.reload(main)
return main
@pytest.fixture
def client(main_module):
return main_module.app.test_client()
@pytest.fixture
def dist(main_module, tmp_path: Path):
"""A throwaway frontend build in place of the real `frontend-dist`."""
(tmp_path / "assets").mkdir()
(tmp_path / "assets" / "app.js").write_text("console.log(1)")
(tmp_path / "index.html").write_text(_INDEX)
(tmp_path / "logo.png").write_bytes(b"\x89PNG")
(tmp_path / "favicon.ico").write_bytes(b"ico")
(tmp_path / "theme-init.js").write_text("// theme")
with (
patch.object(main_module, "FRONTEND_DIST", tmp_path),
patch.object(main_module, "BASE_PATH", ""),
):
yield tmp_path
def test_index_serves_the_app_with_the_default_base_tag(client, dist):
resp = client.get("/")
assert resp.status_code == 200
assert resp.mimetype == "text/html"
assert resp.get_data(as_text=True) == _INDEX
def test_index_rewrites_the_base_tag_for_a_subpath(main_module, client, dist):
with patch.object(main_module, "BASE_PATH", "/books"):
resp = client.get("/")
assert '<base href="/books/" data-shelfmark-base />' in resp.get_data(as_text=True)
def test_index_without_a_frontend_build_is_a_404(client, dist):
(dist / "index.html").unlink()
assert client.get("/").status_code == 404
def test_catch_all_serves_the_app_for_client_side_routes(client, dist):
resp = client.get("/settings/general")
assert resp.status_code == 200
assert resp.get_data(as_text=True) == _INDEX
@pytest.mark.parametrize("path", ["/api/no-such-route", "/api/deeper/no-such-route"])
def test_catch_all_keeps_unknown_api_paths_a_404(client, dist, path):
resp = client.get(path)
assert resp.status_code == 404
assert resp.get_json() == {"error": "Resource not found"}
def test_assets_are_served_from_the_build(client, dist):
resp = client.get("/assets/app.js")
assert resp.status_code == 200
assert resp.get_data(as_text=True) == "console.log(1)"
def test_a_missing_asset_is_a_404_not_the_app(client, dist):
assert client.get("/assets/missing.js").status_code == 404
@pytest.mark.parametrize(
("path", "mimetype"),
[
("/logo.png", "image/png"),
("/favicon.ico", "image/vnd.microsoft.icon"),
("/favicon-32.png", "image/vnd.microsoft.icon"),
("/theme-init.js", "application/javascript"),
],
)
def test_root_files_are_served_with_their_mimetype(client, dist, path, mimetype):
resp = client.get(path)
assert resp.status_code == 200
assert resp.mimetype == mimetype
def test_config_requires_a_session_when_auth_is_on(main_module, client):
with patch.object(main_module, "get_auth_mode", return_value="builtin"):
resp = client.get("/api/config")
assert resp.status_code == 401
def test_config_reports_an_operational_error(client):
with patch("shelfmark.core.onboarding.is_onboarding_complete", side_effect=RuntimeError("x")):
resp = client.get("/api/config")
assert resp.status_code == 500
assert resp.get_json() == {"error": "Could not load the configuration"}