jebin2 commited on
Commit
5ecbd0f
·
1 Parent(s): afe6505

worker test

Browse files
Files changed (1) hide show
  1. tests/test_payments_router.py +525 -0
tests/test_payments_router.py ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Rigorous Tests for Payments Router.
3
+
4
+ Tests cover:
5
+ 1. Helper functions (generate_transaction_id, update_verified_by, process_successful_payment)
6
+ 2. GET /packages endpoint
7
+ 3. POST /create-order endpoint
8
+ 4. POST /verify endpoint
9
+ 5. POST /webhook/razorpay endpoint
10
+ 6. GET /history endpoint
11
+
12
+ Uses mocked Razorpay service and database.
13
+ """
14
+ import pytest
15
+ import json
16
+ from datetime import datetime
17
+ from unittest.mock import patch, MagicMock, AsyncMock
18
+ from fastapi.testclient import TestClient
19
+
20
+
21
+ # =============================================================================
22
+ # 1. Helper Function Tests
23
+ # =============================================================================
24
+
25
+ class TestHelperFunctions:
26
+ """Test helper functions in payments router."""
27
+
28
+ def test_generate_transaction_id_format(self):
29
+ """Generated transaction IDs have correct format."""
30
+ from routers.payments import generate_transaction_id
31
+
32
+ txn_id = generate_transaction_id()
33
+
34
+ assert txn_id.startswith("txn_")
35
+ assert len(txn_id) == 20 # "txn_" + 16 hex chars
36
+
37
+ def test_generate_transaction_id_unique(self):
38
+ """Each generated ID is unique."""
39
+ from routers.payments import generate_transaction_id
40
+
41
+ ids = [generate_transaction_id() for _ in range(100)]
42
+
43
+ assert len(set(ids)) == 100 # All unique
44
+
45
+ def test_update_verified_by_client_first(self):
46
+ """First verification by client sets verified_by to 'client'."""
47
+ from routers.payments import update_verified_by
48
+
49
+ transaction = MagicMock()
50
+ transaction.verified_by = None
51
+
52
+ changed = update_verified_by(transaction, "client")
53
+
54
+ assert transaction.verified_by == "client"
55
+ assert changed == True
56
+
57
+ def test_update_verified_by_webhook_first(self):
58
+ """First verification by webhook sets verified_by to 'webhook'."""
59
+ from routers.payments import update_verified_by
60
+
61
+ transaction = MagicMock()
62
+ transaction.verified_by = None
63
+
64
+ changed = update_verified_by(transaction, "webhook")
65
+
66
+ assert transaction.verified_by == "webhook"
67
+ assert changed == True
68
+
69
+ def test_update_verified_by_both_sources(self):
70
+ """Second verification from other source sets verified_by to 'both'."""
71
+ from routers.payments import update_verified_by
72
+
73
+ # Client first, then webhook
74
+ transaction = MagicMock()
75
+ transaction.verified_by = "client"
76
+
77
+ changed = update_verified_by(transaction, "webhook")
78
+
79
+ assert transaction.verified_by == "both"
80
+ assert changed == True
81
+
82
+ def test_update_verified_by_same_source_no_change(self):
83
+ """Same source verification doesn't change value."""
84
+ from routers.payments import update_verified_by
85
+
86
+ transaction = MagicMock()
87
+ transaction.verified_by = "client"
88
+
89
+ changed = update_verified_by(transaction, "client")
90
+
91
+ assert transaction.verified_by == "client"
92
+ assert changed == False
93
+
94
+
95
+ # =============================================================================
96
+ # 2. GET /packages Tests
97
+ # =============================================================================
98
+
99
+ class TestGetPackages:
100
+ """Test GET /packages endpoint."""
101
+
102
+ def test_list_packages_returns_all(self):
103
+ """List all available packages."""
104
+ from routers.payments import router
105
+ from fastapi import FastAPI
106
+
107
+ app = FastAPI()
108
+ app.include_router(router)
109
+ client = TestClient(app)
110
+
111
+ response = client.get("/payments/packages")
112
+
113
+ assert response.status_code == 200
114
+ data = response.json()
115
+ assert "packages" in data
116
+ assert len(data["packages"]) >= 3 # At least starter, standard, pro
117
+
118
+ def test_packages_have_required_fields(self):
119
+ """Each package has all required fields."""
120
+ from routers.payments import router
121
+ from fastapi import FastAPI
122
+
123
+ app = FastAPI()
124
+ app.include_router(router)
125
+ client = TestClient(app)
126
+
127
+ response = client.get("/payments/packages")
128
+ data = response.json()
129
+
130
+ for pkg in data["packages"]:
131
+ assert "id" in pkg
132
+ assert "name" in pkg
133
+ assert "credits" in pkg
134
+ assert "amount_paise" in pkg
135
+ assert "currency" in pkg
136
+
137
+
138
+ # =============================================================================
139
+ # 3. POST /create-order Tests
140
+ # =============================================================================
141
+
142
+ class TestCreateOrder:
143
+ """Test POST /create-order endpoint."""
144
+
145
+ def test_create_order_requires_auth(self):
146
+ """Create order requires authentication."""
147
+ from routers.payments import router
148
+ from fastapi import FastAPI
149
+
150
+ app = FastAPI()
151
+ app.include_router(router)
152
+ client = TestClient(app)
153
+
154
+ response = client.post(
155
+ "/payments/create-order",
156
+ json={"package_id": "starter"}
157
+ )
158
+
159
+ # Should fail with auth error (401 or 403)
160
+ assert response.status_code in [401, 403, 422]
161
+
162
+ def test_create_order_invalid_package(self):
163
+ """Reject invalid package_id."""
164
+ from routers.payments import router
165
+ from fastapi import FastAPI
166
+ from dependencies import get_current_user
167
+
168
+ app = FastAPI()
169
+
170
+ # Mock authenticated user
171
+ mock_user = MagicMock()
172
+ mock_user.user_id = "test-user"
173
+ mock_user.credits = 100
174
+
175
+ app.dependency_overrides[get_current_user] = lambda: mock_user
176
+ app.include_router(router)
177
+ client = TestClient(app)
178
+
179
+ with patch('routers.payments.is_razorpay_configured', return_value=True):
180
+ response = client.post(
181
+ "/payments/create-order",
182
+ json={"package_id": "invalid_package"}
183
+ )
184
+
185
+ assert response.status_code == 400
186
+ assert "Invalid package" in response.json()["detail"]
187
+
188
+ def test_create_order_razorpay_not_configured(self):
189
+ """Return 503 if Razorpay not configured."""
190
+ from routers.payments import router
191
+ from fastapi import FastAPI
192
+ from dependencies import get_current_user
193
+
194
+ app = FastAPI()
195
+
196
+ mock_user = MagicMock()
197
+ mock_user.user_id = "test-user"
198
+
199
+ app.dependency_overrides[get_current_user] = lambda: mock_user
200
+ app.include_router(router)
201
+ client = TestClient(app)
202
+
203
+ with patch('routers.payments.is_razorpay_configured', return_value=False):
204
+ response = client.post(
205
+ "/payments/create-order",
206
+ json={"package_id": "starter"}
207
+ )
208
+
209
+ assert response.status_code == 503
210
+ assert "not configured" in response.json()["detail"]
211
+
212
+
213
+ # =============================================================================
214
+ # 4. POST /verify Tests
215
+ # =============================================================================
216
+
217
+ class TestVerifyPayment:
218
+ """Test POST /verify endpoint."""
219
+
220
+ def test_verify_requires_auth(self):
221
+ """Verify requires authentication."""
222
+ from routers.payments import router
223
+ from fastapi import FastAPI
224
+
225
+ app = FastAPI()
226
+ app.include_router(router)
227
+ client = TestClient(app)
228
+
229
+ response = client.post(
230
+ "/payments/verify",
231
+ json={
232
+ "razorpay_order_id": "order_123",
233
+ "razorpay_payment_id": "pay_123",
234
+ "razorpay_signature": "sig_123"
235
+ }
236
+ )
237
+
238
+ assert response.status_code in [401, 403, 422]
239
+
240
+ def test_verify_transaction_not_found(self):
241
+ """Return 404 for unknown transaction."""
242
+ from routers.payments import router
243
+ from fastapi import FastAPI
244
+ from dependencies import get_current_user
245
+ from core.database import get_db
246
+
247
+ app = FastAPI()
248
+
249
+ mock_user = MagicMock()
250
+ mock_user.user_id = "test-user"
251
+ mock_user.credits = 100
252
+
253
+ # Mock database that returns no transaction
254
+ async def mock_get_db():
255
+ mock_db = AsyncMock()
256
+ mock_result = MagicMock()
257
+ mock_result.scalar_one_or_none.return_value = None
258
+ mock_db.execute.return_value = mock_result
259
+ yield mock_db
260
+
261
+ app.dependency_overrides[get_current_user] = lambda: mock_user
262
+ app.dependency_overrides[get_db] = mock_get_db
263
+ app.include_router(router)
264
+ client = TestClient(app)
265
+
266
+ with patch('routers.payments.get_razorpay_service') as mock_service:
267
+ mock_service.return_value = MagicMock()
268
+
269
+ response = client.post(
270
+ "/payments/verify",
271
+ json={
272
+ "razorpay_order_id": "order_unknown",
273
+ "razorpay_payment_id": "pay_123",
274
+ "razorpay_signature": "sig_123"
275
+ }
276
+ )
277
+
278
+ assert response.status_code == 404
279
+ assert "not found" in response.json()["detail"].lower()
280
+
281
+
282
+ # =============================================================================
283
+ # 5. POST /webhook/razorpay Tests
284
+ # =============================================================================
285
+
286
+ class TestWebhook:
287
+ """Test POST /webhook/razorpay endpoint."""
288
+
289
+ def test_webhook_requires_signature(self):
290
+ """Webhook requires X-Razorpay-Signature header."""
291
+ from routers.payments import router
292
+ from fastapi import FastAPI
293
+ from core.database import get_db
294
+
295
+ app = FastAPI()
296
+
297
+ async def mock_get_db():
298
+ mock_db = AsyncMock()
299
+ yield mock_db
300
+
301
+ app.dependency_overrides[get_db] = mock_get_db
302
+ app.include_router(router)
303
+ client = TestClient(app)
304
+
305
+ response = client.post(
306
+ "/payments/webhook/razorpay",
307
+ json={"event": "payment.captured"}
308
+ )
309
+
310
+ assert response.status_code == 401
311
+ assert "signature" in response.json()["detail"].lower()
312
+
313
+ def test_webhook_rejects_invalid_signature(self):
314
+ """Webhook rejects invalid signature."""
315
+ from routers.payments import router
316
+ from fastapi import FastAPI
317
+ from core.database import get_db
318
+
319
+ app = FastAPI()
320
+
321
+ async def mock_get_db():
322
+ mock_db = AsyncMock()
323
+ yield mock_db
324
+
325
+ app.dependency_overrides[get_db] = mock_get_db
326
+ app.include_router(router)
327
+ client = TestClient(app)
328
+
329
+ with patch('routers.payments.get_razorpay_service') as mock_service:
330
+ service_instance = MagicMock()
331
+ service_instance.verify_webhook_signature.return_value = False
332
+ mock_service.return_value = service_instance
333
+
334
+ response = client.post(
335
+ "/payments/webhook/razorpay",
336
+ json={"event": "payment.captured"},
337
+ headers={"X-Razorpay-Signature": "invalid-sig"}
338
+ )
339
+
340
+ assert response.status_code == 401
341
+ assert "invalid" in response.json()["detail"].lower()
342
+
343
+ def test_webhook_accepts_valid_signature(self):
344
+ """Webhook accepts valid signature."""
345
+ from routers.payments import router
346
+ from fastapi import FastAPI
347
+ from core.database import get_db
348
+
349
+ app = FastAPI()
350
+
351
+ async def mock_get_db():
352
+ mock_db = AsyncMock()
353
+ yield mock_db
354
+
355
+ app.dependency_overrides[get_db] = mock_get_db
356
+ app.include_router(router)
357
+ client = TestClient(app)
358
+
359
+ with patch('routers.payments.get_razorpay_service') as mock_service:
360
+ service_instance = MagicMock()
361
+ service_instance.verify_webhook_signature.return_value = True
362
+ mock_service.return_value = service_instance
363
+
364
+ response = client.post(
365
+ "/payments/webhook/razorpay",
366
+ json={"event": "unknown.event"},
367
+ headers={"X-Razorpay-Signature": "valid-sig"}
368
+ )
369
+
370
+ assert response.status_code == 200
371
+ assert response.json()["status"] == "ok"
372
+
373
+
374
+ # =============================================================================
375
+ # 6. GET /history Tests
376
+ # =============================================================================
377
+
378
+ class TestPaymentHistory:
379
+ """Test GET /history endpoint."""
380
+
381
+ def test_history_requires_auth(self):
382
+ """History requires authentication."""
383
+ from routers.payments import router
384
+ from fastapi import FastAPI
385
+
386
+ app = FastAPI()
387
+ app.include_router(router)
388
+ client = TestClient(app)
389
+
390
+ response = client.get("/payments/history")
391
+
392
+ assert response.status_code in [401, 403, 422]
393
+
394
+ def test_history_returns_empty_list(self):
395
+ """History returns empty list for user with no transactions."""
396
+ from routers.payments import router
397
+ from fastapi import FastAPI
398
+ from dependencies import get_current_user
399
+ from core.database import get_db
400
+
401
+ app = FastAPI()
402
+
403
+ mock_user = MagicMock()
404
+ mock_user.user_id = "test-user"
405
+
406
+ async def mock_get_db():
407
+ mock_db = AsyncMock()
408
+
409
+ # Mock count query
410
+ mock_count_result = MagicMock()
411
+ mock_count_result.scalar.return_value = 0
412
+
413
+ # Mock transactions query
414
+ mock_txn_result = MagicMock()
415
+ mock_txn_result.scalars.return_value.all.return_value = []
416
+
417
+ mock_db.execute.side_effect = [mock_count_result, mock_txn_result]
418
+ yield mock_db
419
+
420
+ app.dependency_overrides[get_current_user] = lambda: mock_user
421
+ app.dependency_overrides[get_db] = mock_get_db
422
+ app.include_router(router)
423
+ client = TestClient(app)
424
+
425
+ response = client.get("/payments/history")
426
+
427
+ assert response.status_code == 200
428
+ data = response.json()
429
+ assert data["transactions"] == []
430
+ assert data["total_count"] == 0
431
+
432
+ def test_history_pagination_params(self):
433
+ """History respects pagination parameters."""
434
+ from routers.payments import router
435
+ from fastapi import FastAPI
436
+ from dependencies import get_current_user
437
+ from core.database import get_db
438
+
439
+ app = FastAPI()
440
+
441
+ mock_user = MagicMock()
442
+ mock_user.user_id = "test-user"
443
+
444
+ async def mock_get_db():
445
+ mock_db = AsyncMock()
446
+ mock_count_result = MagicMock()
447
+ mock_count_result.scalar.return_value = 50
448
+ mock_txn_result = MagicMock()
449
+ mock_txn_result.scalars.return_value.all.return_value = []
450
+ mock_db.execute.side_effect = [mock_count_result, mock_txn_result]
451
+ yield mock_db
452
+
453
+ app.dependency_overrides[get_current_user] = lambda: mock_user
454
+ app.dependency_overrides[get_db] = mock_get_db
455
+ app.include_router(router)
456
+ client = TestClient(app)
457
+
458
+ response = client.get("/payments/history?page=2&limit=10")
459
+
460
+ assert response.status_code == 200
461
+ data = response.json()
462
+ assert data["page"] == 2
463
+ assert data["limit"] == 10
464
+ assert data["total_count"] == 50
465
+
466
+
467
+ # =============================================================================
468
+ # 7. Response Model Tests
469
+ # =============================================================================
470
+
471
+ class TestResponseModels:
472
+ """Test response model schemas."""
473
+
474
+ def test_package_response_model(self):
475
+ """PackageResponse model validates correctly."""
476
+ from routers.payments import PackageResponse
477
+
478
+ pkg = PackageResponse(
479
+ id="starter",
480
+ name="Starter",
481
+ credits=100,
482
+ amount_paise=9900,
483
+ amount_rupees=99.0,
484
+ currency="INR"
485
+ )
486
+
487
+ assert pkg.id == "starter"
488
+ assert pkg.credits == 100
489
+
490
+ def test_verify_payment_response_model(self):
491
+ """VerifyPaymentResponse model validates correctly."""
492
+ from routers.payments import VerifyPaymentResponse
493
+
494
+ resp = VerifyPaymentResponse(
495
+ success=True,
496
+ message="Payment successful",
497
+ transaction_id="txn_abc123",
498
+ credits_added=100,
499
+ new_balance=500
500
+ )
501
+
502
+ assert resp.success == True
503
+ assert resp.credits_added == 100
504
+
505
+ def test_payment_history_item_model(self):
506
+ """PaymentHistoryItem model validates correctly."""
507
+ from routers.payments import PaymentHistoryItem
508
+
509
+ item = PaymentHistoryItem(
510
+ transaction_id="txn_123",
511
+ package_id="starter",
512
+ credits_amount=100,
513
+ amount_paise=9900,
514
+ currency="INR",
515
+ status="paid",
516
+ gateway="razorpay",
517
+ created_at="2024-01-01T00:00:00"
518
+ )
519
+
520
+ assert item.transaction_id == "txn_123"
521
+ assert item.status == "paid"
522
+
523
+
524
+ if __name__ == "__main__":
525
+ pytest.main([__file__, "-v"])