"""Tests for PWA static file serving in the SPA catch-all endpoint.""" from __future__ import annotations import pathlib from unittest.mock import MagicMock import pytest fastapi = pytest.importorskip("fastapi") from fastapi.testclient import TestClient # noqa: E402 from openjarvis.server.app import create_app # noqa: E402 # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_engine(): engine = MagicMock() engine.engine_id = "mock" engine.health.return_value = True engine.list_models.return_value = ["test-model"] engine.generate.return_value = { "content": "hello", "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, "model": "test-model", "finish_reason": "stop", } return engine def _create_static_dir(tmp_path: pathlib.Path) -> pathlib.Path: """Create a temporary static directory with index.html and PWA files.""" static = tmp_path / "static" static.mkdir() (static / "index.html").write_text("SPA") (static / "sw.js").write_text("// service worker") (static / "manifest.webmanifest").write_text('{"name":"OpenJarvis"}') (static / "pwa-192x192.png").write_bytes(b"\x89PNG placeholder") assets = static / "assets" assets.mkdir() (assets / "app.js").write_text("console.log('app')") return static @pytest.fixture() def client_with_static(tmp_path, monkeypatch): """Create a test client with a real temporary static directory.""" static_dir = _create_static_dir(tmp_path) engine = _make_engine() # Patch Path(__file__).parent to make static_dir resolve to our tmp dir original_truediv = pathlib.Path.__truediv__ def patched_truediv(self, key): result = original_truediv(self, key) # Intercept the "static" lookup in app.py if key == "static" and str(self).endswith("server"): return static_dir return result monkeypatch.setattr(pathlib.Path, "__truediv__", patched_truediv) app = create_app(engine, "test-model") monkeypatch.undo() # Restore immediately after app creation return TestClient(app) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestPWAServing: def test_sw_js_served_as_file(self, client_with_static): """Service worker file should be served directly, not as index.html.""" resp = client_with_static.get("/sw.js") assert resp.status_code == 200 assert "// service worker" in resp.text def test_manifest_served_as_file(self, client_with_static): """Web manifest should be served directly.""" resp = client_with_static.get("/manifest.webmanifest") assert resp.status_code == 200 assert "OpenJarvis" in resp.text def test_icon_served_as_file(self, client_with_static): """PWA icon should be served directly.""" resp = client_with_static.get("/pwa-192x192.png") assert resp.status_code == 200 assert b"PNG" in resp.content def test_api_routes_bypass_spa(self, client_with_static): """API routes should still work regardless of SPA catch-all.""" resp = client_with_static.get("/v1/models") assert resp.status_code == 200 data = resp.json() assert data["object"] == "list" def test_path_traversal_blocked(self, client_with_static): """Path traversal attempts should fall back to index.html.""" resp = client_with_static.get("/../../etc/passwd") assert resp.status_code == 200 # Should get index.html, not the passwd file assert "SPA" in resp.text