setu-trafficmonitor 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. setu_trafficmonitor-2.0.0.dist-info/LICENSE +21 -0
  2. setu_trafficmonitor-2.0.0.dist-info/METADATA +401 -0
  3. setu_trafficmonitor-2.0.0.dist-info/RECORD +42 -0
  4. setu_trafficmonitor-2.0.0.dist-info/WHEEL +5 -0
  5. setu_trafficmonitor-2.0.0.dist-info/top_level.txt +1 -0
  6. trafficmonitor/__init__.py +11 -0
  7. trafficmonitor/admin.py +217 -0
  8. trafficmonitor/analytics/__init__.py +0 -0
  9. trafficmonitor/analytics/enhanced_queries.py +286 -0
  10. trafficmonitor/analytics/serializers.py +238 -0
  11. trafficmonitor/analytics/tests.py +757 -0
  12. trafficmonitor/analytics/urls.py +18 -0
  13. trafficmonitor/analytics/views.py +694 -0
  14. trafficmonitor/apps.py +7 -0
  15. trafficmonitor/circuit_breaker.py +63 -0
  16. trafficmonitor/conf.py +154 -0
  17. trafficmonitor/dashboard_security.py +111 -0
  18. trafficmonitor/db_utils.py +37 -0
  19. trafficmonitor/exceptions.py +93 -0
  20. trafficmonitor/health.py +66 -0
  21. trafficmonitor/load_test.py +423 -0
  22. trafficmonitor/load_test_api.py +307 -0
  23. trafficmonitor/management/__init__.py +1 -0
  24. trafficmonitor/management/commands/__init__.py +1 -0
  25. trafficmonitor/management/commands/cleanup_request_logs.py +77 -0
  26. trafficmonitor/middleware.py +383 -0
  27. trafficmonitor/migrations/0001_initial.py +93 -0
  28. trafficmonitor/migrations/__init__.py +0 -0
  29. trafficmonitor/models.py +206 -0
  30. trafficmonitor/monitoring.py +104 -0
  31. trafficmonitor/permissions.py +64 -0
  32. trafficmonitor/security.py +180 -0
  33. trafficmonitor/settings_production.py +105 -0
  34. trafficmonitor/static/analytics/css/dashboard.css +99 -0
  35. trafficmonitor/static/analytics/js/dashboard-production.js +339 -0
  36. trafficmonitor/static/analytics/js/dashboard-v2.js +697 -0
  37. trafficmonitor/static/analytics/js/dashboard.js +693 -0
  38. trafficmonitor/tasks.py +137 -0
  39. trafficmonitor/templates/analytics/dashboard.html +500 -0
  40. trafficmonitor/tests.py +246 -0
  41. trafficmonitor/views.py +3 -0
  42. trafficmonitor/websocket_consumers.py +128 -0
@@ -0,0 +1,757 @@
1
+ from datetime import timedelta
2
+ from django.contrib.auth import get_user_model
3
+ from django.test import TestCase, Client
4
+ from django.urls import reverse
5
+ from django.utils import timezone
6
+ from rest_framework.test import APIClient
7
+
8
+ from trafficmonitor.models import RequestLog
9
+ from trafficmonitor.analytics.views import AnalyticsQueryHelper
10
+
11
+
12
+ User = get_user_model()
13
+
14
+
15
+ class AnalyticsQueryHelperTest(TestCase):
16
+ """
17
+ Test cases for AnalyticsQueryHelper optimized queries.
18
+ """
19
+
20
+ def setUp(self):
21
+ """Set up test fixtures."""
22
+ self.user = User.objects.create_user(
23
+ username='testuser',
24
+ email='test@example.com',
25
+ password='testpass123'
26
+ )
27
+ self.staff_user = User.objects.create_user(
28
+ username='staffuser',
29
+ email='staff@example.com',
30
+ password='staffpass123',
31
+ is_staff=True
32
+ )
33
+
34
+ # Create test data
35
+ now = timezone.now()
36
+ self.test_logs = []
37
+
38
+ # Create logs for the last 7 days
39
+ for i in range(7):
40
+ timestamp = now - timedelta(days=i, hours=i)
41
+
42
+ # Success requests
43
+ for j in range(5):
44
+ log = RequestLog.objects.create(
45
+ method='GET',
46
+ path=f'/api/test/endpoint{j}/',
47
+ full_url=f'http://test.com/api/test/endpoint{j}/',
48
+ status_code=200,
49
+ user=self.user,
50
+ ip_address=f'192.168.1.{j}',
51
+ response_time_ms=100 + (i * 50),
52
+ query_count=5 + i,
53
+ timestamp=timestamp
54
+ )
55
+ self.test_logs.append(log)
56
+
57
+ # Error requests
58
+ RequestLog.objects.create(
59
+ method='POST',
60
+ path='/api/test/error/',
61
+ full_url='http://test.com/api/test/error/',
62
+ status_code=500,
63
+ user=self.user,
64
+ ip_address='192.168.1.100',
65
+ response_time_ms=1500,
66
+ exception='Internal Server Error',
67
+ timestamp=timestamp
68
+ )
69
+
70
+ # Client error
71
+ RequestLog.objects.create(
72
+ method='GET',
73
+ path='/api/test/notfound/',
74
+ full_url='http://test.com/api/test/notfound/',
75
+ status_code=404,
76
+ ip_address='192.168.1.200',
77
+ response_time_ms=50,
78
+ timestamp=timestamp
79
+ )
80
+
81
+ def test_get_date_range_today(self):
82
+ """Test date range calculation for 'today'."""
83
+ start, end = AnalyticsQueryHelper.get_date_range('today')
84
+ now = timezone.now()
85
+ today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
86
+
87
+ self.assertEqual(start.date(), today_start.date())
88
+ self.assertEqual(start.hour, 0)
89
+
90
+ def test_get_date_range_last_7_days(self):
91
+ """Test date range calculation for 'last_7_days'."""
92
+ start, end = AnalyticsQueryHelper.get_date_range('last_7_days')
93
+ expected_start = timezone.now() - timedelta(days=7)
94
+
95
+ self.assertAlmostEqual(
96
+ (expected_start - start).total_seconds(),
97
+ 0,
98
+ delta=2 # Allow 2 seconds difference
99
+ )
100
+
101
+ def test_get_total_requests(self):
102
+ """Test total request count query."""
103
+ now = timezone.now()
104
+ start = now - timedelta(days=7)
105
+
106
+ total = AnalyticsQueryHelper.get_total_requests(start, now)
107
+
108
+ # 7 days * (5 success + 1 error + 1 client error) = 49
109
+ self.assertEqual(total, 49)
110
+
111
+ def test_get_total_requests_with_method_filter(self):
112
+ """Test total request count with method filter."""
113
+ now = timezone.now()
114
+ start = now - timedelta(days=7)
115
+
116
+ total = AnalyticsQueryHelper.get_total_requests(start, now, method='GET')
117
+
118
+ # 7 days * (5 GET + 1 GET 404) = 42
119
+ self.assertEqual(total, 42)
120
+
121
+ def test_get_total_requests_with_status_filter(self):
122
+ """Test total request count with status code filter."""
123
+ now = timezone.now()
124
+ start = now - timedelta(days=7)
125
+
126
+ total = AnalyticsQueryHelper.get_total_requests(
127
+ start, now,
128
+ status_code_range=(500, 599)
129
+ )
130
+
131
+ # 7 days * 1 500 error = 7
132
+ self.assertEqual(total, 7)
133
+
134
+ def test_get_requests_by_status_code(self):
135
+ """Test requests grouped by status code."""
136
+ now = timezone.now()
137
+ start = now - timedelta(days=7)
138
+
139
+ results = AnalyticsQueryHelper.get_requests_by_status_code(start, now)
140
+
141
+ # Should have 3 status codes: 200, 404, 500
142
+ self.assertEqual(len(results), 3)
143
+
144
+ # 200 should have the most requests
145
+ status_200 = next(r for r in results if r['status_code'] == 200)
146
+ self.assertEqual(status_200['count'], 35)
147
+
148
+ def test_get_requests_by_method(self):
149
+ """Test requests grouped by HTTP method."""
150
+ now = timezone.now()
151
+ start = now - timedelta(days=7)
152
+
153
+ results = AnalyticsQueryHelper.get_requests_by_method(start, now)
154
+
155
+ # Should have GET and POST
156
+ self.assertGreaterEqual(len(results), 2)
157
+
158
+ # GET should have more than POST
159
+ get_count = next(r for r in results if r['method'] == 'GET')['count']
160
+ post_count = next(r for r in results if r['method'] == 'POST')['count']
161
+ self.assertGreater(get_count, post_count)
162
+
163
+ def test_get_top_endpoints(self):
164
+ """Test top endpoints query."""
165
+ now = timezone.now()
166
+ start = now - timedelta(days=7)
167
+
168
+ results = AnalyticsQueryHelper.get_top_endpoints(start, now, limit=5)
169
+
170
+ # Should return up to 5 endpoints
171
+ self.assertLessEqual(len(results), 5)
172
+ self.assertGreater(len(results), 0)
173
+
174
+ # Each result should have required fields
175
+ for result in results:
176
+ self.assertIn('path', result)
177
+ self.assertIn('count', result)
178
+ self.assertIn('avg_response_time', result)
179
+
180
+ def test_get_slowest_endpoints(self):
181
+ """Test slowest endpoints query."""
182
+ now = timezone.now()
183
+ start = now - timedelta(days=7)
184
+
185
+ results = AnalyticsQueryHelper.get_slowest_endpoints(start, now, limit=5)
186
+
187
+ self.assertGreater(len(results), 0)
188
+
189
+ # Results should be ordered by response time descending
190
+ for i in range(len(results) - 1):
191
+ self.assertGreaterEqual(
192
+ results[i]['avg_response_time'],
193
+ results[i + 1]['avg_response_time']
194
+ )
195
+
196
+ def test_get_top_ip_addresses(self):
197
+ """Test top IP addresses query."""
198
+ now = timezone.now()
199
+ start = now - timedelta(days=7)
200
+
201
+ results = AnalyticsQueryHelper.get_top_ip_addresses(start, now, limit=10)
202
+
203
+ self.assertGreater(len(results), 0)
204
+
205
+ # Each result should have ip_address and count
206
+ for result in results:
207
+ self.assertIn('ip_address', result)
208
+ self.assertIn('count', result)
209
+
210
+ def test_get_requests_over_time_daily(self):
211
+ """Test requests over time with daily granularity."""
212
+ now = timezone.now()
213
+ start = now - timedelta(days=7)
214
+
215
+ results = AnalyticsQueryHelper.get_requests_over_time(
216
+ start, now, granularity='day'
217
+ )
218
+
219
+ # Should have entries for each day
220
+ self.assertGreater(len(results), 0)
221
+
222
+ # Each result should have period and count
223
+ for result in results:
224
+ self.assertIn('period', result)
225
+ self.assertIn('count', result)
226
+
227
+ def test_get_requests_over_time_hourly(self):
228
+ """Test requests over time with hourly granularity."""
229
+ now = timezone.now()
230
+ start = now - timedelta(days=1)
231
+
232
+ results = AnalyticsQueryHelper.get_requests_over_time(
233
+ start, now, granularity='hour'
234
+ )
235
+
236
+ self.assertGreater(len(results), 0)
237
+
238
+ def test_get_error_trend(self):
239
+ """Test error trend query."""
240
+ now = timezone.now()
241
+ start = now - timedelta(days=7)
242
+
243
+ results = AnalyticsQueryHelper.get_error_trend(start, now)
244
+
245
+ self.assertGreater(len(results), 0)
246
+
247
+ # Each result should have error counts
248
+ for result in results:
249
+ self.assertIn('period', result)
250
+ self.assertIn('client_errors', result)
251
+ self.assertIn('server_errors', result)
252
+ self.assertIn('total_errors', result)
253
+
254
+ def test_get_hourly_heatmap(self):
255
+ """Test hourly heatmap query."""
256
+ now = timezone.now()
257
+ start = now - timedelta(days=7)
258
+
259
+ results = AnalyticsQueryHelper.get_hourly_heatmap(start, now)
260
+
261
+ # Results should have hours
262
+ self.assertGreater(len(results), 0)
263
+
264
+ # Hours should be between 0-23
265
+ for result in results:
266
+ self.assertGreaterEqual(result['hour'], 0)
267
+ self.assertLessEqual(result['hour'], 23)
268
+
269
+ def test_get_status_code_summary(self):
270
+ """Test status code summary aggregation."""
271
+ now = timezone.now()
272
+ start = now - timedelta(days=7)
273
+
274
+ summary = AnalyticsQueryHelper.get_status_code_summary(start, now)
275
+
276
+ # Should have all status categories
277
+ self.assertIn('success', summary)
278
+ self.assertIn('redirect', summary)
279
+ self.assertIn('client_error', summary)
280
+ self.assertIn('server_error', summary)
281
+ self.assertIn('total', summary)
282
+
283
+ # Total should equal sum of categories
284
+ total_calculated = (
285
+ summary['success'] +
286
+ summary['redirect'] +
287
+ summary['client_error'] +
288
+ summary['server_error']
289
+ )
290
+ self.assertEqual(summary['total'], total_calculated)
291
+
292
+ def test_get_performance_summary(self):
293
+ """Test performance metrics summary."""
294
+ now = timezone.now()
295
+ start = now - timedelta(days=7)
296
+
297
+ summary = AnalyticsQueryHelper.get_performance_summary(start, now)
298
+
299
+ # Should have performance metrics
300
+ self.assertIn('avg_response_time', summary)
301
+ self.assertIn('max_response_time', summary)
302
+ self.assertIn('min_response_time', summary)
303
+ self.assertIn('avg_query_count', summary)
304
+
305
+ # Values should be reasonable
306
+ self.assertIsNotNone(summary['avg_response_time'])
307
+ self.assertGreater(summary['avg_response_time'], 0)
308
+
309
+ def test_get_user_activity(self):
310
+ """Test user activity query."""
311
+ now = timezone.now()
312
+ start = now - timedelta(days=7)
313
+
314
+ activity = AnalyticsQueryHelper.get_user_activity(start, now)
315
+
316
+ # Should have authenticated and anonymous counts
317
+ self.assertIn('authenticated', activity)
318
+ self.assertIn('anonymous_count', activity)
319
+
320
+ # Should have authenticated user data
321
+ self.assertGreater(len(activity['authenticated']), 0)
322
+
323
+ def test_get_comprehensive_analytics(self):
324
+ """Test comprehensive analytics query."""
325
+ now = timezone.now()
326
+ start = now - timedelta(days=7)
327
+
328
+ data = AnalyticsQueryHelper.get_comprehensive_analytics(start, now)
329
+
330
+ # Verify all expected keys are present
331
+ expected_keys = [
332
+ 'totals', 'status_summary', 'status_codes', 'methods',
333
+ 'top_endpoints', 'slowest_endpoints', 'top_ips',
334
+ 'performance', 'users', 'requests_over_time',
335
+ 'error_trend', 'hourly_heatmap'
336
+ ]
337
+
338
+ for key in expected_keys:
339
+ self.assertIn(key, data, f"Missing key: {key}")
340
+
341
+ def test_filtering_by_path(self):
342
+ """Test path filtering."""
343
+ now = timezone.now()
344
+ start = now - timedelta(days=7)
345
+
346
+ # Filter by specific path
347
+ total = AnalyticsQueryHelper.get_total_requests(
348
+ start, now,
349
+ path_contains='endpoint0'
350
+ )
351
+
352
+ # Should only include endpoint0
353
+ self.assertEqual(total, 7) # 7 days
354
+
355
+ def test_filtering_by_user(self):
356
+ """Test user filtering."""
357
+ now = timezone.now()
358
+ start = now - timedelta(days=7)
359
+
360
+ # Filter by user
361
+ total = AnalyticsQueryHelper.get_total_requests(
362
+ start, now,
363
+ user_id=self.user.id
364
+ )
365
+
366
+ # Should only include user's requests (not the 404s which have no user)
367
+ self.assertEqual(total, 42) # 7 days * 6 requests with user
368
+
369
+
370
+ class AnalyticsAPITest(TestCase):
371
+ """
372
+ Test cases for Analytics API endpoints.
373
+ """
374
+
375
+ def setUp(self):
376
+ """Set up test fixtures."""
377
+ self.client = APIClient()
378
+
379
+ # Create regular user
380
+ self.user = User.objects.create_user(
381
+ username='testuser',
382
+ email='test@example.com',
383
+ password='testpass123'
384
+ )
385
+
386
+ # Create staff user
387
+ self.staff_user = User.objects.create_user(
388
+ username='staffuser',
389
+ email='staff@example.com',
390
+ password='staffpass123',
391
+ is_staff=True
392
+ )
393
+
394
+ # Create test logs
395
+ now = timezone.now()
396
+ for i in range(10):
397
+ RequestLog.objects.create(
398
+ method='GET',
399
+ path='/api/test/',
400
+ full_url='http://test.com/api/test/',
401
+ status_code=200,
402
+ user=self.user,
403
+ ip_address='192.168.1.100',
404
+ response_time_ms=100,
405
+ timestamp=now - timedelta(hours=i)
406
+ )
407
+
408
+ def test_analytics_overview_requires_authentication(self):
409
+ """Test that API requires authentication."""
410
+ url = reverse('analytics:api-overview')
411
+ # No authentication
412
+ response = self.client.get(url)
413
+
414
+ # Should return 401 (unauthorized)
415
+ self.assertIn(response.status_code, [401, 403])
416
+
417
+ def test_analytics_overview_success(self):
418
+ """Test successful analytics overview API call."""
419
+ url = reverse('analytics:api-overview')
420
+
421
+ # Authenticate as staff user
422
+ self.client.force_authenticate(user=self.staff_user)
423
+ response = self.client.get(url)
424
+
425
+ self.assertEqual(response.status_code, 200)
426
+
427
+ # Verify response structure
428
+ data = response.json()
429
+ self.assertIn('totals', data)
430
+ self.assertIn('status_summary', data)
431
+ self.assertIn('status_codes', data)
432
+ self.assertIn('methods', data)
433
+ self.assertIn('metadata', data)
434
+
435
+ def test_analytics_overview_with_filters(self):
436
+ """Test analytics API with filters."""
437
+ url = reverse('analytics:api-overview')
438
+
439
+ # Authenticate as staff user
440
+ self.client.force_authenticate(user=self.staff_user)
441
+ response = self.client.get(url, {
442
+ 'range': 'today',
443
+ 'method': 'GET',
444
+ 'status': '200'
445
+ })
446
+
447
+ self.assertEqual(response.status_code, 200)
448
+ data = response.json()
449
+
450
+ # Verify filters were applied
451
+ self.assertIn('filters', data['metadata'])
452
+
453
+ def test_analytics_overview_custom_date_range(self):
454
+ """Test analytics API with custom date range."""
455
+ url = reverse('analytics:api-overview')
456
+
457
+ self.client.force_authenticate(user=self.staff_user)
458
+ response = self.client.get(url, {
459
+ 'range': 'custom',
460
+ 'start_date': '2024-01-01',
461
+ 'end_date': '2024-01-31'
462
+ })
463
+
464
+ self.assertEqual(response.status_code, 200)
465
+
466
+ def test_analytics_overview_invalid_custom_range(self):
467
+ """Test analytics API with invalid custom range."""
468
+ url = reverse('analytics:api-overview')
469
+
470
+ self.client.force_authenticate(user=self.staff_user)
471
+ response = self.client.get(url, {
472
+ 'range': 'custom',
473
+ 'start_date': 'invalid-date',
474
+ 'end_date': '2024-01-31'
475
+ })
476
+
477
+ self.assertEqual(response.status_code, 400)
478
+ self.assertIn('error', response.json())
479
+
480
+ def test_analytics_overview_missing_custom_dates(self):
481
+ """Test analytics API with missing custom dates."""
482
+ url = reverse('analytics:api-overview')
483
+
484
+ self.client.force_authenticate(user=self.staff_user)
485
+ response = self.client.get(url, {
486
+ 'range': 'custom',
487
+ })
488
+
489
+ self.assertEqual(response.status_code, 400)
490
+
491
+ def test_chart_data_api_success(self):
492
+ """Test chart data API endpoint."""
493
+ url = reverse('analytics:api-chart-data', kwargs={'chart_type': 'time-series'})
494
+
495
+ self.client.force_authenticate(user=self.staff_user)
496
+ response = self.client.get(url)
497
+
498
+ self.assertEqual(response.status_code, 200)
499
+
500
+ data = response.json()
501
+ self.assertIn('chart_type', data)
502
+ self.assertIn('data', data)
503
+ self.assertIn('metadata', data)
504
+
505
+ def test_chart_data_api_invalid_chart_type(self):
506
+ """Test chart data API with invalid chart type."""
507
+ url = reverse('analytics:api-chart-data', kwargs={'chart_type': 'invalid-chart'})
508
+
509
+ self.client.force_authenticate(user=self.staff_user)
510
+ response = self.client.get(url)
511
+
512
+ self.assertEqual(response.status_code, 400)
513
+
514
+ def test_chart_data_api_all_types(self):
515
+ """Test all chart types are accessible."""
516
+ chart_types = [
517
+ 'time-series', 'status-codes', 'methods', 'endpoints',
518
+ 'performance', 'heatmap', 'errors'
519
+ ]
520
+
521
+ self.client.force_authenticate(user=self.staff_user)
522
+
523
+ for chart_type in chart_types:
524
+ url = reverse('analytics:api-chart-data', kwargs={'chart_type': chart_type})
525
+ response = self.client.get(url)
526
+
527
+ self.assertEqual(
528
+ response.status_code, 200,
529
+ f"Failed for chart type: {chart_type}"
530
+ )
531
+
532
+
533
+ class AnalyticsDashboardViewTest(TestCase):
534
+ """
535
+ Test cases for Analytics Dashboard view.
536
+ """
537
+
538
+ def setUp(self):
539
+ """Set up test fixtures."""
540
+ self.client = Client()
541
+
542
+ # Create regular user
543
+ self.user = User.objects.create_user(
544
+ username='testuser',
545
+ email='test@example.com',
546
+ password='testpass123'
547
+ )
548
+
549
+ # Create staff user
550
+ self.staff_user = User.objects.create_user(
551
+ username='staffuser',
552
+ email='staff@example.com',
553
+ password='staffpass123',
554
+ is_staff=True
555
+ )
556
+
557
+ def test_dashboard_requires_login(self):
558
+ """Test that dashboard requires authentication."""
559
+ url = reverse('analytics:dashboard')
560
+ response = self.client.get(url)
561
+
562
+ # Should redirect to login
563
+ self.assertEqual(response.status_code, 302)
564
+ self.assertIn('/accounts/login/', response.url)
565
+
566
+ def test_dashboard_requires_staff(self):
567
+ """Test that dashboard requires staff permission."""
568
+ url = reverse('analytics:dashboard')
569
+
570
+ # Login as regular user
571
+ self.client.login(username='testuser', password='testpass123')
572
+ response = self.client.get(url)
573
+
574
+ # Should redirect to login or show 403
575
+ self.assertIn(response.status_code, [302, 403])
576
+
577
+ def test_dashboard_accessible_to_staff(self):
578
+ """Test that staff users can access dashboard."""
579
+ url = reverse('analytics:dashboard')
580
+
581
+ # Login as staff user
582
+ self.client.login(username='staffuser', password='staffpass123')
583
+ response = self.client.get(url)
584
+
585
+ self.assertEqual(response.status_code, 200)
586
+ self.assertIn(b'API Analytics Dashboard', response.content)
587
+
588
+ def test_dashboard_with_filters(self):
589
+ """Test dashboard with filter parameters."""
590
+ url = reverse('analytics:dashboard')
591
+
592
+ self.client.login(username='staffuser', password='staffpass123')
593
+ response = self.client.get(url, {
594
+ 'range': 'last_7_days',
595
+ 'method': 'GET',
596
+ 'status': '200'
597
+ })
598
+
599
+ self.assertEqual(response.status_code, 200)
600
+ self.assertEqual(response.context['filters']['method'], 'GET')
601
+
602
+
603
+ class AnalyticsPerformanceTest(TestCase):
604
+ """
605
+ Test cases for query performance and optimization.
606
+ """
607
+
608
+ def setUp(self):
609
+ """Set up test fixtures."""
610
+ self.user = User.objects.create_user(
611
+ username='testuser',
612
+ email='test@example.com',
613
+ password='testpass123'
614
+ )
615
+
616
+ # Create a larger dataset
617
+ now = timezone.now()
618
+ self.logs_count = 1000
619
+
620
+ # Bulk create logs for performance testing
621
+ logs_to_create = []
622
+ for i in range(self.logs_count):
623
+ logs_to_create.append(
624
+ RequestLog(
625
+ method='GET' if i % 3 == 0 else 'POST',
626
+ path=f'/api/endpoint{i % 10}/',
627
+ full_url=f'http://test.com/api/endpoint{i % 10}/',
628
+ status_code=200 if i % 5 != 0 else 500,
629
+ user=self.user,
630
+ ip_address=f'192.168.1.{i % 255}',
631
+ response_time_ms=50 + (i % 100),
632
+ query_count=5 + (i % 10),
633
+ timestamp=now - timedelta(hours=i % 168) # Last 7 days
634
+ )
635
+ )
636
+
637
+ RequestLog.objects.bulk_create(logs_to_create)
638
+
639
+ def test_query_performance_with_large_dataset(self):
640
+ """Test that queries remain efficient with large dataset."""
641
+ from django.test.utils import override_settings
642
+ from django.db import connection
643
+ from django.test.utils import CaptureQueriesContext
644
+
645
+ now = timezone.now()
646
+ start = now - timedelta(days=7)
647
+
648
+ # Test comprehensive analytics query count
649
+ with CaptureQueriesContext(connection) as context:
650
+ data = AnalyticsQueryHelper.get_comprehensive_analytics(start, now)
651
+
652
+ # Should use a reasonable number of queries (not N+1)
653
+ # Each aggregation is a separate query, so expect ~12-15 queries
654
+ self.assertLess(len(context.captured_queries), 20,
655
+ "Too many queries - possible N+1 issue")
656
+
657
+ # Verify data was returned
658
+ self.assertGreater(data['totals']['selected_range'], 0)
659
+
660
+ def test_top_endpoints_uses_single_query(self):
661
+ """Test that top endpoints uses a single optimized query."""
662
+ from django.db import connection
663
+ from django.test.utils import CaptureQueriesContext
664
+
665
+ now = timezone.now()
666
+ start = now - timedelta(days=7)
667
+
668
+ with CaptureQueriesContext(connection) as context:
669
+ results = AnalyticsQueryHelper.get_top_endpoints(start, now)
670
+
671
+ # Should be a single query with aggregation
672
+ self.assertEqual(len(context.captured_queries), 1)
673
+ self.assertGreater(len(results), 0)
674
+
675
+
676
+ class AnalyticsIntegrationTest(TestCase):
677
+ """
678
+ Integration tests for the complete analytics flow.
679
+ """
680
+
681
+ def setUp(self):
682
+ """Set up test fixtures."""
683
+ self.client = Client()
684
+ self.api_client = APIClient()
685
+
686
+ self.staff_user = User.objects.create_user(
687
+ username='staffuser',
688
+ email='staff@example.com',
689
+ password='staffpass123',
690
+ is_staff=True
691
+ )
692
+
693
+ # Create diverse test data
694
+ now = timezone.now()
695
+
696
+ # Mix of methods
697
+ for method in ['GET', 'POST', 'PUT', 'DELETE']:
698
+ RequestLog.objects.create(
699
+ method=method,
700
+ path='/api/test/',
701
+ full_url='http://test.com/api/test/',
702
+ status_code=200,
703
+ user=self.staff_user,
704
+ response_time_ms=100,
705
+ timestamp=now
706
+ )
707
+
708
+ # Mix of status codes
709
+ for status in [200, 201, 400, 401, 404, 500]:
710
+ RequestLog.objects.create(
711
+ method='GET',
712
+ path=f'/api/status{status}/',
713
+ full_url=f'http://test.com/api/status{status}/',
714
+ status_code=status,
715
+ response_time_ms=100,
716
+ timestamp=now
717
+ )
718
+
719
+ def test_full_dashboard_workflow(self):
720
+ """Test complete dashboard workflow."""
721
+ # Login
722
+ self.client.login(username='staffuser', password='staffpass123')
723
+
724
+ # Access dashboard
725
+ dashboard_url = reverse('analytics:dashboard')
726
+ response = self.client.get(dashboard_url)
727
+ self.assertEqual(response.status_code, 200)
728
+
729
+ # Fetch analytics data via API
730
+ self.api_client.force_authenticate(user=self.staff_user)
731
+ api_url = reverse('analytics:api-overview')
732
+ api_response = self.api_client.get(api_url)
733
+
734
+ self.assertEqual(api_response.status_code, 200)
735
+
736
+ # Verify data structure
737
+ data = api_response.json()
738
+ self.assertGreater(data['totals']['today'], 0)
739
+ self.assertGreater(len(data['methods']), 0)
740
+ self.assertGreater(len(data['status_codes']), 0)
741
+
742
+ def test_dashboard_filter_workflow(self):
743
+ """Test dashboard with various filters."""
744
+ self.api_client.force_authenticate(user=self.staff_user)
745
+
746
+ # Test different range filters
747
+ ranges = ['today', 'yesterday', 'last_7_days', 'last_30_days']
748
+
749
+ for range_type in ranges:
750
+ url = reverse('analytics:api-overview')
751
+ response = self.api_client.get(url, {'range': range_type})
752
+
753
+ self.assertEqual(response.status_code, 200,
754
+ f"Failed for range: {range_type}")
755
+
756
+ data = response.json()
757
+ self.assertEqual(data['metadata']['range_type'], range_type)