Files
SneakyScan/tests/test_stats_api.py
Phillip Tarrant effce42f21 Phase 3 Step 5: Enhanced Dashboard with Charts & Analytics
Implemented dashboard visualizations and statistics API endpoints:

New Features:
- Stats API endpoints (/api/stats/scan-trend, /api/stats/summary)
- Chart.js trending chart showing 30-day scan activity
- Schedules widget displaying next 3 upcoming scheduled scans
- Enhanced Quick Actions with Manage Schedules button

Stats API (web/api/stats.py):
- scan-trend endpoint with configurable days (1-365)
- Summary endpoint for dashboard statistics
- Automatic date range filling with zeros for missing days
- Proper authentication and validation

Dashboard Enhancements (web/templates/dashboard.html):
- Chart.js line chart with dark theme styling
- Real-time schedules widget with human-readable time display
- Auto-refresh for schedules every 30 seconds
- Responsive 8-4 column layout for chart and schedules

Tests (tests/test_stats_api.py):
- 18 comprehensive test cases for stats API
- Coverage for date validation, authentication, edge cases
- Tests for empty data handling and date formatting

Progress: 64% complete (9/14 days)
Next: Step 6 - Scheduler Integration
2025-11-14 14:50:20 -06:00

326 lines
11 KiB
Python

"""
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