4
4
"""Tests for the FakeEmailAnalyzer heuristic."""
5
5
6
6
7
- from collections .abc import Generator
8
- from unittest .mock import MagicMock , patch
7
+ from unittest .mock import MagicMock
9
8
10
9
import pytest
11
- from email_validator import EmailNotValidError
12
10
11
+ from macaron .errors import HeuristicAnalyzerValueError
13
12
from macaron .malware_analyzer .pypi_heuristics .heuristics import HeuristicResult
14
13
from macaron .malware_analyzer .pypi_heuristics .metadata .fake_email import FakeEmailAnalyzer
15
14
from macaron .slsa_analyzer .package_registry .pypi_registry import PyPIPackageJsonAsset
@@ -22,20 +21,13 @@ def analyzer_() -> FakeEmailAnalyzer:
22
21
23
22
24
23
@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 :
26
25
"""Pytest fixture for a mock PyPIPackageJsonAsset."""
27
26
mock_asset = MagicMock (spec = PyPIPackageJsonAsset )
28
27
mock_asset .package_json = {}
29
28
return mock_asset
30
29
31
30
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
-
39
31
def test_analyze_skip_no_emails_present (analyzer : FakeEmailAnalyzer , pypi_package_json_asset_mock : MagicMock ) -> None :
40
32
"""Test the analyzer skips if no author_email or maintainer_email is present."""
41
33
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
44
36
assert info ["message" ] == "No author or maintainer email available."
45
37
46
38
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."""
49
43
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 )
53
47
54
48
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
57
51
) -> 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"
60
62
pypi_package_json_asset_mock .package_json = {"info" : {"author_email" : invalid_email , "maintainer_email" : None }}
61
- mock_validate_email .side_effect = EmailNotValidError ("Invalid email." )
62
63
63
64
result , info = analyzer .analyze (pypi_package_json_asset_mock )
64
-
65
65
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" }
68
67
69
68
70
69
def 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
72
71
) -> 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 ."""
74
73
email = "maintainer@example.net"
75
74
pypi_package_json_asset_mock .package_json = {"info" : {"author_email" : None , "maintainer_email" : email }}
75
+ result , info = analyzer .analyze (pypi_package_json_asset_mock )
76
76
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
82
81
83
- result , info = analyzer .analyze (pypi_package_json_asset_mock )
84
82
assert result == HeuristicResult .PASS
85
83
assert info ["validated_emails" ] == [
86
84
{"normalized" : "maintainer@example.net" , "local_part" : "maintainer" , "domain" : "example.net" }
87
85
]
88
- mock_validate_email .assert_called_once_with (email , check_deliverability = True )
89
86
90
87
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 ("@" )
105
94
106
95
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 }
108
97
}
109
98
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
+
110
104
assert result == HeuristicResult .PASS
111
- assert mock_validate_email .call_count == 2
112
105
113
106
validated_emails = info .get ("validated_emails" )
114
107
assert isinstance (validated_emails , list )
115
108
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
117
110
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 ,
121
114
} in validated_emails
122
115
123
116
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 :
138
118
"""Test is_valid_email returns None on failure."""
139
- mock_validate_email .side_effect = EmailNotValidError ("The email address is not valid." )
140
119
result = analyzer .is_valid_email ("invalid-email" )
141
120
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