Coverage for tests / unit / test_license_checker.py: 100%
84 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
3Covers:
4 - Valid (non-expired) license
5 - Expired license -> print + playsound called twice
6 - License file does not exist -> FileNotFoundError
7 - License file contains an invalid date -> ValueError
8 - playsound is always mocked so no audio device is needed
9"""
11import pytest
12from datetime import datetime, timedelta
13from unittest.mock import patch, mock_open, call
15from src.license_checker import check_license
18# ---------------------------------------------------------------------------
19# helpers
20# ---------------------------------------------------------------------------
22def _date_str(days_offset: int) -> str:
23 """Return a YYYY-MM-DD string relative to today."""
24 return (datetime.today() + timedelta(days=days_offset)).strftime("%Y-%m-%d")
27# ---------------------------------------------------------------------------
28# Valid license
29# ---------------------------------------------------------------------------
31class TestLicenseValid:
32 """License installed recently -> should be valid."""
34 def test_returns_true_when_not_expired(self):
35 installed = _date_str(-10) # installed 10 days ago -> 355 days left
36 with patch("os.path.exists", return_value=True), \
37 patch("builtins.open", mock_open(read_data=installed)), \
38 patch("src.license_checker.playsound") as mock_sound:
40 result = check_license()
42 assert result is True
43 mock_sound.assert_not_called()
45 def test_prints_days_remaining(self, capsys):
46 installed = _date_str(-10)
47 with patch("os.path.exists", return_value=True), \
48 patch("builtins.open", mock_open(read_data=installed)), \
49 patch("src.license_checker.playsound"):
51 check_license()
53 captured = capsys.readouterr()
54 assert "License is valid" in captured.out
55 assert "remaining" in captured.out
57 def test_valid_one_day_before_expiry(self):
58 """Installed 364 days ago -> 1 day left -> still valid.
60 Note: datetime.today() carries the current wall-clock time, so an
61 install date of exactly -365 days produces expiry_date at midnight
62 *today*, which is already in the past. Using -364 guarantees there
63 is always at least one day remaining regardless of time-of-day.
64 """
65 installed = _date_str(-364)
66 with patch("os.path.exists", return_value=True), \
67 patch("builtins.open", mock_open(read_data=installed)), \
68 patch("src.license_checker.playsound") as mock_sound:
70 result = check_license()
72 assert result is True
73 mock_sound.assert_not_called()
76# ---------------------------------------------------------------------------
77# Expired license
78# ---------------------------------------------------------------------------
80class TestLicenseExpired:
81 """License installed more than `duration` days ago -> expired."""
83 def test_returns_false_when_expired(self):
84 installed = _date_str(-400)
85 with patch("os.path.exists", return_value=True), \
86 patch("builtins.open", mock_open(read_data=installed)), \
87 patch("src.license_checker.playsound"):
89 result = check_license()
91 assert result is False
93 def test_prints_expiry_message(self, capsys):
94 installed = _date_str(-400)
95 with patch("os.path.exists", return_value=True), \
96 patch("builtins.open", mock_open(read_data=installed)), \
97 patch("src.license_checker.playsound"):
99 check_license()
101 captured = capsys.readouterr()
102 assert "License expired" in captured.out
104 def test_playsound_called_exactly_twice(self):
105 installed = _date_str(-400)
106 with patch("os.path.exists", return_value=True), \
107 patch("builtins.open", mock_open(read_data=installed)), \
108 patch("src.license_checker.playsound") as mock_sound:
110 check_license()
112 assert mock_sound.call_count == 2
114 def test_playsound_called_with_correct_file(self):
115 installed = _date_str(-400)
116 with patch("os.path.exists", return_value=True), \
117 patch("builtins.open", mock_open(read_data=installed)), \
118 patch("src.license_checker.playsound") as mock_sound:
120 check_license()
122 mock_sound.assert_has_calls([call("alert.wav"), call("alert.wav")])
124 def test_custom_duration_expired(self):
125 """Custom shorter license (30 days) -> 31 days old -> expired."""
126 installed = _date_str(-31)
127 with patch("os.path.exists", return_value=True), \
128 patch("builtins.open", mock_open(read_data=installed)), \
129 patch("src.license_checker.playsound") as mock_sound:
131 result = check_license(license_duration_days=30)
133 assert result is False
134 assert mock_sound.call_count == 2
136 def test_custom_duration_valid(self):
137 """Custom longer license (730 days) -> 400 days old -> still valid."""
138 installed = _date_str(-400)
139 with patch("os.path.exists", return_value=True), \
140 patch("builtins.open", mock_open(read_data=installed)), \
141 patch("src.license_checker.playsound") as mock_sound:
143 result = check_license(license_duration_days=730)
145 assert result is True
146 mock_sound.assert_not_called()
149# ---------------------------------------------------------------------------
150# File not found
151# ---------------------------------------------------------------------------
153class TestLicenseFileNotFound:
154 """License file is missing -> FileNotFoundError must be raised."""
156 def test_raises_file_not_found(self):
157 with patch("os.path.exists", return_value=False):
158 with pytest.raises(FileNotFoundError):
159 check_license()
161 def test_error_message_contains_filename(self):
162 custom_path = "custom/path/license.txt"
163 with patch("os.path.exists", return_value=False):
164 with pytest.raises(FileNotFoundError, match="custom/path/license.txt"):
165 check_license(license_file=custom_path)
167 def test_playsound_not_called_when_file_missing(self):
168 with patch("os.path.exists", return_value=False), \
169 patch("src.license_checker.playsound") as mock_sound:
171 with pytest.raises(FileNotFoundError):
172 check_license()
174 mock_sound.assert_not_called()
177# ---------------------------------------------------------------------------
178# Bad date format inside the file
179# ---------------------------------------------------------------------------
181class TestLicenseBadDateFormat:
182 """License file contains an un-parseable date -> ValueError must be raised."""
184 def test_raises_value_error_on_bad_date(self):
185 with patch("os.path.exists", return_value=True), \
186 patch("builtins.open", mock_open(read_data="not-a-date")):
187 with pytest.raises(ValueError, match="Invalid date format"):
188 check_license()
190 def test_raises_value_error_on_wrong_format(self):
191 with patch("os.path.exists", return_value=True), \
192 patch("builtins.open", mock_open(read_data="17/03/2026")):
193 with pytest.raises(ValueError):
194 check_license()