263 lines
8.4 KiB
Python
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
|