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.
- setu_trafficmonitor-2.0.0.dist-info/LICENSE +21 -0
- setu_trafficmonitor-2.0.0.dist-info/METADATA +401 -0
- setu_trafficmonitor-2.0.0.dist-info/RECORD +42 -0
- setu_trafficmonitor-2.0.0.dist-info/WHEEL +5 -0
- setu_trafficmonitor-2.0.0.dist-info/top_level.txt +1 -0
- trafficmonitor/__init__.py +11 -0
- trafficmonitor/admin.py +217 -0
- trafficmonitor/analytics/__init__.py +0 -0
- trafficmonitor/analytics/enhanced_queries.py +286 -0
- trafficmonitor/analytics/serializers.py +238 -0
- trafficmonitor/analytics/tests.py +757 -0
- trafficmonitor/analytics/urls.py +18 -0
- trafficmonitor/analytics/views.py +694 -0
- trafficmonitor/apps.py +7 -0
- trafficmonitor/circuit_breaker.py +63 -0
- trafficmonitor/conf.py +154 -0
- trafficmonitor/dashboard_security.py +111 -0
- trafficmonitor/db_utils.py +37 -0
- trafficmonitor/exceptions.py +93 -0
- trafficmonitor/health.py +66 -0
- trafficmonitor/load_test.py +423 -0
- trafficmonitor/load_test_api.py +307 -0
- trafficmonitor/management/__init__.py +1 -0
- trafficmonitor/management/commands/__init__.py +1 -0
- trafficmonitor/management/commands/cleanup_request_logs.py +77 -0
- trafficmonitor/middleware.py +383 -0
- trafficmonitor/migrations/0001_initial.py +93 -0
- trafficmonitor/migrations/__init__.py +0 -0
- trafficmonitor/models.py +206 -0
- trafficmonitor/monitoring.py +104 -0
- trafficmonitor/permissions.py +64 -0
- trafficmonitor/security.py +180 -0
- trafficmonitor/settings_production.py +105 -0
- trafficmonitor/static/analytics/css/dashboard.css +99 -0
- trafficmonitor/static/analytics/js/dashboard-production.js +339 -0
- trafficmonitor/static/analytics/js/dashboard-v2.js +697 -0
- trafficmonitor/static/analytics/js/dashboard.js +693 -0
- trafficmonitor/tasks.py +137 -0
- trafficmonitor/templates/analytics/dashboard.html +500 -0
- trafficmonitor/tests.py +246 -0
- trafficmonitor/views.py +3 -0
- 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)
|