Spaces:
Sleeping
Sleeping
| """Tests for inference pipeline.""" | |
| import pytest | |
| import tempfile | |
| import os | |
| from unittest.mock import Mock, patch, MagicMock | |
| import pandas as pd | |
| import numpy as np | |
| from src.phising_detection.inference.pipeline import PhishingDetectionPipeline | |
| class TestPhishingDetectionPipeline: | |
| """Tests for PhishingDetectionPipeline class.""" | |
| def test_init_without_urlscan(self): | |
| """Test initialization without URLScan API key.""" | |
| pipeline = PhishingDetectionPipeline( | |
| model_name="test_model", | |
| model_version=1 | |
| ) | |
| assert pipeline.model_name == "test_model" | |
| assert pipeline.model_version == 1 | |
| assert pipeline.model is None | |
| assert pipeline.scaler is None | |
| assert pipeline.feature_names is None | |
| assert pipeline.urlscan_client is None | |
| def test_init_with_urlscan(self): | |
| """Test initialization with URLScan API key.""" | |
| with patch('src.phising_detection.inference.pipeline.URLScanClient') as mock_client: | |
| pipeline = PhishingDetectionPipeline( | |
| model_name="test_model", | |
| urlscan_api_key="test_key" | |
| ) | |
| mock_client.assert_called_once_with(api_key="test_key") | |
| assert pipeline.urlscan_client is not None | |
| def test_is_loaded_false(self): | |
| """Test is_loaded returns False when model not loaded.""" | |
| pipeline = PhishingDetectionPipeline() | |
| assert pipeline.is_loaded() is False | |
| def test_is_loaded_true(self): | |
| """Test is_loaded returns True when model is loaded.""" | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.model = Mock() | |
| pipeline.scaler = Mock() | |
| pipeline.feature_names = ['feature1', 'feature2'] | |
| assert pipeline.is_loaded() is True | |
| def test_load_model_from_hopsworks(self, mock_joblib_load, mock_connect): | |
| """Test loading model from Hopsworks.""" | |
| # Setup mocks | |
| mock_project = Mock() | |
| mock_mr = Mock() | |
| mock_model_registry = Mock() | |
| mock_model_registry.version = 1 | |
| mock_model_registry.download.return_value = "/tmp/model_dir" | |
| mock_project.get_model_registry.return_value = mock_mr | |
| mock_mr.get_model.return_value = mock_model_registry | |
| mock_connect.return_value = mock_project | |
| # Mock model and scaler | |
| mock_model = Mock() | |
| mock_scaler = Mock() | |
| mock_joblib_load.side_effect = [mock_model, mock_scaler] | |
| # Create temporary feature_names file | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| feature_names_path = os.path.join(tmpdir, "feature_names.txt") | |
| with open(feature_names_path, 'w') as f: | |
| f.write("feature1\nfeature2\nfeature3") | |
| # Mock download to return our temp dir | |
| mock_model_registry.download.return_value = tmpdir | |
| # Test | |
| pipeline = PhishingDetectionPipeline(model_name="test_model") | |
| pipeline.load_model_from_hopsworks() | |
| # Assertions | |
| assert pipeline.model == mock_model | |
| assert pipeline.scaler == mock_scaler | |
| assert pipeline.feature_names == ['feature1', 'feature2', 'feature3'] | |
| mock_connect.assert_called_once() | |
| mock_mr.get_model.assert_called_once_with("test_model") | |
| def test_load_model_with_version(self, mock_joblib_load, mock_connect): | |
| """Test loading specific model version from Hopsworks.""" | |
| # Setup mocks | |
| mock_project = Mock() | |
| mock_mr = Mock() | |
| mock_model_registry = Mock() | |
| mock_model_registry.version = 2 | |
| mock_model_registry.download.return_value = "/tmp/model_dir" | |
| mock_project.get_model_registry.return_value = mock_mr | |
| mock_mr.get_model.return_value = mock_model_registry | |
| mock_connect.return_value = mock_project | |
| mock_joblib_load.side_effect = [Mock(), Mock()] | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| feature_names_path = os.path.join(tmpdir, "feature_names.txt") | |
| with open(feature_names_path, 'w') as f: | |
| f.write("feature1") | |
| mock_model_registry.download.return_value = tmpdir | |
| # Test with specific version | |
| pipeline = PhishingDetectionPipeline(model_name="test_model", model_version=2) | |
| pipeline.load_model_from_hopsworks() | |
| # Should request version 2 | |
| mock_mr.get_model.assert_called_once_with("test_model", version=2) | |
| def test_preprocess_features_not_loaded(self): | |
| """Test preprocessing fails when model not loaded.""" | |
| pipeline = PhishingDetectionPipeline() | |
| features = {'feature1': 10, 'feature2': 20} | |
| with pytest.raises(ValueError) as exc_info: | |
| pipeline.preprocess_features(features) | |
| assert "Model not loaded" in str(exc_info.value) | |
| def test_preprocess_features_success(self): | |
| """Test successful feature preprocessing.""" | |
| # Setup pipeline with mock components | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.model = Mock() | |
| pipeline.feature_names = [ | |
| 'domain_age_days', | |
| 'secure_percentage', | |
| 'has_umbrella_rank', | |
| 'umbrella_rank', | |
| 'has_tls', | |
| 'tls_valid_days', | |
| 'url_length', | |
| 'subdomain_count' | |
| ] | |
| # Mock scaler | |
| mock_scaler = Mock() | |
| mock_scaler.transform.return_value = np.array([[1.5, 0.8, 5000, 365, 25, 1]]) | |
| pipeline.scaler = mock_scaler | |
| # Test features | |
| features = { | |
| 'domain_age_days': 3000, | |
| 'secure_percentage': 95.0, | |
| 'has_umbrella_rank': 1, | |
| 'umbrella_rank': 5000, | |
| 'has_tls': 1, | |
| 'tls_valid_days': 365, | |
| 'url_length': 25, | |
| 'subdomain_count': 1 | |
| } | |
| result = pipeline.preprocess_features(features) | |
| # Assertions | |
| assert isinstance(result, pd.DataFrame) | |
| assert list(result.columns) == pipeline.feature_names | |
| assert len(result) == 1 | |
| mock_scaler.transform.assert_called_once() | |
| def test_preprocess_features_with_missing_features(self): | |
| """Test preprocessing handles missing features.""" | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.model = Mock() | |
| pipeline.feature_names = [ | |
| 'domain_age_days', 'secure_percentage', 'has_umbrella_rank', | |
| 'umbrella_rank', 'has_tls', 'tls_valid_days', 'url_length', 'subdomain_count' | |
| ] | |
| mock_scaler = Mock() | |
| # Mock the scaler to return the same shape as input continuous features (6 features) | |
| mock_scaler.transform.return_value = np.array([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]]) | |
| pipeline.scaler = mock_scaler | |
| # Only provide some features (missing subdomain_count) | |
| features = { | |
| 'domain_age_days': 3000, | |
| 'secure_percentage': 95.0, | |
| 'has_umbrella_rank': 1, | |
| 'umbrella_rank': 5000, | |
| 'has_tls': 1, | |
| 'tls_valid_days': 365, | |
| 'url_length': 25 | |
| } | |
| result = pipeline.preprocess_features(features) | |
| # Should add missing subdomain_count and all features should be present | |
| assert 'subdomain_count' in result.columns | |
| assert len(result.columns) == 8 # All 8 features should be present | |
| assert list(result.columns) == pipeline.feature_names | |
| def test_predict_not_loaded(self): | |
| """Test prediction fails when model not loaded.""" | |
| pipeline = PhishingDetectionPipeline() | |
| features = {'feature1': 10} | |
| with pytest.raises(ValueError) as exc_info: | |
| pipeline.predict(features) | |
| assert "Model not loaded" in str(exc_info.value) | |
| def test_predict_phishing(self): | |
| """Test prediction for phishing URL.""" | |
| # Setup pipeline | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.feature_names = [ | |
| 'domain_age_days', 'secure_percentage', 'has_umbrella_rank', | |
| 'umbrella_rank', 'has_tls', 'tls_valid_days', 'url_length', 'subdomain_count' | |
| ] | |
| # Mock model | |
| mock_model = Mock() | |
| mock_model.predict_proba.return_value = np.array([[0.2, 0.8]]) # 80% phishing | |
| mock_model.predict.return_value = np.array([1]) # Phishing | |
| pipeline.model = mock_model | |
| # Mock scaler | |
| mock_scaler = Mock() | |
| mock_scaler.transform.return_value = np.array([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]]) | |
| pipeline.scaler = mock_scaler | |
| # Test features | |
| features = { | |
| 'domain_age_days': 10, | |
| 'secure_percentage': 50.0, | |
| 'has_umbrella_rank': 0, | |
| 'umbrella_rank': 999999, | |
| 'has_tls': 0, | |
| 'tls_valid_days': 0, | |
| 'url_length': 150, | |
| 'subdomain_count': 5 | |
| } | |
| result = pipeline.predict(features) | |
| # Assertions | |
| assert result['prediction'] == "PHISHING" | |
| assert result['is_phishing'] is True | |
| assert result['confidence'] == 0.8 | |
| assert result['phishing_probability'] == 0.8 | |
| assert result['legitimate_probability'] == 0.2 | |
| def test_predict_legitimate(self): | |
| """Test prediction for legitimate URL.""" | |
| # Setup pipeline | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.feature_names = [ | |
| 'domain_age_days', 'secure_percentage', 'has_umbrella_rank', | |
| 'umbrella_rank', 'has_tls', 'tls_valid_days', 'url_length', 'subdomain_count' | |
| ] | |
| # Mock model | |
| mock_model = Mock() | |
| mock_model.predict_proba.return_value = np.array([[0.9, 0.1]]) # 90% legitimate | |
| mock_model.predict.return_value = np.array([0]) # Legitimate | |
| pipeline.model = mock_model | |
| # Mock scaler | |
| mock_scaler = Mock() | |
| mock_scaler.transform.return_value = np.array([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]]) | |
| pipeline.scaler = mock_scaler | |
| # Test features | |
| features = { | |
| 'domain_age_days': 3000, | |
| 'secure_percentage': 95.0, | |
| 'has_umbrella_rank': 1, | |
| 'umbrella_rank': 5000, | |
| 'has_tls': 1, | |
| 'tls_valid_days': 365, | |
| 'url_length': 25, | |
| 'subdomain_count': 1 | |
| } | |
| result = pipeline.predict(features) | |
| # Assertions | |
| assert result['prediction'] == "LEGITIMATE" | |
| assert result['is_phishing'] is False | |
| assert result['confidence'] == 0.9 | |
| assert result['phishing_probability'] == 0.1 | |
| assert result['legitimate_probability'] == 0.9 | |
| def test_predict_url_without_urlscan(self): | |
| """Test predict_url fails without URLScan client.""" | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.model = Mock() | |
| pipeline.scaler = Mock() | |
| pipeline.feature_names = ['feature1'] | |
| with pytest.raises(ValueError) as exc_info: | |
| pipeline.predict_url("https://example.com") | |
| assert "URLScan client not initialized" in str(exc_info.value) | |
| def test_predict_url_success(self, mock_extract_features): | |
| """Test successful end-to-end URL prediction.""" | |
| # Setup pipeline | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.feature_names = [ | |
| 'domain_age_days', 'secure_percentage', 'has_umbrella_rank', | |
| 'umbrella_rank', 'has_tls', 'tls_valid_days', 'url_length', 'subdomain_count' | |
| ] | |
| # Mock model | |
| mock_model = Mock() | |
| mock_model.predict_proba.return_value = np.array([[0.7, 0.3]]) | |
| mock_model.predict.return_value = np.array([0]) | |
| pipeline.model = mock_model | |
| # Mock scaler | |
| mock_scaler = Mock() | |
| mock_scaler.transform.return_value = np.array([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]]) | |
| pipeline.scaler = mock_scaler | |
| # Mock URLScan client | |
| mock_urlscan_client = Mock() | |
| mock_scan_result = { | |
| 'task': {'uuid': 'test-uuid-123'}, | |
| 'page': {'domainAgeDays': 3000}, | |
| 'stats': {'securePercentage': 95} | |
| } | |
| mock_urlscan_client.submit_and_wait.return_value = mock_scan_result | |
| pipeline.urlscan_client = mock_urlscan_client | |
| # Mock extracted features | |
| extracted_features = { | |
| 'domain_age_days': 3000, | |
| 'secure_percentage': 95.0, | |
| 'has_umbrella_rank': 1, | |
| 'umbrella_rank': 5000, | |
| 'has_tls': 1, | |
| 'tls_valid_days': 365, | |
| 'url_length': 25, | |
| 'subdomain_count': 1 | |
| } | |
| mock_extract_features.return_value = extracted_features | |
| # Test | |
| result = pipeline.predict_url("https://example.com") | |
| # Assertions | |
| assert result['prediction'] == "LEGITIMATE" | |
| assert result['confidence'] == 0.7 | |
| assert result['features'] == extracted_features | |
| assert result['scan_uuid'] == 'test-uuid-123' | |
| mock_urlscan_client.submit_and_wait.assert_called_once_with("https://example.com") | |
| def test_predict_url_scan_fails(self): | |
| """Test predict_url handles URLScan failure.""" | |
| # Setup pipeline | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.model = Mock() | |
| pipeline.scaler = Mock() | |
| pipeline.feature_names = ['feature1'] | |
| # Mock URLScan client that returns None | |
| mock_urlscan_client = Mock() | |
| mock_urlscan_client.submit_and_wait.return_value = None | |
| pipeline.urlscan_client = mock_urlscan_client | |
| # Test | |
| result = pipeline.predict_url("https://example.com") | |
| # Should return error | |
| assert 'error' in result | |
| assert result['prediction'] == "ERROR" | |
| assert result['confidence'] == 0.0 | |
| def test_predict_url_exception_handling(self, mock_extract_features): | |
| """Test predict_url handles exceptions gracefully.""" | |
| # Setup pipeline | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.model = Mock() | |
| pipeline.scaler = Mock() | |
| pipeline.feature_names = ['feature1'] | |
| # Mock URLScan client | |
| mock_urlscan_client = Mock() | |
| mock_urlscan_client.submit_and_wait.return_value = {'task': {}} | |
| pipeline.urlscan_client = mock_urlscan_client | |
| # Mock extract_features to raise exception | |
| mock_extract_features.side_effect = Exception("Test error") | |
| # Test | |
| result = pipeline.predict_url("https://example.com") | |
| # Should return error | |
| assert 'error' in result | |
| assert result['prediction'] == "ERROR" | |
| assert "Test error" in result['error'] | |
| def test_predict_numerical_stability(self): | |
| """Test prediction handles edge cases in probabilities.""" | |
| # Setup pipeline | |
| pipeline = PhishingDetectionPipeline() | |
| pipeline.feature_names = [ | |
| 'domain_age_days', 'secure_percentage', 'has_umbrella_rank', | |
| 'umbrella_rank', 'has_tls', 'tls_valid_days', 'url_length', 'subdomain_count' | |
| ] | |
| # Mock model with extreme probabilities | |
| mock_model = Mock() | |
| mock_model.predict_proba.return_value = np.array([[0.999999, 0.000001]]) | |
| mock_model.predict.return_value = np.array([0]) | |
| pipeline.model = mock_model | |
| mock_scaler = Mock() | |
| mock_scaler.transform.return_value = np.array([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]]) | |
| pipeline.scaler = mock_scaler | |
| features = { | |
| 'domain_age_days': 3000, | |
| 'secure_percentage': 95.0, | |
| 'has_umbrella_rank': 1, | |
| 'umbrella_rank': 5000, | |
| 'has_tls': 1, | |
| 'tls_valid_days': 365, | |
| 'url_length': 25, | |
| 'subdomain_count': 1 | |
| } | |
| result = pipeline.predict(features) | |
| # Should handle extreme values correctly | |
| assert result['confidence'] > 0.99 | |
| assert result['prediction'] == "LEGITIMATE" | |
| assert result['phishing_probability'] < 0.01 | |