Files
OpenJarvis/tests/security/test_network_defaults.py
T
Robby ManihaniandGitHub 259f97b81c fix(server): allow Tauri 2 webview origins on every platform (#292)
The desktop app's chat-completions stream relies on a raw browser
fetch from the Tauri webview to the FastAPI backend, so it is
subject to CORS. The default allowlist only contained
`tauri://localhost`, which is the production webview origin on
macOS, Linux, and iOS. On Windows and Android, Tauri 2 serves the
app from `http://tauri.localhost` (or `https://tauri.localhost`
when `windows.useHttpsScheme` is enabled), so the preflight for
`POST /v1/chat/completions` was rejected and the streaming fetch
threw `TypeError: Failed to fetch` before any byte was read.

Symptom users reported: in the Logs tab the request appears to
succeed up to "Request sent", followed immediately by
"Stream error: Failed to fetch" and "Response: 22 chars" -- which
is exactly the length of the synthesized fallback string
`Error: Failed to fetch` written by InputArea.tsx when streamChat
throws.

Add `http://tauri.localhost` and `https://tauri.localhost` to both
default origin lists (`ServerConfig.cors_origins` and the
`create_app` fallback), and add a regression test that drives a
real preflight against `/v1/chat/completions` for each of the
three Tauri origin schemes.

Browser model loading kept working because it is routed through
the Rust `tauriInvoke('fetch_models')` command, which is a
server-to-server HTTP call not subject to browser CORS -- only
streaming chat goes through the webview's fetch.
2026-04-29 12:55:25 -07:00

185 lines
6.0 KiB
Python

"""Tests for secure network defaults (Section 1 of security hardening)."""
from __future__ import annotations
import ipaddress
import pytest
class TestServerConfigDefaults:
"""ServerConfig should bind to loopback by default."""
def test_default_host_is_loopback(self) -> None:
from openjarvis.core.config import ServerConfig
cfg = ServerConfig()
assert cfg.host == "127.0.0.1"
def test_default_port_unchanged(self) -> None:
from openjarvis.core.config import ServerConfig
cfg = ServerConfig()
assert cfg.port == 8000
def test_cors_origins_default(self) -> None:
from openjarvis.core.config import ServerConfig
cfg = ServerConfig()
assert isinstance(cfg.cors_origins, list)
assert "http://localhost:3000" in cfg.cors_origins
assert "http://localhost:5173" in cfg.cors_origins
# All three Tauri 2 production webview origins must be allowed
# so the desktop chat stream works on every platform.
assert "tauri://localhost" in cfg.cors_origins
assert "http://tauri.localhost" in cfg.cors_origins
assert "https://tauri.localhost" in cfg.cors_origins
assert "*" not in cfg.cors_origins
class TestSecurityConfigDefaults:
"""SecurityConfig should default to redact mode with rate limiting."""
def test_default_mode_is_redact(self) -> None:
from openjarvis.core.config import SecurityConfig
cfg = SecurityConfig()
assert cfg.mode == "redact"
def test_rate_limiting_enabled_by_default(self) -> None:
from openjarvis.core.config import SecurityConfig
cfg = SecurityConfig()
assert cfg.rate_limit_enabled is True
def test_bypass_defaults_conservative(self) -> None:
from openjarvis.core.config import SecurityConfig
cfg = SecurityConfig()
assert cfg.local_engine_bypass is False
assert cfg.local_tool_bypass is False
def test_profile_default_empty(self) -> None:
from openjarvis.core.config import SecurityConfig
cfg = SecurityConfig()
assert cfg.profile == ""
def _is_loopback(host: str) -> bool:
"""Check if a host string is a loopback address."""
try:
return ipaddress.ip_address(host).is_loopback
except ValueError:
return host in ("localhost", "")
class TestNonLoopbackAuthEnforcement:
"""Server must require API key when binding non-loopback."""
def test_loopback_allows_no_key(self) -> None:
assert _is_loopback("127.0.0.1")
def test_wildcard_is_not_loopback(self) -> None:
assert not _is_loopback("0.0.0.0")
def test_non_loopback_requires_key(self) -> None:
starlette = pytest.importorskip("starlette") # noqa: F841
from openjarvis.server.auth_middleware import check_bind_safety
try:
check_bind_safety("0.0.0.0", api_key="")
assert False, "Should have raised"
except SystemExit:
pass
def test_non_loopback_with_key_ok(self) -> None:
starlette = pytest.importorskip("starlette") # noqa: F841
from openjarvis.server.auth_middleware import check_bind_safety
check_bind_safety("0.0.0.0", api_key="oj_sk_test123")
class TestCORSConfiguration:
"""CORS should use configured origins, not wildcard."""
def test_create_app_uses_configured_origins(self) -> None:
pytest.importorskip("fastapi")
from unittest.mock import MagicMock
from fastapi.testclient import TestClient
from openjarvis.server.app import create_app
mock_engine = MagicMock()
mock_engine.health.return_value = True
mock_engine.list_models.return_value = ["test-model"]
app = create_app(
mock_engine,
"test-model",
cors_origins=["http://localhost:3000"],
)
client = TestClient(app)
resp = client.options(
"/health",
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "GET",
},
)
assert (
resp.headers.get("access-control-allow-origin") == "http://localhost:3000"
)
resp2 = client.options(
"/health",
headers={
"Origin": "http://evil.com",
"Access-Control-Request-Method": "GET",
},
)
assert resp2.headers.get("access-control-allow-origin") != "http://evil.com"
def test_default_origins_allow_tauri_webview(self) -> None:
"""Regression: Tauri 2 desktop chat-completions stream must not
be blocked by CORS preflight on any platform.
macOS / Linux use ``tauri://localhost``; Windows / Android use
``http://tauri.localhost`` (or the ``https`` variant when
``windows.useHttpsScheme`` is enabled). The default origin list
in ``create_app`` must accept all three so the user does not
see "Stream error: Failed to fetch" in the desktop logs.
"""
pytest.importorskip("fastapi")
from unittest.mock import MagicMock
from fastapi.testclient import TestClient
from openjarvis.server.app import create_app
mock_engine = MagicMock()
mock_engine.health.return_value = True
mock_engine.list_models.return_value = ["test-model"]
app = create_app(mock_engine, "test-model")
client = TestClient(app)
for origin in (
"tauri://localhost",
"http://tauri.localhost",
"https://tauri.localhost",
):
resp = client.options(
"/v1/chat/completions",
headers={
"Origin": origin,
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "content-type",
},
)
assert resp.headers.get("access-control-allow-origin") == origin, (
f"Tauri origin {origin} was not allowed by default CORS list"
)