11import json
22import pathlib
3- from unittest .mock import MagicMock , patch
3+ import sys
4+ import time
5+ from unittest .mock import patch
46
57import pytest
68from typer .testing import CliRunner
79
810from rendercv import __version__
9- from rendercv .cli .app import app , warn_if_new_version_is_available
11+ from rendercv .cli .app import (
12+ VERSION_CHECK_TTL_SECONDS ,
13+ app ,
14+ get_cache_dir ,
15+ read_version_cache ,
16+ warn_if_new_version_is_available ,
17+ write_version_cache ,
18+ )
1019
1120
1221def test_all_commands_are_registered ():
@@ -49,6 +58,96 @@ def test_shows_help_when_no_args(self, mock_warn):
4958 mock_warn .assert_called_once ()
5059
5160
61+ class TestGetCacheDir :
62+ def test_returns_platform_appropriate_path (self ):
63+ cache_dir = get_cache_dir ()
64+
65+ assert cache_dir .name == "rendercv"
66+ if sys .platform == "darwin" :
67+ assert "Library/Caches" in str (cache_dir )
68+ elif sys .platform == "win32" :
69+ assert "Local" in str (cache_dir )
70+
71+ def test_respects_xdg_cache_home_on_linux (self , tmp_path , monkeypatch ):
72+ monkeypatch .setattr ("rendercv.cli.app.sys.platform" , "linux" )
73+ monkeypatch .setenv ("XDG_CACHE_HOME" , str (tmp_path ))
74+
75+ assert get_cache_dir () == tmp_path / "rendercv"
76+
77+
78+ class TestReadVersionCache :
79+ def test_returns_none_for_missing_file (self , tmp_path , monkeypatch ):
80+ monkeypatch .setattr (
81+ "rendercv.cli.app.get_version_cache_file" ,
82+ lambda : tmp_path / "nonexistent.json" ,
83+ )
84+
85+ assert read_version_cache () is None
86+
87+ def test_returns_none_for_corrupt_file (self , tmp_path , monkeypatch ):
88+ cache_file = tmp_path / "version_check.json"
89+ cache_file .write_text ("not valid json!!!" , encoding = "utf-8" )
90+ monkeypatch .setattr (
91+ "rendercv.cli.app.get_version_cache_file" ,
92+ lambda : cache_file ,
93+ )
94+
95+ assert read_version_cache () is None
96+
97+ def test_returns_none_for_incomplete_data (self , tmp_path , monkeypatch ):
98+ cache_file = tmp_path / "version_check.json"
99+ cache_file .write_text (json .dumps ({"latest_version" : "1.0" }), encoding = "utf-8" )
100+ monkeypatch .setattr (
101+ "rendercv.cli.app.get_version_cache_file" ,
102+ lambda : cache_file ,
103+ )
104+
105+ assert read_version_cache () is None
106+
107+ def test_returns_data_for_valid_cache (self , tmp_path , monkeypatch ):
108+ cache_file = tmp_path / "version_check.json"
109+ cache_data = {"last_check" : time .time (), "latest_version" : "2.0.0" }
110+ cache_file .write_text (json .dumps (cache_data ), encoding = "utf-8" )
111+ monkeypatch .setattr (
112+ "rendercv.cli.app.get_version_cache_file" ,
113+ lambda : cache_file ,
114+ )
115+
116+ result = read_version_cache ()
117+
118+ assert result ["latest_version" ] == "2.0.0"
119+
120+
121+ class TestWriteVersionCache :
122+ def test_creates_cache_file (self , tmp_path , monkeypatch ):
123+ cache_file = tmp_path / "subdir" / "version_check.json"
124+ monkeypatch .setattr (
125+ "rendercv.cli.app.get_version_cache_file" ,
126+ lambda : cache_file ,
127+ )
128+
129+ write_version_cache ("2.0.0" )
130+
131+ data = json .loads (cache_file .read_text (encoding = "utf-8" ))
132+ assert data ["latest_version" ] == "2.0.0"
133+ assert "last_check" in data
134+
135+
136+ def write_cache (tmp_path , version , age_seconds = 0 ):
137+ """Helper to write a version cache file for testing."""
138+ cache_file = tmp_path / "version_check.json"
139+ cache_file .write_text (
140+ json .dumps (
141+ {
142+ "last_check" : time .time () - age_seconds ,
143+ "latest_version" : version ,
144+ }
145+ ),
146+ encoding = "utf-8" ,
147+ )
148+ return cache_file
149+
150+
52151class TestWarnIfNewVersionIsAvailable :
53152 @pytest .mark .parametrize (
54153 ("version" , "should_warn" ),
@@ -58,17 +157,14 @@ class TestWarnIfNewVersionIsAvailable:
58157 (__version__ , False ),
59158 ],
60159 )
61- @patch ("urllib.request.urlopen" )
62- def test_warns_when_newer_version_available (
63- self , mock_urlopen , version , should_warn , capsys
160+ def test_warns_from_fresh_cache (
161+ self , version , should_warn , tmp_path , capsys , monkeypatch
64162 ):
65- mock_response = MagicMock ()
66- mock_response .read .return_value = json .dumps (
67- {"info" : {"version" : version }}
68- ).encode ("utf-8" )
69- mock_response .info .return_value .get_content_charset .return_value = "utf-8"
70- mock_response .__enter__ .return_value = mock_response
71- mock_urlopen .return_value = mock_response
163+ write_cache (tmp_path , version , age_seconds = 0 )
164+ monkeypatch .setattr (
165+ "rendercv.cli.app.get_version_cache_file" ,
166+ lambda : tmp_path / "version_check.json" ,
167+ )
72168
73169 warn_if_new_version_is_available ()
74170
@@ -78,11 +174,68 @@ def test_warns_when_newer_version_available(
78174 else :
79175 assert "new version" not in captured .out .lower ()
80176
81- @patch ("urllib.request.urlopen" )
82- def test_handles_network_errors_gracefully (self , mock_urlopen , capsys ):
83- mock_urlopen .side_effect = Exception ("Network error" )
177+ @patch ("rendercv.cli.app.fetch_latest_version_from_pypi" )
178+ def test_fresh_cache_does_not_fetch (self , mock_fetch , tmp_path , monkeypatch ):
179+ write_cache (tmp_path , "99.0.0" , age_seconds = 0 )
180+ monkeypatch .setattr (
181+ "rendercv.cli.app.get_version_cache_file" ,
182+ lambda : tmp_path / "version_check.json" ,
183+ )
184+
185+ warn_if_new_version_is_available ()
186+
187+ mock_fetch .assert_not_called ()
188+
189+ @patch ("rendercv.cli.app.fetch_latest_version_from_pypi" , return_value = "99.0.0" )
190+ def test_stale_cache_warns_from_stale_data_and_refreshes (
191+ self , mock_fetch , tmp_path , capsys , monkeypatch
192+ ):
193+ write_cache (tmp_path , "98.0.0" , age_seconds = VERSION_CHECK_TTL_SECONDS + 1 )
194+ monkeypatch .setattr (
195+ "rendercv.cli.app.get_version_cache_file" ,
196+ lambda : tmp_path / "version_check.json" ,
197+ )
198+
199+ warn_if_new_version_is_available ()
200+
201+ captured = capsys .readouterr ()
202+ assert "new version" in captured .out .lower ()
203+ mock_fetch .assert_called_once ()
204+
205+ @patch ("rendercv.cli.app.fetch_latest_version_from_pypi" , return_value = "99.0.0" )
206+ def test_missing_cache_shows_no_warning_and_refreshes (
207+ self ,
208+ mock_fetch ,
209+ tmp_path ,
210+ capsys ,
211+ monkeypatch ,
212+ ):
213+ monkeypatch .setattr (
214+ "rendercv.cli.app.get_version_cache_file" ,
215+ lambda : tmp_path / "version_check.json" ,
216+ )
84217
85218 warn_if_new_version_is_available ()
86219
87220 captured = capsys .readouterr ()
88221 assert "new version" not in captured .out .lower ()
222+ mock_fetch .assert_called_once ()
223+
224+ @patch ("rendercv.cli.app.fetch_latest_version_from_pypi" , return_value = None )
225+ def test_network_failure_preserves_existing_cache (
226+ self ,
227+ mock_fetch , # NOQA: ARG002
228+ tmp_path ,
229+ monkeypatch ,
230+ ):
231+ write_cache (tmp_path , "99.0.0" , age_seconds = VERSION_CHECK_TTL_SECONDS + 1 )
232+ cache_file = tmp_path / "version_check.json"
233+ monkeypatch .setattr (
234+ "rendercv.cli.app.get_version_cache_file" ,
235+ lambda : cache_file ,
236+ )
237+
238+ warn_if_new_version_is_available ()
239+
240+ data = json .loads (cache_file .read_text (encoding = "utf-8" ))
241+ assert data ["latest_version" ] == "99.0.0"
0 commit comments