Coverage for tests / unit / test_license_checker1.py: 100%
92 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 22:28 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 22:28 +0000
1"""
2Unit tests for src/license_checker.py — written with pytest-mock.
4Advantages over the unittest.mock version (test_license_checker.py):
5 - No nested `with patch()` context managers
6 - `mocker` fixture auto-resets every mock after each test automatically
7 - Flat, readable test bodies — mock setup reads top-to-bottom
8 - Zero unittest.mock imports — mocker.mock_open / mocker.call used throughout
9"""
11import pytest
12from datetime import datetime, timedelta
14from src.license_checker import check_license
17# ---------------------------------------------------------------------------
18# helpers
19# ---------------------------------------------------------------------------
21def _date_str(days_offset: int) -> str:
22 """Return a YYYY-MM-DD date string relative to today."""
23 return (datetime.today() + timedelta(days=days_offset)).strftime("%Y-%m-%d")
26# ---------------------------------------------------------------------------
27# Valid license
28# ---------------------------------------------------------------------------
30class TestLicenseValid:
31 """License installed recently -> should be valid."""
33 def test_returns_true_when_not_expired(self, mocker):
34 mocker.patch("os.path.exists", return_value=True)
35 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-10)))
36 mock_sound = mocker.patch("src.license_checker.playsound")
38 result = check_license()
40 assert result is True
41 mock_sound.assert_not_called()
43 def test_prints_days_remaining(self, mocker, capsys):
44 mocker.patch("os.path.exists", return_value=True)
45 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-10)))
46 mocker.patch("src.license_checker.playsound")
48 check_license()
50 captured = capsys.readouterr()
51 assert "License is valid" in captured.out
52 assert "remaining" in captured.out
54 def test_valid_one_day_before_expiry(self, mocker):
55 """364 days old -> 1 day remaining -> still valid."""
56 mocker.patch("os.path.exists", return_value=True)
57 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-364)))
58 mock_sound = mocker.patch("src.license_checker.playsound")
60 result = check_license()
62 assert result is True
63 mock_sound.assert_not_called()
66# ---------------------------------------------------------------------------
67# Expired license
68# ---------------------------------------------------------------------------
70class TestLicenseExpired:
71 """License installed more than `duration` days ago -> expired."""
73 def test_returns_false_when_expired(self, mocker):
74 mocker.patch("os.path.exists", return_value=True)
75 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-400)))
76 mocker.patch("src.license_checker.playsound")
78 assert check_license() is False
80 def test_prints_expiry_message(self, mocker, capsys):
81 mocker.patch("os.path.exists", return_value=True)
82 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-400)))
83 mocker.patch("src.license_checker.playsound")
85 check_license()
87 assert "License expired" in capsys.readouterr().out
89 def test_playsound_called_exactly_twice(self, mocker):
90 mocker.patch("os.path.exists", return_value=True)
91 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-400)))
92 mock_sound = mocker.patch("src.license_checker.playsound")
94 check_license()
96 assert mock_sound.call_count == 2
98 def test_playsound_called_with_correct_file(self, mocker):
99 mocker.patch("os.path.exists", return_value=True)
100 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-400)))
101 mock_sound = mocker.patch("src.license_checker.playsound")
103 check_license()
105 mock_sound.assert_has_calls([mocker.call("alert.wav"), mocker.call("alert.wav")])
107 def test_custom_duration_expired(self, mocker):
108 """Custom shorter license (30 days) -> 31 days old -> expired."""
109 mocker.patch("os.path.exists", return_value=True)
110 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-31)))
111 mock_sound = mocker.patch("src.license_checker.playsound")
113 result = check_license(license_duration_days=30)
115 assert result is False
116 assert mock_sound.call_count == 2
118 def test_custom_duration_valid(self, mocker):
119 """Custom longer license (730 days) -> 400 days old -> still valid."""
120 mocker.patch("os.path.exists", return_value=True)
121 mocker.patch("builtins.open", mocker.mock_open(read_data=_date_str(-400)))
122 mock_sound = mocker.patch("src.license_checker.playsound")
124 result = check_license(license_duration_days=730)
126 assert result is True
127 mock_sound.assert_not_called()
130# ---------------------------------------------------------------------------
131# File not found
132# ---------------------------------------------------------------------------
134class TestLicenseFileNotFound:
135 """License file is missing -> FileNotFoundError must be raised."""
137 def test_raises_file_not_found(self, mocker):
138 mocker.patch("os.path.exists", return_value=False)
140 with pytest.raises(FileNotFoundError):
141 check_license()
143 def test_error_message_contains_filename(self, mocker):
144 mocker.patch("os.path.exists", return_value=False)
146 with pytest.raises(FileNotFoundError, match="custom/path/license.txt"):
147 check_license(license_file="custom/path/license.txt")
149 def test_playsound_not_called_when_file_missing(self, mocker):
150 mocker.patch("os.path.exists", return_value=False)
151 mock_sound = mocker.patch("src.license_checker.playsound")
153 with pytest.raises(FileNotFoundError):
154 check_license()
156 mock_sound.assert_not_called()
159# ---------------------------------------------------------------------------
160# Bad date format inside the file
161# ---------------------------------------------------------------------------
163class TestLicenseBadDateFormat:
164 """License file contains an un-parseable date -> ValueError must be raised."""
166 def test_raises_value_error_on_bad_date(self, mocker):
167 mocker.patch("os.path.exists", return_value=True)
168 mocker.patch("builtins.open", mocker.mock_open(read_data="not-a-date"))
170 with pytest.raises(ValueError, match="Invalid date format"):
171 check_license()
173 def test_raises_value_error_on_wrong_format(self, mocker):
174 mocker.patch("os.path.exists", return_value=True)
175 mocker.patch("builtins.open", mocker.mock_open(read_data="17/03/2026"))
177 with pytest.raises(ValueError):
178 check_license()