jebin2 commited on
Commit
3f71f90
·
1 Parent(s): 08ca14b

feat: add audit service tests (14 tests, all passing)

Browse files

- Client/server event logging
- Request metadata extraction
- Error handling and edge cases
- Audit log queries by user and action type

Files changed (1) hide show
  1. tests/test_audit_service.py +413 -0
tests/test_audit_service.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Comprehensive Tests for Audit Service
3
+
4
+ Tests cover:
5
+ 1. Client event logging
6
+ 2. Server event logging
7
+ 3. Request metadata extraction
8
+ 4. Async logging
9
+ 5. Error handling
10
+ 6. AuditLog model integration
11
+
12
+ Uses mocked database and request objects.
13
+ """
14
+ import pytest
15
+ from datetime import datetime
16
+ from unittest.mock import MagicMock, AsyncMock, patch
17
+ from fastapi import Request
18
+
19
+
20
+ # ============================================================================
21
+ # 1. Client Event Logging Tests
22
+ # ============================================================================
23
+
24
+ class TestClientEventLogging:
25
+ """Test client-side event logging."""
26
+
27
+ @pytest.mark.asyncio
28
+ async def test_log_client_event_success(self, db_session):
29
+ """Log successful client event."""
30
+ from services.audit_service import AuditService
31
+ from core.models import AuditLog
32
+ from sqlalchemy import select
33
+
34
+ # Create mock request
35
+ mock_request = MagicMock()
36
+ mock_request.client.host = "192.168.1.1"
37
+ mock_request.headers.get.side_effect = lambda k, default=None: {
38
+ "user-agent": "Mozilla/5.0",
39
+ "referer": "https://example.com"
40
+ }.get(k.lower(), default)
41
+
42
+ # Log client event
43
+ await AuditService.log_event(
44
+ db=db_session,
45
+ log_type="client",
46
+ action="page_view",
47
+ status="success",
48
+ client_user_id="temp_123",
49
+ details={"page": "/home"},
50
+ request=mock_request
51
+ )
52
+
53
+ # Verify log was created
54
+ result = await db_session.execute(
55
+ select(AuditLog).where(AuditLog.action == "page_view")
56
+ )
57
+ log = result.scalar_one_or_none()
58
+
59
+ assert log is not None
60
+ assert log.log_type == "client"
61
+ assert log.action == "page_view"
62
+ assert log.status == "success"
63
+ assert log.client_user_id == "temp_123"
64
+ assert log.ip_address == "192.168.1.1"
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_log_client_event_with_user(self, db_session):
68
+ """Log client event with authenticated user."""
69
+ from services.audit_service import AuditService
70
+ from core.models import User, AuditLog
71
+ from sqlalchemy import select
72
+
73
+ # Create user
74
+ user = User(user_id="usr_audit", email="audit@example.com")
75
+ db_session.add(user)
76
+ await db_session.commit()
77
+
78
+ mock_request = MagicMock()
79
+ mock_request.client.host = "10.0.0.1"
80
+ mock_request.headers.get.return_value = None
81
+
82
+ # Log with user_id
83
+ await AuditService.log_event(
84
+ db=db_session,
85
+ log_type="client",
86
+ action="login",
87
+ status="success",
88
+ user_id=user.id,
89
+ client_user_id="temp_456",
90
+ request=mock_request
91
+ )
92
+
93
+ result = await db_session.execute(
94
+ select(AuditLog).where(AuditLog.user_id == user.id)
95
+ )
96
+ log = result.scalar_one_or_none()
97
+
98
+ assert log.user_id == user.id
99
+ assert log.client_user_id == "temp_456"
100
+
101
+
102
+ # ============================================================================
103
+ # 2. Server Event Logging Tests
104
+ # ============================================================================
105
+
106
+ class TestServerEventLogging:
107
+ """Test server-side event logging."""
108
+
109
+ @pytest.mark.asyncio
110
+ async def test_log_server_event(self, db_session):
111
+ """Log server event."""
112
+ from services.audit_service import AuditService
113
+ from core.models import User, AuditLog
114
+ from sqlalchemy import select
115
+
116
+ user = User(user_id="usr_server", email="server@example.com")
117
+ db_session.add(user)
118
+ await db_session.commit()
119
+
120
+ await AuditService.log_event(
121
+ db=db_session,
122
+ log_type="server",
123
+ action="credit_deduction",
124
+ status="success",
125
+ user_id=user.id,
126
+ details={"amount": 10, "reason": "video_generation"}
127
+ )
128
+
129
+ result = await db_session.execute(
130
+ select(AuditLog).where(AuditLog.action == "credit_deduction")
131
+ )
132
+ log = result.scalar_one_or_none()
133
+
134
+ assert log.log_type == "server"
135
+ assert log.details["amount"] == 10
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_log_server_failure(self, db_session):
139
+ """Log server failure event."""
140
+ from services.audit_service import AuditService
141
+ from core.models import AuditLog
142
+ from sqlalchemy import select
143
+
144
+ await AuditService.log_event(
145
+ db=db_session,
146
+ log_type="server",
147
+ action="job_processing",
148
+ status="failure",
149
+ error_message="API quota exceeded",
150
+ details={"job_id": "job_123"}
151
+ )
152
+
153
+ result = await db_session.execute(
154
+ select(AuditLog).where(AuditLog.status == "failure")
155
+ )
156
+ log = result.scalar_one_or_none()
157
+
158
+ assert log.error_message == "API quota exceeded"
159
+ assert log.status == "failure"
160
+
161
+
162
+ # ============================================================================
163
+ # 3. Request Metadata Extraction Tests
164
+ # ============================================================================
165
+
166
+ class TestRequestMetadata:
167
+ """Test extraction of request metadata."""
168
+
169
+ @pytest.mark.asyncio
170
+ async def test_extract_ip_address(self, db_session):
171
+ """Extract IP address from request."""
172
+ from services.audit_service import AuditService
173
+ from core.models import AuditLog
174
+ from sqlalchemy import select
175
+
176
+ mock_request = MagicMock()
177
+ mock_request.client.host = "203.0.113.42"
178
+ mock_request.headers.get.return_value = None
179
+
180
+ await AuditService.log_event(
181
+ db=db_session,
182
+ log_type="client",
183
+ action="api_call",
184
+ status="success",
185
+ request=mock_request
186
+ )
187
+
188
+ result = await db_session.execute(select(AuditLog).where(AuditLog.action == "api_call"))
189
+ log = result.scalar_one_or_none()
190
+
191
+ assert log.ip_address == "203.0.113.42"
192
+
193
+ @pytest.mark.asyncio
194
+ async def test_extract_user_agent(self, db_session):
195
+ """Extract user agent from request."""
196
+ from services.audit_service import AuditService
197
+ from core.models import AuditLog
198
+ from sqlalchemy import select
199
+
200
+ mock_request = MagicMock()
201
+ mock_request.client.host = "192.168.1.1"
202
+ mock_request.headers.get.side_effect = lambda k, default=None: {
203
+ "user-agent": "MyApp/1.0 (iOS)"
204
+ }.get(k.lower(), default)
205
+
206
+ await AuditService.log_event(
207
+ db=db_session,
208
+ log_type="client",
209
+ action="mobile_request",
210
+ status="success",
211
+ request=mock_request
212
+ )
213
+
214
+ result = await db_session.execute(select(AuditLog).where(AuditLog.action == "mobile_request"))
215
+ log = result.scalar_one_or_none()
216
+
217
+ assert "MyApp" in log.user_agent
218
+
219
+ @pytest.mark.asyncio
220
+ async def test_extract_referer(self, db_session):
221
+ """Extract referer from request."""
222
+ from services.audit_service import AuditService
223
+ from core.models import AuditLog
224
+ from sqlalchemy import select
225
+
226
+ mock_request = MagicMock()
227
+ mock_request.client.host = "192.168.1.1"
228
+ mock_request.headers.get.side_effect = lambda k, default=None: {
229
+ "referer": "https://example.com/previous-page"
230
+ }.get(k.lower(), default)
231
+
232
+ await AuditService.log_event(
233
+ db=db_session,
234
+ log_type="client",
235
+ action="navigation",
236
+ status="success",
237
+ request=mock_request
238
+ )
239
+
240
+ result = await db_session.execute(select(AuditLog).where(AuditLog.action == "navigation"))
241
+ log = result.scalar_one_or_none()
242
+
243
+ assert "example.com" in log.refer_url
244
+
245
+
246
+ # ============================================================================
247
+ # 4. Error Handling Tests
248
+ # ============================================================================
249
+
250
+ class TestAuditErrorHandling:
251
+ """Test error handling in audit service."""
252
+
253
+ @pytest.mark.asyncio
254
+ async def test_log_without_request(self, db_session):
255
+ """Can log events without request object."""
256
+ from services.audit_service import AuditService
257
+ from core.models import AuditLog
258
+ from sqlalchemy import select
259
+
260
+ # No request provided
261
+ await AuditService.log_event(
262
+ db=db_session,
263
+ log_type="server",
264
+ action="background_task",
265
+ status="success"
266
+ )
267
+
268
+ result = await db_session.execute(select(AuditLog).where(AuditLog.action == "background_task"))
269
+ log = result.scalar_one_or_none()
270
+
271
+ assert log is not None
272
+ assert log.ip_address is None # No request means no IP
273
+
274
+ @pytest.mark.asyncio
275
+ async def test_log_with_missing_request_client(self, db_session):
276
+ """Handle request without client attribute."""
277
+ from services.audit_service import AuditService
278
+ from core.models import AuditLog
279
+ from sqlalchemy import select
280
+
281
+ mock_request = MagicMock()
282
+ mock_request.client = None # No client
283
+ mock_request.headers.get.return_value = None
284
+
285
+ # Should not crash
286
+ await AuditService.log_event(
287
+ db=db_session,
288
+ log_type="client",
289
+ action="edge_case",
290
+ status="success",
291
+ request=mock_request
292
+ )
293
+
294
+ result = await db_session.execute(select(AuditLog).where(AuditLog.action == "edge_case"))
295
+ log = result.scalar_one_or_none()
296
+
297
+ assert log is not None
298
+
299
+
300
+ # ============================================================================
301
+ # 5. Details and Extra Data Tests
302
+ # ============================================================================
303
+
304
+ class TestAuditDetails:
305
+ """Test storing structured details in audit logs."""
306
+
307
+ @pytest.mark.asyncio
308
+ async def test_store_complex_details(self, db_session):
309
+ """Store complex JSON details."""
310
+ from services.audit_service import AuditService
311
+ from core.models import AuditLog
312
+ from sqlalchemy import select
313
+
314
+ complex_details = {
315
+ "user_action": "purchase",
316
+ "items": ["credits_100", "credits_500"],
317
+ "total_amount": 14900,
318
+ "metadata": {
319
+ "source": "web",
320
+ "campaign": "summer_sale"
321
+ }
322
+ }
323
+
324
+ await AuditService.log_event(
325
+ db=db_session,
326
+ log_type="server",
327
+ action="purchase_attempt",
328
+ status="success",
329
+ details=complex_details
330
+ )
331
+
332
+ result = await db_session.execute(select(AuditLog).where(AuditLog.action == "purchase_attempt"))
333
+ log = result.scalar_one_or_none()
334
+
335
+ assert log.details["total_amount"] == 14900
336
+ assert len(log.details["items"]) == 2
337
+ assert log.details["metadata"]["campaign"] == "summer_sale"
338
+
339
+
340
+ # ============================================================================
341
+ # 6. Audit Query Tests
342
+ # ============================================================================
343
+
344
+ class TestAuditQueries:
345
+ """Test querying audit logs."""
346
+
347
+ @pytest.mark.asyncio
348
+ async def test_query_by_user(self, db_session):
349
+ """Query audit logs by user."""
350
+ from services.audit_service import AuditService
351
+ from core.models import User, AuditLog
352
+ from sqlalchemy import select
353
+
354
+ user = User(user_id="usr_query", email="query@example.com")
355
+ db_session.add(user)
356
+ await db_session.commit()
357
+
358
+ # Create multiple logs for user
359
+ for i in range(3):
360
+ await AuditService.log_event(
361
+ db=db_session,
362
+ log_type="server",
363
+ action=f"action_{i}",
364
+ status="success",
365
+ user_id=user.id
366
+ )
367
+
368
+ # Query user's logs
369
+ result = await db_session.execute(
370
+ select(AuditLog).where(AuditLog.user_id == user.id)
371
+ )
372
+ logs = result.scalars().all()
373
+
374
+ assert len(logs) == 3
375
+
376
+ @pytest.mark.asyncio
377
+ async def test_query_by_action_type(self, db_session):
378
+ """Query logs by action type."""
379
+ from services.audit_service import AuditService
380
+ from core.models import AuditLog
381
+ from sqlalchemy import select
382
+
383
+ # Create different action types
384
+ await AuditService.log_event(
385
+ db=db_session,
386
+ log_type="client",
387
+ action="login",
388
+ status="success"
389
+ )
390
+ await AuditService.log_event(
391
+ db=db_session,
392
+ log_type="client",
393
+ action="login",
394
+ status="failure"
395
+ )
396
+ await AuditService.log_event(
397
+ db=db_session,
398
+ log_type="client",
399
+ action="logout",
400
+ status="success"
401
+ )
402
+
403
+ # Query only login actions
404
+ result = await db_session.execute(
405
+ select(AuditLog).where(AuditLog.action == "login")
406
+ )
407
+ logs = result.scalars().all()
408
+
409
+ assert len(logs) == 2
410
+
411
+
412
+ if __name__ == "__main__":
413
+ pytest.main([__file__, "-v"])