import tempfile import time import unittest.mock from pathlib import Path from django.contrib.auth import get_user_model from django.core.cache import cache from django.test import TestCase from django.urls import reverse class ServerStatsTests(TestCase): def setUp(self): User = get_user_model() self.staff = User.objects.create_user( username='Boss', password='pw', is_staff=True, ) self.normal = User.objects.create_user(username='NormalUser', password='pw') cache.clear() def _authorized(self, user=None): session = self.client.session session['authorized'] = True session.save() if user is not None: self.client.force_login(user) return session def test_staff_api_returns_snapshot(self): self._authorized(self.staff) resp = self.client.get(reverse('landing:server_stats_api')) self.assertEqual(resp.status_code, 200) data = resp.json() for key in ('app', 'system', 'content', 'recent', 'traffic', 'generated_at'): self.assertIn(key, data) self.assertIn('users', data['content']) self.assertIn('versions', data['recent']) def test_api_payload_cached(self): self._authorized(self.staff) first = self.client.get(reverse('landing:server_stats_api')).json() second = self.client.get(reverse('landing:server_stats_api')).json() self.assertEqual(first['generated_at'], second['generated_at']) def test_non_staff_api_forbidden(self): self._authorized(self.normal) resp = self.client.get(reverse('landing:server_stats_api')) self.assertEqual(resp.status_code, 403) self.assertEqual(resp.json()['detail'], 'Only staff can view server stats.') def test_page_is_staff_shell(self): self._authorized(self.staff) resp = self.client.get(reverse('landing:server_stats')) self.assertEqual(resp.status_code, 200) self.assertContains(resp, 'stats-updated') self.assertContains(resp, '/api/stats/') def test_page_forbidden_for_non_staff(self): self._authorized(self.normal) resp = self.client.get(reverse('landing:server_stats')) self.assertEqual(resp.status_code, 403) def test_anonymous_redirects_to_login(self): self._authorized() resp = self.client.get(reverse('landing:server_stats')) self.assertEqual(resp.status_code, 302) class TrafficStatsTests(TestCase): def test_parses_access_log(self): from landing import views with tempfile.TemporaryDirectory() as tmp: log = Path(tmp) / 'gunicorn-access.log' now = time.strftime('%d/%b/%Y:%H:%M:%S %z', time.localtime()) log.write_text(''.join([ f'127.0.0.1 [{now}] "GET /home/ HTTP/1.1" 200 1200 0.004\n', f'127.0.0.1 [{now}] "POST /api/uploads/ HTTP/1.1" 200 80 0.020\n', f'127.0.0.1 [{now}] "GET /packs/test-pack/ HTTP/1.1" 404 300 0.001\n', f'10.0.0.5 [{now}] "GET /api/stats/ HTTP/1.1" 500 50 0.150\n', ])) with unittest.mock.patch.object(views, 'GUNICORN_ACCESS_LOG', log): stats = views._traffic_stats() self.assertIsNotNone(stats) self.assertEqual(stats['requests_total'], 4) self.assertEqual(stats['requests_min'], 4) self.assertEqual(stats['status'], {'2xx': 2, '3xx': 0, '4xx': 1, '5xx': 1}) self.assertEqual(stats['max_ms'], 150.0) self.assertEqual(len(stats['recent']), 4) self.assertEqual(stats['recent'][0]['status'], 500) def test_missing_log_returns_none(self): from landing import views with tempfile.TemporaryDirectory() as tmp: log = Path(tmp) / 'nope.log' with unittest.mock.patch.object(views, 'GUNICORN_ACCESS_LOG', log): self.assertIsNone(views._traffic_stats())