Files
wehub-resource-sync 7a0da7932b
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
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

254 lines
8.2 KiB
Python

"""
Tests for security/decorators.py — require_json_body decorator.
Covers all three error formats and common edge cases:
empty body, non-dict JSON payloads, malformed JSON, and valid dicts.
"""
import json
import pytest
from flask import Flask
from local_deep_research.security.decorators import require_json_body
@pytest.fixture
def app():
"""Minimal Flask app with routes using each error format."""
app = Flask(__name__)
@app.route("/simple", methods=["POST"])
@require_json_body()
def simple_route():
return {"ok": True}
@app.route("/simple-custom", methods=["POST"])
@require_json_body(error_message="Query parameter is required")
def simple_custom_route():
return {"ok": True}
@app.route("/status", methods=["POST"])
@require_json_body(error_format="status")
def status_route():
return {"ok": True}
@app.route("/status-custom", methods=["POST"])
@require_json_body(
error_format="status", error_message="No settings data provided"
)
def status_custom_route():
return {"ok": True}
@app.route("/success", methods=["POST"])
@require_json_body(error_format="success")
def success_route():
return {"ok": True}
@app.route("/success-custom", methods=["POST"])
@require_json_body(
error_format="success", error_message="Missing required fields"
)
def success_custom_route():
return {"ok": True}
return app
@pytest.fixture
def client(app):
return app.test_client()
# ---------------------------------------------------------------------------
# Valid dict payloads — should always pass through
# ---------------------------------------------------------------------------
class TestValidPayloads:
def test_valid_dict_passes_simple(self, client):
resp = client.post(
"/simple",
data=json.dumps({"key": "value"}),
content_type="application/json",
)
assert resp.status_code == 200
assert resp.json["ok"] is True
def test_valid_dict_passes_status(self, client):
resp = client.post(
"/status",
data=json.dumps({"key": "value"}),
content_type="application/json",
)
assert resp.status_code == 200
def test_valid_dict_passes_success(self, client):
resp = client.post(
"/success",
data=json.dumps({"key": "value"}),
content_type="application/json",
)
assert resp.status_code == 200
def test_empty_dict_passes(self, client):
"""An empty dict {} is a valid JSON body."""
resp = client.post(
"/simple",
data=json.dumps({}),
content_type="application/json",
)
assert resp.status_code == 200
def test_nested_dict_passes(self, client):
resp = client.post(
"/simple",
data=json.dumps({"nested": {"a": 1}, "list": [1, 2]}),
content_type="application/json",
)
assert resp.status_code == 200
# ---------------------------------------------------------------------------
# Empty / missing body — should reject
# ---------------------------------------------------------------------------
class TestEmptyBody:
def test_no_body_simple(self, client):
resp = client.post("/simple")
assert resp.status_code == 400
assert resp.json["error"] == "Request body must be valid JSON"
def test_no_body_status(self, client):
resp = client.post("/status")
assert resp.status_code == 400
assert resp.json["status"] == "error"
assert resp.json["message"] == "Request body must be valid JSON"
def test_no_body_success(self, client):
resp = client.post("/success")
assert resp.status_code == 400
assert resp.json["success"] is False
assert resp.json["error"] == "Request body must be valid JSON"
def test_empty_string_body(self, client):
resp = client.post("/simple", data="", content_type="application/json")
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# Malformed JSON — should reject
# ---------------------------------------------------------------------------
class TestMalformedJSON:
def test_invalid_json_string(self, client):
resp = client.post(
"/simple",
data="{not valid json",
content_type="application/json",
)
assert resp.status_code == 400
assert "error" in resp.json
def test_plain_text_body(self, client):
resp = client.post(
"/simple", data="hello world", content_type="text/plain"
)
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# Non-dict JSON values — should reject
# ---------------------------------------------------------------------------
class TestNonDictJSON:
def test_json_null(self, client):
resp = client.post(
"/simple",
data=json.dumps(None),
content_type="application/json",
)
assert resp.status_code == 400
def test_json_list(self, client):
resp = client.post(
"/simple",
data=json.dumps([1, 2, 3]),
content_type="application/json",
)
assert resp.status_code == 400
def test_json_string(self, client):
resp = client.post(
"/simple",
data=json.dumps("just a string"),
content_type="application/json",
)
assert resp.status_code == 400
def test_json_number(self, client):
resp = client.post(
"/simple",
data=json.dumps(42),
content_type="application/json",
)
assert resp.status_code == 400
def test_json_boolean(self, client):
resp = client.post(
"/simple",
data=json.dumps(True),
content_type="application/json",
)
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# Custom error messages
# ---------------------------------------------------------------------------
class TestCustomMessages:
def test_simple_custom_message(self, client):
resp = client.post("/simple-custom")
assert resp.status_code == 400
assert resp.json["error"] == "Query parameter is required"
def test_status_custom_message(self, client):
resp = client.post("/status-custom")
assert resp.status_code == 400
assert resp.json["message"] == "No settings data provided"
def test_success_custom_message(self, client):
resp = client.post("/success-custom")
assert resp.status_code == 400
assert resp.json["error"] == "Missing required fields"
# ---------------------------------------------------------------------------
# Error format response structure
# ---------------------------------------------------------------------------
class TestErrorFormatStructure:
def test_simple_format_keys(self, client):
resp = client.post("/simple")
assert resp.status_code == 400
assert set(resp.json.keys()) == {"error"}
def test_status_format_keys(self, client):
resp = client.post("/status")
assert resp.status_code == 400
assert set(resp.json.keys()) == {"status", "message"}
def test_success_format_keys(self, client):
resp = client.post("/success")
assert resp.status_code == 400
assert set(resp.json.keys()) == {"success", "error"}
# ---------------------------------------------------------------------------
# Decorator preserves function metadata
# ---------------------------------------------------------------------------
class TestDecoratorMetadata:
def test_wraps_preserves_name(self, app):
"""@wraps should preserve the original function name."""
with app.test_request_context():
for rule in app.url_map.iter_rules():
if rule.endpoint == "simple_route":
view_func = app.view_functions[rule.endpoint]
assert view_func.__name__ == "simple_route"
return
pytest.fail("simple_route endpoint not found")