test_search.py (5837B)
1 """Tests for the search endpoint.""" 2 3 from __future__ import annotations 4 5 from unittest.mock import AsyncMock, patch 6 import pytest 7 from fastapi import FastAPI 8 from fastapi.testclient import TestClient 9 10 from app.routes.search import router as search_router 11 12 def _test_app() -> FastAPI: 13 """Build a minimal app with search router, no auth middleware.""" 14 app = FastAPI() 15 app.include_router(search_router) 16 return app 17 18 def _mock_client(**overrides) -> AsyncMock: 19 client = AsyncMock() 20 client.search_messages = AsyncMock(return_value={"results": []}) 21 for name, mock in overrides.items(): 22 setattr(client, name, mock) 23 return client 24 25 26 class TestSearchEmpty: 27 def test_empty_query_returns_empty(self) -> None: 28 mock_client = _mock_client() 29 app = _test_app() 30 with patch("app.routes.search.get_hermes_client", return_value=mock_client): 31 with TestClient(app) as tc: 32 resp = tc.get("/api/search?q=") 33 assert resp.status_code == 200 34 assert resp.json() == {"results": []} 35 36 def test_no_query_param_returns_empty(self) -> None: 37 mock_client = _mock_client() 38 app = _test_app() 39 with patch("app.routes.search.get_hermes_client", return_value=mock_client): 40 with TestClient(app) as tc: 41 resp = tc.get("/api/search") 42 assert resp.status_code == 200 43 assert resp.json() == {"results": []} 44 45 def test_whitespace_query_returns_empty(self) -> None: 46 mock_client = _mock_client() 47 app = _test_app() 48 with patch("app.routes.search.get_hermes_client", return_value=mock_client): 49 with TestClient(app) as tc: 50 resp = tc.get("/api/search?q=+") 51 assert resp.status_code == 200 52 assert resp.json() == {"results": []} 53 54 def test_hermes_not_called_on_empty_query(self) -> None: 55 """Empty queries should not call the Hermes client.""" 56 mock_client = _mock_client() 57 app = _test_app() 58 with patch("app.routes.search.get_hermes_client", return_value=mock_client): 59 with TestClient(app) as tc: 60 tc.get("/api/search?q=") 61 mock_client.search_messages.assert_not_awaited() 62 63 64 class TestSearchResults: 65 def test_search_returns_grouped_results(self) -> None: 66 mock_client = _mock_client( 67 search_messages=AsyncMock(return_value={ 68 "results": [ 69 { 70 "session_id": "s1", 71 "session_name": "Session One", 72 "messages": [ 73 {"id": "m1", "content": "test message", "role": "user"}, 74 {"id": "m2", "content": "test reply", "role": "assistant"}, 75 ], 76 }, 77 { 78 "session_id": "s2", 79 "session_name": "Session Two", 80 "messages": [ 81 {"id": "m3", "content": "another test", "role": "user"}, 82 ], 83 }, 84 ], 85 }) 86 ) 87 app = _test_app() 88 with patch("app.routes.search.get_hermes_client", return_value=mock_client): 89 with TestClient(app) as tc: 90 resp = tc.get("/api/search?q=test") 91 assert resp.status_code == 200 92 data = resp.json() 93 assert "results" in data 94 assert len(data["results"]) == 2 95 # First group 96 assert data["results"][0]["session_id"] == "s1" 97 assert data["results"][0]["session_name"] == "Session One" 98 assert len(data["results"][0]["messages"]) == 2 99 assert data["results"][0]["messages"][0]["id"] == "m1" 100 assert data["results"][0]["messages"][0]["content"] == "test message" 101 assert data["results"][0]["messages"][0]["role"] == "user" 102 assert data["results"][0]["messages"][1]["id"] == "m2" 103 assert data["results"][0]["messages"][1]["role"] == "assistant" 104 # Second group 105 assert data["results"][1]["session_id"] == "s2" 106 assert data["results"][1]["session_name"] == "Session Two" 107 assert len(data["results"][1]["messages"]) == 1 108 assert data["results"][1]["messages"][0]["id"] == "m3" 109 assert data["results"][1]["messages"][0]["content"] == "another test" 110 111 def test_search_with_no_results(self) -> None: 112 mock_client = _mock_client( 113 search_messages=AsyncMock(return_value={"results": []}) 114 ) 115 app = _test_app() 116 with patch("app.routes.search.get_hermes_client", return_value=mock_client): 117 with TestClient(app) as tc: 118 resp = tc.get("/api/search?q=nonexistent") 119 assert resp.status_code == 200 120 assert resp.json() == {"results": []} 121 122 def test_search_api_error_returns_empty(self) -> None: 123 mock_client = _mock_client( 124 search_messages=AsyncMock(side_effect=Exception("timeout")) 125 ) 126 app = _test_app() 127 with patch("app.routes.search.get_hermes_client", return_value=mock_client): 128 with TestClient(app) as tc: 129 resp = tc.get("/api/search?q=test") 130 assert resp.status_code == 200 131 assert resp.json() == {"results": []} 132 133 def test_search_hermes_called_with_query(self) -> None: 134 """Verify the search query is passed to Hermes.""" 135 mock_client = _mock_client( 136 search_messages=AsyncMock(return_value={"results": []}) 137 ) 138 app = _test_app() 139 with patch("app.routes.search.get_hermes_client", return_value=mock_client): 140 with TestClient(app) as tc: 141 tc.get("/api/search?q=my%20search%20query") 142 mock_client.search_messages.assert_awaited_once_with("my search query")