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

1""" 

2Unit tests for src/license_checker.py — written with pytest-mock. 

3 

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""" 

10 

11import pytest 

12from datetime import datetime, timedelta 

13 

14from src.license_checker import check_license 

15 

16 

17# --------------------------------------------------------------------------- 

18# helpers 

19# --------------------------------------------------------------------------- 

20 

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") 

24 

25 

26# --------------------------------------------------------------------------- 

27# Valid license 

28# --------------------------------------------------------------------------- 

29 

30class TestLicenseValid: 

31 """License installed recently -> should be valid.""" 

32 

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") 

37 

38 result = check_license() 

39 

40 assert result is True 

41 mock_sound.assert_not_called() 

42 

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") 

47 

48 check_license() 

49 

50 captured = capsys.readouterr() 

51 assert "License is valid" in captured.out 

52 assert "remaining" in captured.out 

53 

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") 

59 

60 result = check_license() 

61 

62 assert result is True 

63 mock_sound.assert_not_called() 

64 

65 

66# --------------------------------------------------------------------------- 

67# Expired license 

68# --------------------------------------------------------------------------- 

69 

70class TestLicenseExpired: 

71 """License installed more than `duration` days ago -> expired.""" 

72 

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") 

77 

78 assert check_license() is False 

79 

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") 

84 

85 check_license() 

86 

87 assert "License expired" in capsys.readouterr().out 

88 

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") 

93 

94 check_license() 

95 

96 assert mock_sound.call_count == 2 

97 

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") 

102 

103 check_license() 

104 

105 mock_sound.assert_has_calls([mocker.call("alert.wav"), mocker.call("alert.wav")]) 

106 

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") 

112 

113 result = check_license(license_duration_days=30) 

114 

115 assert result is False 

116 assert mock_sound.call_count == 2 

117 

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") 

123 

124 result = check_license(license_duration_days=730) 

125 

126 assert result is True 

127 mock_sound.assert_not_called() 

128 

129 

130# --------------------------------------------------------------------------- 

131# File not found 

132# --------------------------------------------------------------------------- 

133 

134class TestLicenseFileNotFound: 

135 """License file is missing -> FileNotFoundError must be raised.""" 

136 

137 def test_raises_file_not_found(self, mocker): 

138 mocker.patch("os.path.exists", return_value=False) 

139 

140 with pytest.raises(FileNotFoundError): 

141 check_license() 

142 

143 def test_error_message_contains_filename(self, mocker): 

144 mocker.patch("os.path.exists", return_value=False) 

145 

146 with pytest.raises(FileNotFoundError, match="custom/path/license.txt"): 

147 check_license(license_file="custom/path/license.txt") 

148 

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") 

152 

153 with pytest.raises(FileNotFoundError): 

154 check_license() 

155 

156 mock_sound.assert_not_called() 

157 

158 

159# --------------------------------------------------------------------------- 

160# Bad date format inside the file 

161# --------------------------------------------------------------------------- 

162 

163class TestLicenseBadDateFormat: 

164 """License file contains an un-parseable date -> ValueError must be raised.""" 

165 

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")) 

169 

170 with pytest.raises(ValueError, match="Invalid date format"): 

171 check_license() 

172 

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")) 

176 

177 with pytest.raises(ValueError): 

178 check_license() 

179