1+ """Unit tests for the /api/v2/ayah-claude endpoint."""
2+
3+ import pytest
4+ from fastapi .testclient import TestClient
5+ from unittest .mock import MagicMock , patch , mock_open
6+ import json
7+
8+
9+ @pytest .fixture
10+ def client ():
11+ """Create a test client for the FastAPI app."""
12+ from src .ansari .app .main_api import app
13+ return TestClient (app )
14+
15+
16+ @pytest .fixture
17+ def mock_settings ():
18+ """Mock settings with test API key."""
19+ with patch ("src.ansari.app.main_api.get_settings" ) as mock :
20+ settings = MagicMock ()
21+ settings .QURAN_DOT_COM_API_KEY .get_secret_value .return_value = "test-api-key"
22+ settings .AYAH_SYSTEM_PROMPT_FILE_NAME = "ayah_system_prompt.md"
23+ settings .MONGO_URL = "mongodb://test:27017"
24+ settings .MONGO_DB_NAME = "test_db"
25+ mock .return_value = settings
26+ yield settings
27+
28+
29+ @pytest .fixture
30+ def mock_db ():
31+ """Mock database for testing."""
32+ with patch ("src.ansari.app.main_api.AnsariDB" ) as mock_class :
33+ db_instance = MagicMock ()
34+ db_instance .get_quran_answer = MagicMock ()
35+ db_instance .store_quran_answer = MagicMock ()
36+ mock_class .return_value = db_instance
37+ yield db_instance
38+
39+
40+ @pytest .fixture
41+ def mock_ansari_claude ():
42+ """Mock AnsariClaude for testing."""
43+ with patch ("src.ansari.app.main_api.AnsariClaude" ) as mock :
44+ instance = MagicMock ()
45+ # Mock the generator response
46+ def mock_generator ():
47+ yield "This is a test response "
48+ yield "about the ayah "
49+ yield "with citations."
50+ instance .replace_message_history .return_value = mock_generator ()
51+ mock .return_value = instance
52+ yield mock
53+
54+
55+ class TestAyahClaudeEndpoint :
56+ """Test cases for the /api/v2/ayah-claude endpoint."""
57+
58+ def test_endpoint_exists (self , client ):
59+ """Test that the endpoint is registered."""
60+ response = client .post (
61+ "/api/v2/ayah-claude" ,
62+ json = {
63+ "surah" : 1 ,
64+ "ayah" : 1 ,
65+ "question" : "What is the meaning?" ,
66+ "apikey" : "wrong-key"
67+ }
68+ )
69+ # Should not return 404
70+ assert response .status_code != 404
71+
72+ def test_authentication_required (self , client , mock_settings ):
73+ """Test that API key authentication is enforced."""
74+ # Test with wrong API key
75+ response = client .post (
76+ "/api/v2/ayah-claude" ,
77+ json = {
78+ "surah" : 1 ,
79+ "ayah" : 1 ,
80+ "question" : "What is the meaning?" ,
81+ "apikey" : "wrong-api-key"
82+ }
83+ )
84+ assert response .status_code == 401
85+ assert response .json ()["detail" ] == "Unauthorized"
86+
87+ def test_successful_request_with_valid_key (self , client , mock_settings , mock_db , mock_ansari_claude ):
88+ """Test successful request with valid API key."""
89+ # Mock the file reading for system prompt
90+ with patch ("builtins.open" , mock_open (read_data = "Test system prompt" )):
91+ # Mock that no cached answer exists
92+ mock_db .get_quran_answer .return_value = None
93+
94+ response = client .post (
95+ "/api/v2/ayah-claude" ,
96+ json = {
97+ "surah" : 1 ,
98+ "ayah" : 1 ,
99+ "question" : "What is the meaning?" ,
100+ "apikey" : "test-api-key" ,
101+ "use_cache" : True
102+ }
103+ )
104+
105+ assert response .status_code == 200
106+ assert "response" in response .json ()
107+ assert response .json ()["response" ] == "This is a test response about the ayah with citations."
108+
109+ def test_cache_retrieval (self , client , mock_settings , mock_db ):
110+ """Test that cached answers are returned when available."""
111+ # Mock a cached answer
112+ cached_answer = "This is a cached response"
113+ mock_db .get_quran_answer .return_value = cached_answer
114+
115+ response = client .post (
116+ "/api/v2/ayah-claude" ,
117+ json = {
118+ "surah" : 1 ,
119+ "ayah" : 1 ,
120+ "question" : "What is the meaning?" ,
121+ "apikey" : "test-api-key" ,
122+ "use_cache" : True
123+ }
124+ )
125+
126+ assert response .status_code == 200
127+ assert response .json ()["response" ] == cached_answer
128+ # Verify that get_quran_answer was called
129+ mock_db .get_quran_answer .assert_called_once_with (1 , 1 , "What is the meaning?" )
130+
131+ def test_cache_disabled (self , client , mock_settings , mock_db , mock_ansari_claude ):
132+ """Test that cache is bypassed when use_cache is False."""
133+ with patch ("builtins.open" , mock_open (read_data = "Test system prompt" )):
134+ # Even if cache has an answer, it shouldn't be used
135+ mock_db .get_quran_answer .return_value = "Cached answer"
136+
137+ response = client .post (
138+ "/api/v2/ayah-claude" ,
139+ json = {
140+ "surah" : 2 ,
141+ "ayah" : 255 ,
142+ "question" : "Explain this verse" ,
143+ "apikey" : "test-api-key" ,
144+ "use_cache" : False
145+ }
146+ )
147+
148+ assert response .status_code == 200
149+ # Should not return cached answer
150+ assert response .json ()["response" ] != "Cached answer"
151+ # get_quran_answer should not be called when cache is disabled
152+ mock_db .get_quran_answer .assert_not_called ()
153+
154+ def test_augment_question_feature (self , client , mock_settings , mock_db , mock_ansari_claude ):
155+ """Test that augment_question adds enhancement instructions."""
156+ with patch ("builtins.open" , mock_open (read_data = "Test system prompt" )):
157+ mock_db .get_quran_answer .return_value = None
158+
159+ response = client .post (
160+ "/api/v2/ayah-claude" ,
161+ json = {
162+ "surah" : 3 ,
163+ "ayah" : 14 ,
164+ "question" : "What does this mean?" ,
165+ "apikey" : "test-api-key" ,
166+ "augment_question" : True ,
167+ "use_cache" : False
168+ }
169+ )
170+
171+ assert response .status_code == 200
172+ # Verify that AnsariClaude was called
173+ mock_ansari_claude .assert_called_once ()
174+ # Verify the message passed included enhancement
175+ call_args = mock_ansari_claude .return_value .replace_message_history .call_args
176+ messages = call_args [0 ][0 ]
177+ assert "search relevant tafsir sources" in messages [0 ]["content" ]
178+
179+ def test_database_storage (self , client , mock_settings , mock_db , mock_ansari_claude ):
180+ """Test that responses are stored in the database."""
181+ with patch ("builtins.open" , mock_open (read_data = "Test system prompt" )):
182+ mock_db .get_quran_answer .return_value = None
183+
184+ response = client .post (
185+ "/api/v2/ayah-claude" ,
186+ json = {
187+ "surah" : 4 ,
188+ "ayah" : 34 ,
189+ "question" : "Explain the context" ,
190+ "apikey" : "test-api-key" ,
191+ "use_cache" : True
192+ }
193+ )
194+
195+ assert response .status_code == 200
196+ # Verify that store_quran_answer was called
197+ mock_db .store_quran_answer .assert_called_once_with (
198+ 4 , 34 , "Explain the context" ,
199+ "This is a test response about the ayah with citations."
200+ )
201+
202+ def test_ayah_specific_system_prompt (self , client , mock_settings , mock_db , mock_ansari_claude ):
203+ """Test that ayah-specific system prompt is loaded."""
204+ system_prompt_content = "Special ayah system prompt"
205+
206+ with patch ("builtins.open" , mock_open (read_data = system_prompt_content )):
207+ mock_db .get_quran_answer .return_value = None
208+
209+ response = client .post (
210+ "/api/v2/ayah-claude" ,
211+ json = {
212+ "surah" : 5 ,
213+ "ayah" : 3 ,
214+ "question" : "What is the significance?" ,
215+ "apikey" : "test-api-key"
216+ }
217+ )
218+
219+ assert response .status_code == 200
220+ # Verify AnsariClaude was initialized with the system prompt
221+ mock_ansari_claude .assert_called_once ()
222+ call_args = mock_ansari_claude .call_args
223+ assert call_args [1 ]["system_prompt" ] == system_prompt_content
224+
225+ def test_ayah_id_calculation (self , client , mock_settings , mock_db , mock_ansari_claude ):
226+ """Test that ayah_id is calculated correctly for tafsir filtering."""
227+ with patch ("builtins.open" , mock_open (read_data = "Test system prompt" )):
228+ mock_db .get_quran_answer .return_value = None
229+
230+ # Test with Surah 2, Ayah 255 (Ayat al-Kursi)
231+ # Expected ayah_id = 2 * 1000 + 255 = 2255
232+ response = client .post (
233+ "/api/v2/ayah-claude" ,
234+ json = {
235+ "surah" : 2 ,
236+ "ayah" : 255 ,
237+ "question" : "Explain Ayat al-Kursi" ,
238+ "apikey" : "test-api-key"
239+ }
240+ )
241+
242+ assert response .status_code == 200
243+ # The ayah_id should be used in the context
244+ call_args = mock_ansari_claude .return_value .replace_message_history .call_args
245+ messages = call_args [0 ][0 ]
246+ assert "Surah 2, Ayah 255" in messages [0 ]["content" ]
247+
248+ def test_error_handling (self , client , mock_settings , mock_db ):
249+ """Test that errors are handled gracefully."""
250+ with patch ("src.ansari.app.main_api.AnsariClaude" ) as mock_claude :
251+ # Make AnsariClaude raise an exception
252+ mock_claude .side_effect = Exception ("Test error" )
253+ mock_db .get_quran_answer .return_value = None
254+
255+ with patch ("builtins.open" , mock_open (read_data = "Test system prompt" )):
256+ response = client .post (
257+ "/api/v2/ayah-claude" ,
258+ json = {
259+ "surah" : 1 ,
260+ "ayah" : 1 ,
261+ "question" : "Test question" ,
262+ "apikey" : "test-api-key"
263+ }
264+ )
265+
266+ assert response .status_code == 500
267+ assert response .json ()["detail" ] == "Internal server error"
268+
269+ def test_request_validation (self , client , mock_settings ):
270+ """Test that request validation works correctly."""
271+ # Missing required fields
272+ response = client .post (
273+ "/api/v2/ayah-claude" ,
274+ json = {
275+ "surah" : 1 ,
276+ # Missing ayah, question, and apikey
277+ }
278+ )
279+ assert response .status_code == 422 # Unprocessable Entity
280+
281+ # Invalid data types
282+ response = client .post (
283+ "/api/v2/ayah-claude" ,
284+ json = {
285+ "surah" : "not-a-number" ,
286+ "ayah" : 1 ,
287+ "question" : "Test" ,
288+ "apikey" : "test-key"
289+ }
290+ )
291+ assert response .status_code == 422
0 commit comments