""" Tests for stats API endpoints. Tests dashboard statistics and trending data endpoints. """ import pytest from datetime import datetime, timedelta from web.models import Scan class TestStatsAPI: """Test suite for stats API endpoints.""" def test_scan_trend_default_30_days(self, client, auth_headers, db_session): """Test scan trend endpoint with default 30 days.""" # Create test scans over multiple days today = datetime.utcnow() for i in range(5): scan_date = today - timedelta(days=i) for j in range(i + 1): # Create 1, 2, 3, 4, 5 scans per day scan = Scan( config_file='/app/configs/test.yaml', timestamp=scan_date, status='completed', duration=10.5 ) db_session.add(scan) db_session.commit() # Request trend data response = client.get('/api/stats/scan-trend', headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert 'labels' in data assert 'values' in data assert 'start_date' in data assert 'end_date' in data assert 'total_scans' in data # Should have 30 days of data assert len(data['labels']) == 30 assert len(data['values']) == 30 # Total scans should match (1+2+3+4+5 = 15) assert data['total_scans'] == 15 # Values should be non-negative integers assert all(isinstance(v, int) for v in data['values']) assert all(v >= 0 for v in data['values']) def test_scan_trend_custom_days(self, client, auth_headers, db_session): """Test scan trend endpoint with custom number of days.""" # Create test scans today = datetime.utcnow() for i in range(10): scan = Scan( config_file='/app/configs/test.yaml', timestamp=today - timedelta(days=i), status='completed', duration=10.5 ) db_session.add(scan) db_session.commit() # Request 7 days of data response = client.get('/api/stats/scan-trend?days=7', headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert len(data['labels']) == 7 assert len(data['values']) == 7 assert data['total_scans'] == 7 def test_scan_trend_max_days_365(self, client, auth_headers): """Test scan trend endpoint accepts maximum 365 days.""" response = client.get('/api/stats/scan-trend?days=365', headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert len(data['labels']) == 365 def test_scan_trend_rejects_days_over_365(self, client, auth_headers): """Test scan trend endpoint rejects more than 365 days.""" response = client.get('/api/stats/scan-trend?days=366', headers=auth_headers) assert response.status_code == 400 data = response.get_json() assert 'error' in data assert '365' in data['error'] def test_scan_trend_rejects_days_less_than_1(self, client, auth_headers): """Test scan trend endpoint rejects days less than 1.""" response = client.get('/api/stats/scan-trend?days=0', headers=auth_headers) assert response.status_code == 400 data = response.get_json() assert 'error' in data def test_scan_trend_fills_missing_days_with_zero(self, client, auth_headers, db_session): """Test scan trend fills days with no scans as zero.""" # Create scans only on specific days today = datetime.utcnow() # Create scan 5 days ago scan1 = Scan( config_file='/app/configs/test.yaml', timestamp=today - timedelta(days=5), status='completed', duration=10.5 ) db_session.add(scan1) # Create scan 10 days ago scan2 = Scan( config_file='/app/configs/test.yaml', timestamp=today - timedelta(days=10), status='completed', duration=10.5 ) db_session.add(scan2) db_session.commit() # Request 15 days response = client.get('/api/stats/scan-trend?days=15', headers=auth_headers) assert response.status_code == 200 data = response.get_json() # Should have 15 days of data assert len(data['values']) == 15 # Most days should be zero zero_days = sum(1 for v in data['values'] if v == 0) assert zero_days >= 13 # At least 13 days with no scans def test_scan_trend_requires_authentication(self, client): """Test scan trend endpoint requires authentication.""" response = client.get('/api/stats/scan-trend') assert response.status_code == 401 def test_summary_endpoint(self, client, auth_headers, db_session): """Test summary statistics endpoint.""" # Create test scans with different statuses today = datetime.utcnow() # 5 completed scans for i in range(5): scan = Scan( config_file='/app/configs/test.yaml', timestamp=today - timedelta(days=i), status='completed', duration=10.5 ) db_session.add(scan) # 2 failed scans for i in range(2): scan = Scan( config_file='/app/configs/test.yaml', timestamp=today - timedelta(days=i), status='failed', duration=5.0 ) db_session.add(scan) # 1 running scan scan = Scan( config_file='/app/configs/test.yaml', timestamp=today, status='running', duration=None ) db_session.add(scan) db_session.commit() # Request summary response = client.get('/api/stats/summary', headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert 'total_scans' in data assert 'completed_scans' in data assert 'failed_scans' in data assert 'running_scans' in data assert 'scans_today' in data assert 'scans_this_week' in data # Verify counts assert data['total_scans'] == 8 assert data['completed_scans'] == 5 assert data['failed_scans'] == 2 assert data['running_scans'] == 1 assert data['scans_today'] >= 1 assert data['scans_this_week'] >= 1 def test_summary_with_no_scans(self, client, auth_headers): """Test summary endpoint with no scans.""" response = client.get('/api/stats/summary', headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert data['total_scans'] == 0 assert data['completed_scans'] == 0 assert data['failed_scans'] == 0 assert data['running_scans'] == 0 assert data['scans_today'] == 0 assert data['scans_this_week'] == 0 def test_summary_scans_today(self, client, auth_headers, db_session): """Test summary counts scans today correctly.""" today = datetime.utcnow() yesterday = today - timedelta(days=1) # Create 3 scans today for i in range(3): scan = Scan( config_file='/app/configs/test.yaml', timestamp=today, status='completed', duration=10.5 ) db_session.add(scan) # Create 2 scans yesterday for i in range(2): scan = Scan( config_file='/app/configs/test.yaml', timestamp=yesterday, status='completed', duration=10.5 ) db_session.add(scan) db_session.commit() response = client.get('/api/stats/summary', headers=auth_headers) assert response.status_code == 200 data = response.get_json() assert data['scans_today'] == 3 assert data['scans_this_week'] >= 3 def test_summary_scans_this_week(self, client, auth_headers, db_session): """Test summary counts scans this week correctly.""" today = datetime.utcnow() # Create scans over the last 10 days for i in range(10): scan = Scan( config_file='/app/configs/test.yaml', timestamp=today - timedelta(days=i), status='completed', duration=10.5 ) db_session.add(scan) db_session.commit() response = client.get('/api/stats/summary', headers=auth_headers) assert response.status_code == 200 data = response.get_json() # Last 7 days (0-6) = 7 scans assert data['scans_this_week'] == 7 def test_summary_requires_authentication(self, client): """Test summary endpoint requires authentication.""" response = client.get('/api/stats/summary') assert response.status_code == 401 def test_scan_trend_date_format(self, client, auth_headers, db_session): """Test scan trend returns dates in correct format.""" # Create a scan scan = Scan( config_file='/app/configs/test.yaml', timestamp=datetime.utcnow(), status='completed', duration=10.5 ) db_session.add(scan) db_session.commit() response = client.get('/api/stats/scan-trend?days=7', headers=auth_headers) assert response.status_code == 200 data = response.get_json() # Check date format (YYYY-MM-DD) for label in data['labels']: assert len(label) == 10 assert label[4] == '-' assert label[7] == '-' # Try parsing to ensure valid date datetime.strptime(label, '%Y-%m-%d') def test_scan_trend_consecutive_dates(self, client, auth_headers): """Test scan trend returns consecutive dates.""" response = client.get('/api/stats/scan-trend?days=7', headers=auth_headers) assert response.status_code == 200 data = response.get_json() labels = data['labels'] # Convert to datetime objects dates = [datetime.strptime(label, '%Y-%m-%d') for label in labels] # Check dates are consecutive for i in range(len(dates) - 1): diff = dates[i + 1] - dates[i] assert diff.days == 1, f"Dates not consecutive: {dates[i]} to {dates[i+1]}" def test_scan_trend_ends_with_today(self, client, auth_headers): """Test scan trend ends with today's date.""" response = client.get('/api/stats/scan-trend?days=7', headers=auth_headers) assert response.status_code == 200 data = response.get_json() # Last date should be today today = datetime.utcnow().date() last_date = datetime.strptime(data['labels'][-1], '%Y-%m-%d').date() assert last_date == today