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