From db94ac050fabc5b448a4a7ac174e78ab5b97ee75 Mon Sep 17 00:00:00 2001 From: adavyas Date: Sat, 28 Mar 2026 20:10:47 -0700 Subject: [PATCH] Fix empty search query validation --- src/schemas/api.py | 19 ++++++++++--------- tests/routes/test_peers.py | 16 ++++++++++++++++ tests/test_schema_validations.py | 14 ++++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/schemas/api.py b/src/schemas/api.py index c411b07f..121292cc 100644 --- a/src/schemas/api.py +++ b/src/schemas/api.py @@ -552,11 +552,7 @@ class ConclusionBatchCreate(BaseModel): class MessageSearchOptions(BaseModel): - query: Annotated[ - str, - BeforeValidator(strip_nul_bytes), - Field(min_length=1, description="Search query"), - ] + query: Annotated[str, Field(description="Search query")] filters: dict[str, Any] | None = Field( default=None, description="Filters to scope the search" ) @@ -567,15 +563,20 @@ class MessageSearchOptions(BaseModel): description="Number of results to return", ) - @field_validator("query", mode="after") + @field_validator("query", mode="before") @classmethod - def sanitize_query(cls, v: str) -> str: - if not v: + def sanitize_query(cls, v: Any) -> Any: + if not isinstance(v, str): + return v + + sanitized = cast(str, strip_nul_bytes(v)) + if v != "" and sanitized == "": raise PydanticCustomError( "string_too_short", "String should have at least 1 character", ) - return v + + return sanitized # --------------------------------------------------------------------------- diff --git a/tests/routes/test_peers.py b/tests/routes/test_peers.py index c8047f02..2f0617e8 100644 --- a/tests/routes/test_peers.py +++ b/tests/routes/test_peers.py @@ -689,6 +689,22 @@ def test_search_peer_empty_query( assert isinstance(data, list) +def test_search_peer_nul_only_query_rejected( + client: TestClient, sample_data: tuple[Workspace, Peer] +): + """Test peer search with a query that becomes empty after sanitization.""" + test_workspace, test_peer = sample_data + + response = client.post( + f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/search", + json={"query": "\x00", "limit": 10}, + ) + assert response.status_code == 422 + error = response.json()["detail"][0] + assert error["loc"] == ["body", "query"] + assert error["type"] == "string_too_short" + + def test_search_peer_nonexistent( client: TestClient, sample_data: tuple[Workspace, Peer] ): diff --git a/tests/test_schema_validations.py b/tests/test_schema_validations.py index c667b99c..4d73131f 100644 --- a/tests/test_schema_validations.py +++ b/tests/test_schema_validations.py @@ -8,6 +8,7 @@ from src.schemas import ( DialecticOptions, DocumentCreate, DocumentMetadata, + MessageSearchOptions, MessageCreate, ObservationInput, PeerCreate, @@ -217,6 +218,19 @@ class TestResolvedConfigurationMigration: class TestSanitizedRequiredFields: + def test_message_search_query_allows_literal_empty_string(self): + search = MessageSearchOptions.model_validate({"query": ""}) + + assert search.query == "" + + def test_message_search_query_rejects_nul_only_input(self): + with pytest.raises(ValidationError) as exc_info: + MessageSearchOptions.model_validate({"query": "\x00"}) + + error_dict = exc_info.value.errors()[0] + assert error_dict["loc"] == ("query",) + assert error_dict["type"] == "string_too_short" + def test_conclusion_content_rejects_nul_only_input(self): with pytest.raises(ValidationError) as exc_info: ConclusionCreate(