"""Tests for uncovered branches in security_headers.py.""" import flask import pytest from local_deep_research.security.security_headers import SecurityHeaders def _make_app(**config_overrides): """Create a minimal Flask app with SecurityHeaders.""" app = flask.Flask(__name__) app.config["TESTING"] = True for k, v in config_overrides.items(): app.config[k] = v SecurityHeaders(app) return app class TestIsApiRoute: def test_api_prefix(self): assert SecurityHeaders._is_api_route("/api/research") is True def test_research_api_prefix(self): assert SecurityHeaders._is_api_route("/research/api/test") is True def test_history_api_prefix(self): assert SecurityHeaders._is_api_route("/history/api") is True def test_static_not_api(self): assert SecurityHeaders._is_api_route("/static/file.js") is False def test_root_not_api(self): assert SecurityHeaders._is_api_route("/") is False def test_settings_not_api(self): assert SecurityHeaders._is_api_route("/settings/") is False class TestValidateCorsConfig: def test_credentials_with_wildcard_raises(self): with pytest.raises( ValueError, match="Cannot use credentials with wildcard" ): _make_app( SECURITY_CORS_ALLOWED_ORIGINS="*", SECURITY_CORS_ALLOW_CREDENTIALS=True, ) def test_credentials_with_specific_origin_ok(self): app = _make_app( SECURITY_CORS_ALLOWED_ORIGINS="https://example.com", SECURITY_CORS_ALLOW_CREDENTIALS=True, ) assert app is not None def test_cors_disabled_skips_validation(self): # Should not raise even with bad config when CORS is disabled app = _make_app( SECURITY_CORS_ENABLED=False, SECURITY_CORS_ALLOWED_ORIGINS="*", SECURITY_CORS_ALLOW_CREDENTIALS=True, ) assert app is not None def test_multi_origin_with_credentials_logs_info(self): app = _make_app( SECURITY_CORS_ALLOWED_ORIGINS="https://a.com,https://b.com", SECURITY_CORS_ALLOW_CREDENTIALS=True, ) assert app is not None class TestAddSecurityHeaders: def test_basic_headers_present(self): app = _make_app() with app.test_client() as client: @app.route("/test") def test_route(): return "ok" resp = client.get("/test") assert "Content-Security-Policy" in resp.headers assert "X-Frame-Options" in resp.headers assert "X-Content-Type-Options" in resp.headers assert "Permissions-Policy" in resp.headers assert "Referrer-Policy" in resp.headers def test_hsts_not_set_on_http(self): app = _make_app() with app.test_client() as client: @app.route("/test") def test_route(): return "ok" resp = client.get("/test") assert "Strict-Transport-Security" not in resp.headers def test_static_path_no_cache_control(self): """Static paths should NOT get no-cache headers.""" app = _make_app() @app.route("/static/test.js") def static_test(): return "js" with app.test_client() as client: resp = client.get("/static/test.js") cache = resp.headers.get("Cache-Control", "") assert "no-store" not in cache def test_non_static_path_gets_no_cache(self): app = _make_app() @app.route("/page") def page(): return "html" with app.test_client() as client: resp = client.get("/page") assert "no-store" in resp.headers.get("Cache-Control", "") def test_coep_policy_from_config(self): app = _make_app(SECURITY_COEP_POLICY="require-corp") @app.route("/test") def test_route(): return "ok" with app.test_client() as client: resp = client.get("/test") assert ( resp.headers["Cross-Origin-Embedder-Policy"] == "require-corp" ) class TestCorsHeaders: def test_wildcard_origin(self): app = _make_app(SECURITY_CORS_ALLOWED_ORIGINS="*") @app.route("/api/test") def api_test(): return "ok" with app.test_client() as client: resp = client.get("/api/test") assert resp.headers["Access-Control-Allow-Origin"] == "*" assert "Access-Control-Allow-Credentials" not in resp.headers def test_single_origin(self): app = _make_app( SECURITY_CORS_ALLOWED_ORIGINS="https://example.com", SECURITY_CORS_ALLOW_CREDENTIALS=True, ) @app.route("/api/test") def api_test(): return "ok" with app.test_client() as client: resp = client.get("/api/test") assert ( resp.headers["Access-Control-Allow-Origin"] == "https://example.com" ) assert resp.headers["Access-Control-Allow-Credentials"] == "true" def test_multi_origin_reflects_matching(self): app = _make_app( SECURITY_CORS_ALLOWED_ORIGINS="https://a.com,https://b.com", SECURITY_CORS_ALLOW_CREDENTIALS=True, ) @app.route("/api/test") def api_test(): return "ok" with app.test_client() as client: resp = client.get("/api/test", headers={"Origin": "https://b.com"}) assert ( resp.headers["Access-Control-Allow-Origin"] == "https://b.com" ) assert resp.headers["Access-Control-Allow-Credentials"] == "true" def test_multi_origin_non_matching_omits_acao(self): app = _make_app( SECURITY_CORS_ALLOWED_ORIGINS="https://a.com,https://b.com", ) @app.route("/api/test") def api_test(): return "ok" with app.test_client() as client: resp = client.get( "/api/test", headers={"Origin": "https://evil.com"} ) assert "Access-Control-Allow-Origin" not in resp.headers def test_non_api_route_no_cors(self): app = _make_app(SECURITY_CORS_ALLOWED_ORIGINS="*") @app.route("/page") def page(): return "ok" with app.test_client() as client: resp = client.get("/page") assert "Access-Control-Allow-Origin" not in resp.headers def test_max_age_header(self): app = _make_app(SECURITY_CORS_ALLOWED_ORIGINS="*") @app.route("/api/test") def api_test(): return "ok" with app.test_client() as client: resp = client.get("/api/test") assert resp.headers["Access-Control-Max-Age"] == "3600" def test_vary_origin_present_when_acao_is_dynamic(self): """A reflected (non-wildcard) ACAO must carry Vary: Origin so shared caches don't serve one origin's ACAO to another.""" app = _make_app( SECURITY_CORS_ALLOWED_ORIGINS="https://a.com,https://b.com", ) @app.route("/api/test") def api_test(): return "ok" with app.test_client() as client: resp = client.get("/api/test", headers={"Origin": "https://b.com"}) assert ( resp.headers["Access-Control-Allow-Origin"] == "https://b.com" ) vary_tokens = [ v.strip().lower() for v in resp.headers.get("Vary", "").split(",") ] assert "origin" in vary_tokens def test_vary_origin_absent_for_wildcard(self): """A fixed wildcard ACAO is the same for every origin, so it must NOT add Vary: Origin (which would needlessly fragment caches).""" app = _make_app(SECURITY_CORS_ALLOWED_ORIGINS="*") @app.route("/api/test") def api_test(): return "ok" with app.test_client() as client: resp = client.get("/api/test", headers={"Origin": "https://x.com"}) assert resp.headers["Access-Control-Allow-Origin"] == "*" vary_tokens = [ v.strip().lower() for v in resp.headers.get("Vary", "").split(",") ] assert "origin" not in vary_tokens def test_vary_origin_not_duplicated(self): """When the response already carries a Vary header, Origin is appended once rather than duplicated.""" from flask import make_response app = _make_app( SECURITY_CORS_ALLOWED_ORIGINS="https://a.com,https://b.com", ) @app.route("/api/test") def api_test(): resp = make_response("ok") resp.headers["Vary"] = "Accept-Encoding" return resp with app.test_client() as client: resp = client.get("/api/test", headers={"Origin": "https://b.com"}) vary_tokens = [ v.strip().lower() for v in resp.headers.get("Vary", "").split(",") if v.strip() ] assert vary_tokens.count("origin") == 1 assert "accept-encoding" in vary_tokens class TestGetCspPolicy: def test_default_connect_src(self): app = _make_app() sh = SecurityHeaders() sh.app = app csp = sh.get_csp_policy() assert "connect-src 'self'" in csp def test_custom_connect_src(self): app = _make_app( SECURITY_CSP_CONNECT_SRC="'self' https://api.example.com" ) sh = SecurityHeaders() sh.app = app csp = sh.get_csp_policy() assert "connect-src 'self' https://api.example.com" in csp def test_csp_contains_required_directives(self): app = _make_app() sh = SecurityHeaders() sh.app = app csp = sh.get_csp_policy() for directive in [ "default-src", "script-src", "style-src", "img-src", "object-src 'none'", ]: assert directive in csp class TestGetPermissionsPolicy: def test_disables_dangerous_features(self): policy = SecurityHeaders.get_permissions_policy() for feature in ["geolocation", "camera", "microphone", "payment"]: assert f"{feature}=()" in policy