secure-web/backend/tests/test_scans.py

263 lines
8.4 KiB
Python

"""
Tests for scan creation and management.
"""
import pytest
import json
from unittest.mock import patch, MagicMock
from django.test import TestCase, Client
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from websites.models import Website, Scan, Issue, Metric, ScanStatus
class TestScanCreation(APITestCase):
"""Tests for scan creation via API."""
def test_create_scan_valid_url(self):
"""Test creating a scan with a valid URL."""
response = self.client.post(
'/api/scans/',
{'url': 'https://example.com'},
format='json'
)
assert response.status_code == status.HTTP_201_CREATED
assert 'id' in response.data
assert response.data['status'] == 'pending'
def test_create_scan_creates_website(self):
"""Test that creating a scan also creates a Website record."""
response = self.client.post(
'/api/scans/',
{'url': 'https://newsite.example.com'},
format='json'
)
assert response.status_code == status.HTTP_201_CREATED
assert Website.objects.filter(url='https://newsite.example.com').exists()
def test_create_scan_reuses_website(self):
"""Test that scanning same URL reuses Website record."""
# Create first scan
self.client.post(
'/api/scans/',
{'url': 'https://example.com'},
format='json'
)
# Create second scan for same URL
self.client.post(
'/api/scans/',
{'url': 'https://example.com'},
format='json'
)
# Should only have one website
assert Website.objects.filter(domain='example.com').count() == 1
# But two scans
assert Scan.objects.count() == 2
def test_create_scan_invalid_url(self):
"""Test that invalid URL is rejected."""
response = self.client.post(
'/api/scans/',
{'url': 'not-a-valid-url'},
format='json'
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_scan_localhost_blocked(self):
"""Test that localhost URLs are blocked."""
response = self.client.post(
'/api/scans/',
{'url': 'http://localhost:8000'},
format='json'
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_create_scan_private_ip_blocked(self):
"""Test that private IP URLs are blocked."""
response = self.client.post(
'/api/scans/',
{'url': 'http://192.168.1.1'},
format='json'
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
class TestScanRetrieval(APITestCase):
"""Tests for retrieving scan results."""
def setUp(self):
"""Set up test data."""
self.website = Website.objects.create(
url='https://example.com',
domain='example.com'
)
self.scan = Scan.objects.create(
website=self.website,
status=ScanStatus.DONE,
performance_score=85,
accessibility_score=90,
seo_score=75,
best_practices_score=80,
security_score=70,
overall_score=80
)
# Add some issues
Issue.objects.create(
scan=self.scan,
category='security',
severity='medium',
tool='header_check',
title='Missing CSP Header',
description='Content-Security-Policy header is not set.'
)
Issue.objects.create(
scan=self.scan,
category='performance',
severity='low',
tool='lighthouse',
title='Large JavaScript bundle',
description='Bundle size exceeds recommended limit.'
)
# Add some metrics
Metric.objects.create(
scan=self.scan,
name='first_contentful_paint',
display_name='First Contentful Paint',
value=1200,
unit='ms',
source='lighthouse'
)
def test_get_scan_detail(self):
"""Test retrieving scan details."""
response = self.client.get(f'/api/scans/{self.scan.id}/')
assert response.status_code == status.HTTP_200_OK
assert response.data['id'] == str(self.scan.id)
assert response.data['overall_score'] == 80
def test_scan_includes_issues(self):
"""Test that scan detail includes issues."""
response = self.client.get(f'/api/scans/{self.scan.id}/')
assert 'issues' in response.data
assert len(response.data['issues']) == 2
def test_scan_includes_metrics(self):
"""Test that scan detail includes metrics."""
response = self.client.get(f'/api/scans/{self.scan.id}/')
assert 'metrics' in response.data
assert len(response.data['metrics']) == 1
def test_list_scans(self):
"""Test listing all scans."""
response = self.client.get('/api/scans/')
assert response.status_code == status.HTTP_200_OK
assert 'results' in response.data
assert len(response.data['results']) >= 1
def test_get_nonexistent_scan(self):
"""Test retrieving a scan that doesn't exist."""
import uuid
fake_id = uuid.uuid4()
response = self.client.get(f'/api/scans/{fake_id}/')
assert response.status_code == status.HTTP_404_NOT_FOUND
class TestScanModel(TestCase):
"""Tests for Scan model methods."""
def setUp(self):
"""Set up test data."""
self.website = Website.objects.create(
url='https://example.com',
domain='example.com'
)
self.scan = Scan.objects.create(
website=self.website,
status=ScanStatus.DONE
)
def test_calculate_security_score_no_issues(self):
"""Test security score calculation with no issues."""
score = self.scan.calculate_security_score()
assert score == 100
def test_calculate_security_score_with_issues(self):
"""Test security score calculation with security issues."""
Issue.objects.create(
scan=self.scan,
category='security',
severity='high',
tool='owasp_zap',
title='XSS Vulnerability',
description='Cross-site scripting vulnerability found.'
)
score = self.scan.calculate_security_score()
assert score == 85 # 100 - 15 (high severity)
def test_calculate_security_score_multiple_issues(self):
"""Test security score with multiple issues."""
Issue.objects.create(
scan=self.scan,
category='security',
severity='critical',
tool='owasp_zap',
title='SQL Injection',
description='SQL injection vulnerability.'
)
Issue.objects.create(
scan=self.scan,
category='headers',
severity='medium',
tool='header_check',
title='Missing HSTS',
description='HSTS header not set.'
)
score = self.scan.calculate_security_score()
# 100 - 25 (critical) - 8 (medium) = 67
assert score == 67
def test_calculate_overall_score(self):
"""Test overall score calculation."""
self.scan.performance_score = 80
self.scan.security_score = 90
self.scan.accessibility_score = 85
self.scan.seo_score = 75
self.scan.best_practices_score = 70
self.scan.save()
overall = self.scan.calculate_overall_score()
# Weighted average based on model weights
assert overall is not None
assert 0 <= overall <= 100
def test_calculate_overall_score_missing_values(self):
"""Test overall score with some missing values."""
self.scan.performance_score = 80
self.scan.security_score = None
self.scan.accessibility_score = None
self.scan.seo_score = None
self.scan.best_practices_score = None
self.scan.save()
overall = self.scan.calculate_overall_score()
# Should still calculate with available scores
assert overall == 80 # Only performance available