Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

326 lines
10 KiB
Python

"""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