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
326 lines
11 KiB
Python
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
|