mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-26 23:51:14 +02:00
fix: strip 'body' root from validation error summary
This commit is contained in:
parent
15962d205f
commit
0118744c20
2 changed files with 49 additions and 2 deletions
|
|
@ -243,7 +243,12 @@ def _validation_error_handler(
|
|||
]
|
||||
|
||||
def _segment(field: dict[str, Any]) -> str:
|
||||
loc = " -> ".join(field["loc"])
|
||||
# Drop the "body" request root so model-level errors read as a plain
|
||||
# sentence and field errors read as "field -> sub", not "body -> field".
|
||||
path = field["loc"]
|
||||
if path and path[0] == "body":
|
||||
path = path[1:]
|
||||
loc = " -> ".join(path)
|
||||
return f"{loc}: {field['msg']}" if loc else field["msg"]
|
||||
|
||||
summary = "; ".join(_segment(f) for f in fields)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import json
|
|||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from pydantic import BaseModel, model_validator
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from app.exceptions import (
|
||||
|
|
@ -32,6 +33,23 @@ from app.exceptions import (
|
|||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class _RequireOne(BaseModel):
|
||||
"""Body model with a cross-field rule, used to test the 422 summary.
|
||||
|
||||
Defined at module scope (not inside the app factory) so FastAPI's ``Body``
|
||||
``TypeAdapter`` can resolve the forward reference.
|
||||
"""
|
||||
|
||||
a: str | None = None
|
||||
b: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _at_least_one(self):
|
||||
if not self.a and not self.b:
|
||||
raise ValueError("Provide at least one of 'a' or 'b'.")
|
||||
return self
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers - lightweight FastAPI app that re-uses the real global handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -39,7 +57,7 @@ pytestmark = pytest.mark.unit
|
|||
|
||||
def _make_test_app():
|
||||
"""Build a minimal FastAPI app with the same handlers as the real one."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
|
@ -124,6 +142,10 @@ def _make_test_app():
|
|||
async def validated(item: Item):
|
||||
return item.model_dump()
|
||||
|
||||
@app.post("/require-one")
|
||||
async def require_one(payload: _RequireOne = Body(...)):
|
||||
return payload.model_dump()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
|
@ -277,6 +299,26 @@ class TestValidationErrorHandler:
|
|||
body = _assert_envelope(resp, 422)
|
||||
assert body["error"]["code"] == "VALIDATION_ERROR"
|
||||
|
||||
def test_field_error_drops_body_root(self, client):
|
||||
# The "body" request root is noise in the human summary; the field name
|
||||
# is kept so clients can still attach the error inline.
|
||||
resp = client.post("/require-one", json={"a": 123})
|
||||
body = _assert_envelope(resp, 422)
|
||||
msg = body["error"]["message"]
|
||||
assert "body" not in msg
|
||||
assert "a:" in msg
|
||||
assert body["error"]["fields"][0]["loc"] == ["body", "a"]
|
||||
|
||||
def test_model_level_error_reads_as_sentence(self, client):
|
||||
# A cross-field rule (loc == ["body"]) must read as a plain sentence,
|
||||
# not "body: ...", while fields still carry the location for clients.
|
||||
resp = client.post("/require-one", json={})
|
||||
body = _assert_envelope(resp, 422)
|
||||
assert body["error"]["message"] == (
|
||||
"Validation failed: Provide at least one of 'a' or 'b'."
|
||||
)
|
||||
assert body["error"]["fields"][0]["loc"] == ["body"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SurfSenseError class hierarchy unit tests
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue