44"""Tests for the FakeEmailAnalyzer heuristic."""
55
66
7- from collections .abc import Generator
8- from unittest .mock import MagicMock , patch
7+ from unittest .mock import MagicMock
98
109import pytest
11- from email_validator import EmailNotValidError
1210
11+ from macaron .errors import HeuristicAnalyzerValueError
1312from macaron .malware_analyzer .pypi_heuristics .heuristics import HeuristicResult
1413from macaron .malware_analyzer .pypi_heuristics .metadata .fake_email import FakeEmailAnalyzer
1514from macaron .slsa_analyzer .package_registry .pypi_registry import PyPIPackageJsonAsset
@@ -22,20 +21,13 @@ def analyzer_() -> FakeEmailAnalyzer:
2221
2322
2423@pytest .fixture (name = "pypi_package_json_asset_mock" )
25- def pypi_package_json_asset_mock_fixture () -> MagicMock :
24+ def pypi_package_json_asset_mock_ () -> MagicMock :
2625 """Pytest fixture for a mock PyPIPackageJsonAsset."""
2726 mock_asset = MagicMock (spec = PyPIPackageJsonAsset )
2827 mock_asset .package_json = {}
2928 return mock_asset
3029
3130
32- @pytest .fixture (name = "mock_validate_email" )
33- def mock_validate_email_fixture () -> Generator [MagicMock ]:
34- """Patch validate_email and mock its behavior."""
35- with patch ("macaron.malware_analyzer.pypi_heuristics.metadata.fake_email.validate_email" ) as mock :
36- yield mock
37-
38-
3931def test_analyze_skip_no_emails_present (analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock ) -> None :
4032 """Test the analyzer skips if no author_email or maintainer_email is present."""
4133 pypi_package_json_asset_mock .package_json = {"info" : {"author_email" : None , "maintainer_email" : None }}
@@ -44,99 +36,98 @@ def test_analyze_skip_no_emails_present(analyzer: FakeEmailAnalyzer, pypi_packag
4436 assert info ["message" ] == "No author or maintainer email available."
4537
4638
47- def test_analyze_skip_no_info_key (analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock ) -> None :
48- """Test the analyzer skips if 'info' key is missing in PyPI data."""
39+ def test_analyze_raises_error_for_missing_info_key (
40+ analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock
41+ ) -> None :
42+ """Test the analyzer raises an error if the 'info' key is missing in the PyPI data."""
4943 pypi_package_json_asset_mock .package_json = {} # No 'info' key
50- result , info = analyzer . analyze ( pypi_package_json_asset_mock )
51- assert result == HeuristicResult . SKIP
52- assert info [ "message" ] == " No package info available."
44+ with pytest . raises ( HeuristicAnalyzerValueError ) as exc_info :
45+ analyzer . analyze ( pypi_package_json_asset_mock )
46+ assert " No package info available." in str ( exc_info . value )
5347
5448
55- def test_analyze_fail_invalid_email (
56- analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock , mock_validate_email : MagicMock
49+ def test_analyze_fail_no_email_found_in_field (
50+ analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock
5751) -> None :
58- """Test analyzer fails for an invalid email format."""
59- invalid_email = "invalid-email"
52+ """Test the analyzer fails if an email field does not contain a parsable email address."""
53+ pypi_package_json_asset_mock .package_json = {"info" : {"author_email" : "not an email" , "maintainer_email" : None }}
54+ result , info = analyzer .analyze (pypi_package_json_asset_mock )
55+ assert result == HeuristicResult .FAIL
56+ assert info == {"message" : "no emails found in the email field" }
57+
58+
59+ def test_analyze_fail_invalid_email (analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock ) -> None :
60+ """Test analyzer fails if the email field contains an invalid email format."""
61+ invalid_email = "user@example"
6062 pypi_package_json_asset_mock .package_json = {"info" : {"author_email" : invalid_email , "maintainer_email" : None }}
61- mock_validate_email .side_effect = EmailNotValidError ("Invalid email." )
6263
6364 result , info = analyzer .analyze (pypi_package_json_asset_mock )
64-
6565 assert result == HeuristicResult .FAIL
66- assert info == {"email" : invalid_email }
67- mock_validate_email .assert_called_once_with (invalid_email , check_deliverability = True )
66+ assert info == {"message" : "no emails found in the email field" }
6867
6968
7069def test_analyze_pass_only_maintainer_email_valid (
71- analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock , mock_validate_email : MagicMock
70+ analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock
7271) -> None :
73- """Test analyzer passes when only maintainer_email is present and valid ."""
72+ """Test the analyzer passes if only a valid maintainer_email is present and deliverability is not checked ."""
7473 email = "maintainer@example.net"
7574 pypi_package_json_asset_mock .package_json = {"info" : {"author_email" : None , "maintainer_email" : email }}
75+ result , info = analyzer .analyze (pypi_package_json_asset_mock )
7676
77- mock_email_info = MagicMock ()
78- mock_email_info .normalized = "maintainer@example.net"
79- mock_email_info .local_part = "maintainer"
80- mock_email_info .domain = "example.net"
81- mock_validate_email .return_value = mock_email_info
77+ if analyzer .check_deliverability :
78+ assert result == HeuristicResult .FAIL
79+ assert info == {"invalid_email" : email }
80+ return
8281
83- result , info = analyzer .analyze (pypi_package_json_asset_mock )
8482 assert result == HeuristicResult .PASS
8583 assert info ["validated_emails" ] == [
8684 {"normalized" : "maintainer@example.net" , "local_part" : "maintainer" , "domain" : "example.net" }
8785 ]
88- mock_validate_email .assert_called_once_with (email , check_deliverability = True )
8986
9087
91- def test_analyze_pass_both_emails_valid (
92- analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock , mock_validate_email : MagicMock
93- ) -> None :
94- """Test the analyzer passes when both emails are present and valid."""
95-
96- def side_effect (email : str , check_deliverability : bool ) -> MagicMock : # pylint: disable=unused-argument
97- local_part , domain = email .split ("@" )
98- mock_email_info = MagicMock ()
99- mock_email_info .normalized = email
100- mock_email_info .local_part = local_part
101- mock_email_info .domain = domain
102- return mock_email_info
103-
104- mock_validate_email .side_effect = side_effect
88+ def test_analyze_pass_both_emails_valid (analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock ) -> None :
89+ """Test the analyzer passes if both emails are valid and deliverability is not checked."""
90+ author_email = "example@gmail.com"
91+ author_local_part , author_domain = author_email .split ("@" )
92+ maintainer_email = "maintainer@example.net"
93+ maintainer_local_part , maintainer_domain = maintainer_email .split ("@" )
10594
10695 pypi_package_json_asset_mock .package_json = {
107- "info" : {"author_email" : "author@example.com" , "maintainer_email" : "maintainer@example.net" }
96+ "info" : {"author_email" : author_email , "maintainer_email" : maintainer_email }
10897 }
10998 result , info = analyzer .analyze (pypi_package_json_asset_mock )
99+ if analyzer .check_deliverability :
100+ assert result == HeuristicResult .FAIL
101+ assert info == {"invalid_email" : maintainer_email }
102+ return
103+
110104 assert result == HeuristicResult .PASS
111- assert mock_validate_email .call_count == 2
112105
113106 validated_emails = info .get ("validated_emails" )
114107 assert isinstance (validated_emails , list )
115108 assert len (validated_emails ) == 2
116- assert {"normalized" : "author@example.com" , "local_part" : "author" , "domain" : "example.com" } in validated_emails
109+ assert {"normalized" : author_email , "local_part" : author_local_part , "domain" : author_domain } in validated_emails
117110 assert {
118- "normalized" : "maintainer@example.net" ,
119- "local_part" : "maintainer" ,
120- "domain" : "example.net" ,
111+ "normalized" : maintainer_email ,
112+ "local_part" : maintainer_local_part ,
113+ "domain" : maintainer_domain ,
121114 } in validated_emails
122115
123116
124- def test_is_valid_email_success (analyzer : FakeEmailAnalyzer , mock_validate_email : MagicMock ) -> None :
125- """Test is_valid_email returns the validation object on success."""
126- mock_validated_email = MagicMock ()
127- mock_validated_email .normalized = "test@example.com"
128- mock_validated_email .local_part = "test"
129- mock_validated_email .domain = "example.com"
130-
131- mock_validate_email .return_value = mock_validated_email
132- result = analyzer .is_valid_email ("test@example.com" )
133- assert result == mock_validated_email
134- mock_validate_email .assert_called_once_with ("test@example.com" , check_deliverability = True )
135-
136-
137- def test_is_valid_email_failure (analyzer : FakeEmailAnalyzer , mock_validate_email : MagicMock ) -> None :
117+ def test_is_valid_email_failure (analyzer : FakeEmailAnalyzer ) -> None :
138118 """Test is_valid_email returns None on failure."""
139- mock_validate_email .side_effect = EmailNotValidError ("The email address is not valid." )
140119 result = analyzer .is_valid_email ("invalid-email" )
141120 assert result is None
142- mock_validate_email .assert_called_once_with ("invalid-email" , check_deliverability = True )
121+
122+
123+ def test_get_emails (analyzer : FakeEmailAnalyzer ) -> None :
124+ """Test the get_emails method."""
125+ email_field = "test@example.com, another test <another@example.org>"
126+ expected = ["test@example.com" , "another@example.org" ]
127+ assert analyzer .get_emails (email_field ) == expected
128+
129+ email_field_no_email = "this is not an email"
130+ assert analyzer .get_emails (email_field_no_email ) == []
131+
132+ email_field_empty = ""
133+ assert analyzer .get_emails (email_field_empty ) == []
0 commit comments