Skip to content

Commit 40998e1

Browse files
committed
test: included some more tests for full coverage due to heuristic changes, modified docstrings.
1 parent 24d224e commit 40998e1

12 files changed

+455
-113
lines changed

src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,15 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes
9999
maintainers_join_date: list[datetime] | None = self._get_maintainers_join_date(
100100
pypi_package_json.pypi_registry, pypi_package_json.component_name
101101
)
102+
# If there is no maintainer join date information, then it is malformed package metadata
102103
if not maintainers_join_date:
103104
error_msg = "Metadata has no maintainers or join dates for them"
104105
logger.debug(error_msg)
105106
raise HeuristicAnalyzerValueError(error_msg)
106107

107108
latest_release_date: datetime | None = self._get_latest_release_date(pypi_package_json)
109+
# Upload time is standardized by PyPI, so if it is not in the expected format then it is
110+
# malformed package metadata
108111
if not latest_release_date:
109112
error_msg = "Unable to parse latest upload time"
110113
logger.debug(error_msg)

tests/malware_analyzer/pypi/test_anomalous_version.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212

1313

1414
def test_analyze_no_information(pypi_package_json: MagicMock) -> None:
15-
"""Test for when there is no release information, so error"""
15+
"""Test for when there is no release information (should error)
16+
17+
Parameters
18+
----------
19+
pypi_package_json: MagicMock
20+
The PyPIPackageJsonAsset MagicMock fixture.
21+
"""
1622
analyzer = AnomalousVersionAnalyzer()
1723

1824
pypi_package_json.get_releases.return_value = None
@@ -22,7 +28,13 @@ def test_analyze_no_information(pypi_package_json: MagicMock) -> None:
2228

2329

2430
def test_analyze_invalid_time(pypi_package_json: MagicMock) -> None:
25-
"""Test for when the supplied upload time does not conform with PEP 440, so error."""
31+
"""Test for when the supplied upload time does not conform with PEP 440 (should error).
32+
33+
Parameters
34+
----------
35+
pypi_package_json: MagicMock
36+
The PyPIPackageJsonAsset MagicMock fixture.
37+
"""
2638
analyzer = AnomalousVersionAnalyzer()
2739
version = "1.1"
2840
release = {
@@ -60,7 +72,13 @@ def test_analyze_invalid_time(pypi_package_json: MagicMock) -> None:
6072

6173

6274
def test_analyze_no_time(pypi_package_json: MagicMock) -> None:
63-
"""Test for when there is no supplied upload time, so error."""
75+
"""Test for when there is no supplied upload time (should error).
76+
77+
Parameters
78+
----------
79+
pypi_package_json: MagicMock
80+
The PyPIPackageJsonAsset MagicMock fixture.
81+
"""
6482
analyzer = AnomalousVersionAnalyzer()
6583
version = "1.1"
6684
release = {
@@ -253,6 +271,8 @@ def test_analyze(
253271
254272
Parameters
255273
----------
274+
pypi_package_json: MagicMock
275+
The PyPIPackageJsonAsset MagicMock fixture.
256276
version : str
257277
the version number for the test package.
258278
upload_date : str
@@ -297,3 +317,47 @@ def test_analyze(
297317
actual_result = analyzer.analyze(pypi_package_json)
298318

299319
assert actual_result == expected_result
320+
321+
322+
def test_multiple_releases(pypi_package_json: MagicMock) -> None:
323+
"""Test when there are multiple releases of the package (should skip).
324+
325+
Parameters
326+
----------
327+
pypi_package_json: MagicMock
328+
The PyPIPackageJsonAsset MagicMock fixture.
329+
"""
330+
analyzer = AnomalousVersionAnalyzer()
331+
release_content = [
332+
{
333+
"comment_text": "",
334+
"digests": {
335+
"blake2b_256": "defa2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3",
336+
"md5": "9203bbb130f8ddb38269f4861c170d04",
337+
"sha256": "168bcccbf5106132e90b85659297700194369b8f6b3e5a03769614f0d200e370",
338+
},
339+
"downloads": -1,
340+
"filename": "ttttttttest_nester.py-0.1.0.tar.gz",
341+
"has_sig": False,
342+
"md5_digest": "9203bbb130f8ddb38269f4861c170d04",
343+
"packagetype": "sdist",
344+
"python_version": "source",
345+
"requires_python": None,
346+
"size": 546,
347+
"upload_time": "2016-10-13T05:42:27",
348+
"upload_time_iso_8601": "2016-10-13T05:42:27.073842Z",
349+
"url": "https://files.pythonhosted.org/packages/de/fa/"
350+
+ "2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/ttttttttest_nester.py-0.1.0.tar.gz",
351+
"yanked": False,
352+
"yanked_reason": None,
353+
}
354+
]
355+
releases = { # this can just be the same content, as it'll be skipped anyway
356+
"0.1": release_content,
357+
"0.2": release_content,
358+
}
359+
pypi_package_json.get_releases.return_value = releases
360+
expected_result: tuple[HeuristicResult, dict] = (HeuristicResult.SKIP, {})
361+
362+
actual_result = analyzer.analyze(pypi_package_json)
363+
assert actual_result == expected_result

tests/malware_analyzer/pypi/test_closer_release_join_date.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@
1212
from macaron.malware_analyzer.pypi_heuristics.metadata.closer_release_join_date import CloserReleaseJoinDateAnalyzer
1313

1414

15-
def test_analyze_pass(pypi_package_json: MagicMock) -> None:
16-
"""Test analyze method when the heuristic should pass."""
15+
def test_far_away_release_join_date(pypi_package_json: MagicMock) -> None:
16+
"""Test when the maintainer join date is far away from the upload date (should pass).
17+
18+
Parameters
19+
----------
20+
pypi_package_json: MagicMock
21+
The PyPIPackageJsonAsset MagicMock fixture.
22+
"""
1723
analyzer = CloserReleaseJoinDateAnalyzer()
1824

1925
# Set up mock return values.
@@ -31,8 +37,14 @@ def test_analyze_pass(pypi_package_json: MagicMock) -> None:
3137
assert "latest_release_date" in detail_info
3238

3339

34-
def test_analyze_process(pypi_package_json: MagicMock) -> None:
35-
"""Test analyze method when the heuristic should fail."""
40+
def test_closer_release_join_date(pypi_package_json: MagicMock) -> None:
41+
"""Test when the maintainer join date is close to the upload date (should fail).
42+
43+
Parameters
44+
----------
45+
pypi_package_json: MagicMock
46+
The PyPIPackageJsonAsset MagicMock fixture.
47+
"""
3648
analyzer = CloserReleaseJoinDateAnalyzer()
3749

3850
# Set up mock return values.
@@ -50,13 +62,45 @@ def test_analyze_process(pypi_package_json: MagicMock) -> None:
5062
assert "latest_release_date" in detail_info
5163

5264

53-
def test_analyze_no_maintainers(pypi_package_json: MagicMock) -> None:
54-
"""Test analyze method when there are no maintainers, raising an error."""
65+
def test_no_maintainers(pypi_package_json: MagicMock) -> None:
66+
"""Test when there are no maintainers (should error).
67+
68+
Parameters
69+
----------
70+
pypi_package_json: MagicMock
71+
The PyPIPackageJsonAsset MagicMock fixture.
72+
"""
5573
analyzer = CloserReleaseJoinDateAnalyzer()
5674

5775
# Set up mock return values.
5876
pypi_package_json.pypi_registry.get_maintainers_of_package.return_value = None
59-
pypi_package_json.get_latest_release_upload_time.return_value = "2022-06-20T12:00:00"
77+
pypi_package_json.component_name = "mock1"
78+
79+
# Call the method.
80+
with pytest.raises(HeuristicAnalyzerValueError):
81+
_ = analyzer.analyze(pypi_package_json)
82+
83+
84+
@pytest.mark.parametrize(
85+
("upload_time"),
86+
[pytest.param("20 June 2022 at 12pm", id="test_incorrect_format"), pytest.param(None, id="test_no_upload_time")],
87+
)
88+
def test_malformed_upload_time(pypi_package_json: MagicMock, upload_time: str | None) -> None:
89+
"""Test when the upload time is not in the expected format (should error).
90+
91+
Parameters
92+
----------
93+
pypi_package_json: MagicMock
94+
The PyPIPackageJsonAsset MagicMock fixture.
95+
upload_time: str | None
96+
The upload time for the package in any format that isn't the expected one.
97+
"""
98+
analyzer = CloserReleaseJoinDateAnalyzer()
99+
100+
# Set up mock return values.
101+
pypi_package_json.pypi_registry.get_maintainers_of_package.return_value = ["maintainer1"]
102+
pypi_package_json.pypi_registry.get_maintainer_join_date.side_effect = [datetime(2022, 6, 18)]
103+
pypi_package_json.get_latest_release_upload_time.return_value = upload_time
60104
pypi_package_json.component_name = "mock1"
61105

62106
# Call the method.

tests/malware_analyzer/pypi/test_empty_project_link_analyzer.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved.
22
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
33

44
"""Tests for heuristic detecting malicious metadata from PyPI"""
@@ -36,8 +36,14 @@ def setup_empty_project_link_analyzer() -> dict:
3636
}
3737

3838

39-
def test_analyze_no_links(empty_project_link_analyzer: dict) -> None:
40-
"""Test for result failed."""
39+
def test_no_links(empty_project_link_analyzer: dict) -> None:
40+
"""Test with no links (should fail).
41+
42+
Parameters
43+
----------
44+
empty_project_link_analyzer: dict
45+
A configured EmptyProjectLinkAnalyzer from the fixture.
46+
"""
4147
mock_pypi_package_fail = empty_project_link_analyzer["mock_pypi_package_fail"]
4248
mock_pypi_package_fail.get_project_links.return_value = {}
4349
expected_result: tuple[HeuristicResult, dict] = (HeuristicResult.FAIL, {})
@@ -47,8 +53,14 @@ def test_analyze_no_links(empty_project_link_analyzer: dict) -> None:
4753
assert result == expected_result
4854

4955

50-
def test_analyze_with_links(empty_project_link_analyzer: dict) -> None:
51-
"""Test for result passed."""
56+
def test_with_links(empty_project_link_analyzer: dict) -> None:
57+
"""Test with links present (should pass).
58+
59+
Parameters
60+
----------
61+
empty_project_link_analyzer: dict
62+
A configured EmptyProjectLinkAnalyzer from the fixture.
63+
"""
5264
package_links = empty_project_link_analyzer["package_links"]
5365
mock_pypi_package_pass = empty_project_link_analyzer["mock_pypi_package_pass"]
5466
mock_pypi_package_pass.get_project_links.return_value = package_links
@@ -57,14 +69,3 @@ def test_analyze_with_links(empty_project_link_analyzer: dict) -> None:
5769
result = empty_project_link_analyzer["analyzer"].analyze(mock_pypi_package_pass)
5870

5971
assert result == expected_result
60-
61-
62-
def test_analyze_none(empty_project_link_analyzer: dict) -> None:
63-
"""Test for result skip."""
64-
mock_pypi_package_pass = empty_project_link_analyzer["mock_pypi_package_pass"]
65-
mock_pypi_package_pass.get_project_links.return_value = None
66-
expected_result: tuple[HeuristicResult, dict] = (HeuristicResult.FAIL, {})
67-
68-
result = empty_project_link_analyzer["analyzer"].analyze(mock_pypi_package_pass)
69-
70-
assert result == expected_result

tests/malware_analyzer/pypi/test_high_release_frequency.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from macaron.malware_analyzer.pypi_heuristics.metadata.high_release_frequency import HighReleaseFrequencyAnalyzer
1313

1414

15-
def test_analyze_high_frequency_pass(pypi_package_json: MagicMock) -> None:
16-
"""Test HighReleaseFrequencyAnalyzer with low release frequency (should pass).
15+
def test_low_release_frequency(pypi_package_json: MagicMock) -> None:
16+
"""Test with low release frequency (should pass).
1717
1818
Parameters
1919
----------
@@ -37,8 +37,8 @@ def test_analyze_high_frequency_pass(pypi_package_json: MagicMock) -> None:
3737
assert detail_info == {"frequency": 9}
3838

3939

40-
def test_analyze_low_frequency_fail(pypi_package_json: MagicMock) -> None:
41-
"""Test HighReleaseFrequencyAnalyzer with high release frequency (should fail).
40+
def test_high_release_frequency(pypi_package_json: MagicMock) -> None:
41+
"""Test with high release frequency (should fail).
4242
4343
Parameters
4444
----------
@@ -62,8 +62,8 @@ def test_analyze_low_frequency_fail(pypi_package_json: MagicMock) -> None:
6262
assert detail_info == {"frequency": 1}
6363

6464

65-
def test_analyze_no_releases(pypi_package_json: MagicMock) -> None:
66-
"""Test HighReleaseFrequencyAnalyzer when no releases are available (should error for a malformed package).
65+
def test_no_releases(pypi_package_json: MagicMock) -> None:
66+
"""Test when no releases are available (should error).
6767
6868
Parameters
6969
----------
@@ -80,8 +80,8 @@ def test_analyze_no_releases(pypi_package_json: MagicMock) -> None:
8080
_ = analyzer.analyze(pypi_package_json)
8181

8282

83-
def test_analyze_single_release_skip(pypi_package_json: MagicMock) -> None:
84-
"""Test HighReleaseFrequencyAnalyzer with a single release (should skip).
83+
def test_single_release(pypi_package_json: MagicMock) -> None:
84+
"""Test with a single release (should skip).
8585
8686
Parameters
8787
----------

tests/malware_analyzer/pypi/test_one_release_analyzer.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,29 @@ def setup_one_release_analyzer() -> dict:
3232
}
3333

3434

35-
def test_analyze_no_releases(one_release_analyzer: dict) -> None:
36-
"""No release information available, should error for a malformed package."""
35+
def test_no_releases(one_release_analyzer: dict) -> None:
36+
"""No release information available (should error).
37+
38+
Parameters
39+
----------
40+
one_release_analyzer: dict
41+
A configured OneReleaseAnalyzer from the fixture.
42+
"""
3743
mock_pypi_package_pass = one_release_analyzer["mock_pypi_package_pass"]
3844
mock_pypi_package_pass.get_releases.return_value = None
3945

4046
with pytest.raises(HeuristicAnalyzerValueError):
4147
_ = one_release_analyzer["analyzer"].analyze(mock_pypi_package_pass)
4248

4349

44-
def test_analyze_one_release(one_release_analyzer: dict) -> None:
45-
"""Test for result failed."""
50+
def test_one_release(one_release_analyzer: dict) -> None:
51+
"""Test for a single release (should fail).
52+
53+
Parameters
54+
----------
55+
one_release_analyzer: dict
56+
A configured OneReleaseAnalyzer from the fixture.
57+
"""
4658
release = {
4759
"0.1.0": [
4860
{
@@ -78,8 +90,14 @@ def test_analyze_one_release(one_release_analyzer: dict) -> None:
7890
assert result == expected_result
7991

8092

81-
def test_analyze_multiple_releases(one_release_analyzer: dict) -> None:
82-
"""Test for result passed."""
93+
def test_multiple_releases(one_release_analyzer: dict) -> None:
94+
"""Test with multiple releases (should pass).
95+
96+
Parameters
97+
----------
98+
one_release_analyzer: dict
99+
A configured OneReleaseAnalyzer from the fixture.
100+
"""
83101
releases = {
84102
"0.0.1": [],
85103
"0.10.0": [

0 commit comments

Comments
 (0)