mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-15 01:28:27 +00:00
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.
121 lines
3.6 KiB
Python
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"}
|