jebin2 commited on
Commit
0fe9140
·
1 Parent(s): a295e63

Add response inspector tests

Browse files
Files changed (1) hide show
  1. tests/test_response_inspector.py +294 -0
tests/test_response_inspector.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test suite for Response Inspector
3
+
4
+ Tests response analysis logic for determining credit actions:
5
+ - Confirm credits (successful operations)
6
+ - Refund credits (failed operations)
7
+ - Keep reserved (pending async operations)
8
+ """
9
+ import pytest
10
+ import json
11
+ from fastapi import Response
12
+
13
+ from services.credit_service.response_inspector import ResponseInspector
14
+
15
+
16
+ # =============================================================================
17
+ # Synchronous Endpoint Tests
18
+ # =============================================================================
19
+
20
+ def test_should_confirm_sync_success():
21
+ """Test confirmation for successful sync operation (200)."""
22
+ response = Response(content=json.dumps({"result": "success"}), status_code=200)
23
+ inspector = ResponseInspector()
24
+
25
+ assert inspector.should_confirm(response, "sync", {"result": "success"}) is True
26
+ assert inspector.should_refund(response, "sync", {"result": "success"}) is False
27
+
28
+
29
+ def test_should_confirm_sync_created():
30
+ """Test confirmation for sync operation (201)."""
31
+ response = Response(content=json.dumps({"id": "123"}), status_code=201)
32
+ inspector = ResponseInspector()
33
+
34
+ assert inspector.should_confirm(response, "sync", {"id": "123"}) is True
35
+
36
+
37
+ def test_should_refund_sync_client_error():
38
+ """Test refund for sync client error (400)."""
39
+ response = Response(
40
+ content=json.dumps({"detail": "Invalid request"}),
41
+ status_code=400
42
+ )
43
+ inspector = ResponseInspector()
44
+
45
+ assert inspector.should_confirm(response, "sync", {"detail": "Invalid request"}) is False
46
+ assert inspector.should_refund(response, "sync", {"detail": "Invalid request"}) is True
47
+
48
+
49
+ def test_should_refund_sync_server_error():
50
+ """Test refund for sync server error (500)."""
51
+ response = Response(
52
+ content=json.dumps({"detail": "Internal error"}),
53
+ status_code=500
54
+ )
55
+ inspector = ResponseInspector()
56
+
57
+ assert inspector.should_refund(response, "sync", {"detail": "Internal error"}) is True
58
+
59
+
60
+ # =============================================================================
61
+ # Asynchronous Endpoint Tests - Job Creation
62
+ # =============================================================================
63
+
64
+ def test_async_job_creation_success():
65
+ """Test async job creation - should keep reserved."""
66
+ response = Response(
67
+ content=json.dumps({"job_id": "job_123", "status": "queued"}),
68
+ status_code=200
69
+ )
70
+ inspector = ResponseInspector()
71
+ response_data = {"job_id": "job_123", "status": "queued"}
72
+
73
+ # Job created successfully, but not complete yet
74
+ assert inspector.should_confirm(response, "async", response_data) is False
75
+ assert inspector.should_refund(response, "async", response_data) is False
76
+
77
+
78
+ def test_async_job_creation_failure():
79
+ """Test async job creation failure - should refund."""
80
+ response = Response(
81
+ content=json.dumps({"detail": "Validation failed"}),
82
+ status_code=400
83
+ )
84
+ inspector = ResponseInspector()
85
+ response_data = {"detail": "Validation failed"}
86
+
87
+ # Job creation failed, refund credits
88
+ assert inspector.should_confirm(response, "async", response_data) is False
89
+ assert inspector.should_refund(response, "async", response_data) is True
90
+
91
+
92
+ # =============================================================================
93
+ # Asynchronous Endpoint Tests - Status Checks
94
+ # =============================================================================
95
+
96
+ def test_async_status_completed():
97
+ """Test async job status check - completed."""
98
+ response = Response(
99
+ content=json.dumps({"job_id": "job_123", "status": "completed", "result": "..."}),
100
+ status_code=200
101
+ )
102
+ inspector = ResponseInspector()
103
+ response_data = {"job_id": "job_123", "status": "completed", "result": "..."}
104
+
105
+ # Job completed, confirm credits
106
+ assert inspector.should_confirm(response, "async", response_data) is True
107
+ assert inspector.should_refund(response, "async", response_data) is False
108
+
109
+
110
+ def test_async_status_processing():
111
+ """Test async job status check - still processing."""
112
+ response = Response(
113
+ content=json.dumps({"job_id": "job_123", "status": "processing"}),
114
+ status_code=200
115
+ )
116
+ inspector = ResponseInspector()
117
+ response_data = {"job_id": "job_123", "status": "processing"}
118
+
119
+ # Job still processing, keep reserved
120
+ assert inspector.should_confirm(response, "async", response_data) is False
121
+ assert inspector.should_refund(response, "async", response_data) is False
122
+
123
+
124
+ def test_async_status_queued():
125
+ """Test async job status check - still queued."""
126
+ response = Response(
127
+ content=json.dumps({"job_id": "job_123", "status": "queued", "position": 5}),
128
+ status_code=200
129
+ )
130
+ inspector = ResponseInspector()
131
+ response_data = {"job_id": "job_123", "status": "queued", "position": 5}
132
+
133
+ # Job still queued, keep reserved
134
+ assert inspector.should_confirm(response, "async", response_data) is False
135
+ assert inspector.should_refund(response, "async", response_data) is False
136
+
137
+
138
+ def test_async_status_failed_refundable():
139
+ """Test async job status check - failed with refundable error."""
140
+ response = Response(
141
+ content=json.dumps({
142
+ "job_id": "job_123",
143
+ "status": "failed",
144
+ "error_message": "API_KEY_INVALID - The API key is invalid"
145
+ }),
146
+ status_code=200
147
+ )
148
+ inspector = ResponseInspector()
149
+ response_data = {
150
+ "job_id": "job_123",
151
+ "status": "failed",
152
+ "error_message": "API_KEY_INVALID - The API key is invalid"
153
+ }
154
+
155
+ # Job failed with refundable error, refund credits
156
+ assert inspector.should_confirm(response, "async", response_data) is False
157
+ assert inspector.should_refund(response, "async", response_data) is True
158
+
159
+
160
+ def test_async_status_failed_non_refundable():
161
+ """Test async job status check - failed with non-refundable error."""
162
+ response = Response(
163
+ content=json.dumps({
164
+ "job_id": "job_123",
165
+ "status": "failed",
166
+ "error_message": "SAFETY: Content blocked by safety filters"
167
+ }),
168
+ status_code=200
169
+ )
170
+ inspector = ResponseInspector()
171
+ response_data = {
172
+ "job_id": "job_123",
173
+ "status": "failed",
174
+ "error_message": "SAFETY: Content blocked by safety filters"
175
+ }
176
+
177
+ # Job failed with non-refundable error, confirm credits (keep deducted)
178
+ assert inspector.should_confirm(response, "async", response_data) is False
179
+ assert inspector.should_refund(response, "async", response_data) is False
180
+
181
+
182
+ # =============================================================================
183
+ # Refund Reason Tests
184
+ # =============================================================================
185
+
186
+ def test_get_refund_reason_server_error():
187
+ """Test refund reason for server error."""
188
+ response = Response(content="", status_code=500)
189
+ inspector = ResponseInspector()
190
+
191
+ reason = inspector.get_refund_reason(response, None)
192
+ assert "Server error: 500" == reason
193
+
194
+
195
+ def test_get_refund_reason_client_error():
196
+ """Test refund reason for client error."""
197
+ response = Response(
198
+ content=json.dumps({"detail": "Invalid input"}),
199
+ status_code=400
200
+ )
201
+ inspector = ResponseInspector()
202
+ response_data = {"detail": "Invalid input"}
203
+
204
+ reason = inspector.get_refund_reason(response, response_data)
205
+ assert "Request error: Invalid input" == reason
206
+
207
+
208
+ def test_get_refund_reason_job_failure():
209
+ """Test refund reason for job failure."""
210
+ response = Response(content="", status_code=200)
211
+ inspector = ResponseInspector()
212
+ response_data = {"error_message": "API timeout after 60 seconds"}
213
+
214
+ reason = inspector.get_refund_reason(response, response_data)
215
+ assert "Job failed" in reason
216
+ assert "API timeout" in reason
217
+
218
+
219
+ def test_get_refund_reason_unknown():
220
+ """Test refund reason when unknown."""
221
+ response = Response(content="", status_code=200)
222
+ inspector = ResponseInspector()
223
+
224
+ reason = inspector.get_refund_reason(response, None)
225
+ assert reason == "Unknown error"
226
+
227
+
228
+ # =============================================================================
229
+ # Response Body Parsing Tests
230
+ # =============================================================================
231
+
232
+ def test_parse_response_body_valid_json():
233
+ """Test parsing valid JSON response body."""
234
+ body = b'{"key": "value", "number": 123}'
235
+ inspector = ResponseInspector()
236
+
237
+ parsed = inspector.parse_response_body(body)
238
+ assert parsed == {"key": "value", "number": 123}
239
+
240
+
241
+ def test_parse_response_body_invalid_json():
242
+ """Test parsing invalid JSON response body."""
243
+ body = b'not json'
244
+ inspector = ResponseInspector()
245
+
246
+ parsed = inspector.parse_response_body(body)
247
+ assert parsed is None
248
+
249
+
250
+ def test_parse_response_body_empty():
251
+ """Test parsing empty response body."""
252
+ body = b''
253
+ inspector = ResponseInspector()
254
+
255
+ parsed = inspector.parse_response_body(body)
256
+ assert parsed is None
257
+
258
+
259
+ # =============================================================================
260
+ # Edge Cases
261
+ # =============================================================================
262
+
263
+ def test_free_endpoint():
264
+ """Test free endpoint (no credit cost)."""
265
+ response = Response(content=json.dumps({"result": "ok"}), status_code=200)
266
+ inspector = ResponseInspector()
267
+
268
+ # Free endpoints shouldn't trigger credit actions
269
+ # (handled by middleware checking cost=0, but inspector should be safe)
270
+ assert inspector.should_confirm(response, "free", {"result": "ok"}) is False
271
+
272
+
273
+ def test_async_missing_status_field():
274
+ """Test async response missing status field."""
275
+ response = Response(
276
+ content=json.dumps({"job_id": "abc", "message": "Processing"}),
277
+ status_code=200
278
+ )
279
+ inspector = ResponseInspector()
280
+ response_data = {"job_id": "abc", "message": "Processing"}
281
+
282
+ # No status field, should keep reserved
283
+ assert inspector.should_confirm(response, "async", response_data) is False
284
+ assert inspector.should_refund(response, "async", response_data) is False
285
+
286
+
287
+ def test_async_none_response_data():
288
+ """Test async with None response data."""
289
+ response = Response(content=b"", status_code=500)
290
+ inspector = ResponseInspector()
291
+
292
+ # Server error with no parseable response
293
+ assert inspector.should_confirm(response, "async", None) is False
294
+ assert inspector.should_refund(response, "async", None) is True