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