22
33import importlib .util
44import os
5+ import tempfile
6+ from pathlib import Path
57
68import requests
79from unittest import TestCase
8- from unittest .mock import mock_open , patch
10+ from unittest .mock import patch
911
1012# The script has a hyphenated filename, so we need importlib to load it
1113_SCRIPT_PATH = os .path .join (
@@ -37,28 +39,36 @@ class TestHashFile(TestCase):
3739
3840 def test_returns_sha256_hex (self ):
3941 """hash_file returns a 64-char hex string."""
40- with patch ("builtins.open" , mock_open (read_data = b"cert-data" )):
41- result = restart_on_ca_change .hash_file ("/tmp/secrets/ca.crt" )
42+ with tempfile .NamedTemporaryFile () as f :
43+ f .write (b"cert-data" )
44+ f .flush ()
45+ result = restart_on_ca_change .hash_file (Path (f .name ))
4246 self .assertIsInstance (result , str )
4347 self .assertEqual (len (result ), 64 )
4448
4549 def test_different_content_different_hash (self ):
4650 """hash_file returns different hashes for different file contents."""
47- with patch ("builtins.open" , mock_open (read_data = b"cert-v1" )):
48- hash_v1 = restart_on_ca_change .hash_file ("/tmp/secrets/ca.crt" )
51+ with tempfile .NamedTemporaryFile () as f1 , tempfile .NamedTemporaryFile () as f2 :
52+ f1 .write (b"cert-v1" )
53+ f1 .flush ()
54+ hash_v1 = restart_on_ca_change .hash_file (Path (f1 .name ))
4955
50- with patch ("builtins.open" , mock_open (read_data = b"cert-v2" )):
51- hash_v2 = restart_on_ca_change .hash_file ("/tmp/secrets/ca.crt" )
56+ f2 .write (b"cert-v2" )
57+ f2 .flush ()
58+ hash_v2 = restart_on_ca_change .hash_file (Path (f2 .name ))
5259
5360 self .assertNotEqual (hash_v1 , hash_v2 )
5461
5562 def test_same_content_same_hash (self ):
5663 """hash_file returns the same hash for the same content."""
57- with patch ("builtins.open" , mock_open (read_data = b"cert-data" )):
58- hash_1 = restart_on_ca_change .hash_file ("/tmp/secrets/ca.crt" )
64+ with tempfile .NamedTemporaryFile () as f1 , tempfile .NamedTemporaryFile () as f2 :
65+ f1 .write (b"cert-data" )
66+ f1 .flush ()
67+ hash_1 = restart_on_ca_change .hash_file (Path (f1 .name ))
5968
60- with patch ("builtins.open" , mock_open (read_data = b"cert-data" )):
61- hash_2 = restart_on_ca_change .hash_file ("/tmp/secrets/ca.crt" )
69+ f2 .write (b"cert-data" )
70+ f2 .flush ()
71+ hash_2 = restart_on_ca_change .hash_file (Path (f2 .name ))
6272
6373 self .assertEqual (hash_1 , hash_2 )
6474
@@ -67,49 +77,54 @@ def test_same_content_same_hash(self):
6777class TestMain (TestCase ):
6878 """Tests for main function."""
6979
70- @patch ("os.path.exists" , return_value = False )
71- def test_ca_file_missing_skips (self , _mock_exists ):
80+ def test_ca_file_missing_skips (self ):
7281 """main skips when CA file does not exist."""
73- with patch ("builtins.print" ) as mock_print :
74- restart_on_ca_change .main ()
75- mock_print .assert_called_once_with (
76- "CA file /tmp/secrets/ca.crt does not exist, skipping"
77- )
82+ with tempfile .TemporaryDirectory () as tmpdir :
83+ env = {** ENV_VARS , "CA_DIR" : tmpdir }
84+ with patch .dict (os .environ , env ), patch ("builtins.print" ) as mock_print :
85+ restart_on_ca_change .main ()
86+ mock_print .assert_called_once_with (
87+ f"CA file { Path (tmpdir ) / 'ca.crt' } does not exist, skipping"
88+ )
7889
7990 def test_initial_load_skips_restart (self ):
8091 """main writes hash and skips restart on initial load."""
81- # ca_file exists, hash_file_path does not
82- with patch (
83- "os.path.exists" , side_effect = lambda p : p == "/tmp/secrets/ca.crt"
84- ), patch .object (
85- restart_on_ca_change , "hash_file" , return_value = "abc123"
86- ), patch (
87- "builtins.open" , mock_open ()
88- ), patch (
89- "builtins.print"
90- ) as mock_print :
91- restart_on_ca_change .main ()
92- mock_print .assert_called_once_with ("Initial CA load, skipping restart" )
92+ with tempfile .TemporaryDirectory () as tmpdir :
93+ ca_file = Path (tmpdir ) / "ca.crt"
94+ ca_file .write_bytes (b"cert-data" )
95+ env = {** ENV_VARS , "CA_DIR" : tmpdir }
96+ with patch .dict (os .environ , env ), patch ("builtins.print" ) as mock_print :
97+ restart_on_ca_change .main ()
98+ mock_print .assert_called_once_with ("Initial CA load, skipping restart" )
99+ # Hash file should have been created
100+ hash_file = Path (tmpdir ) / ".ca-hash-previous"
101+ self .assertTrue (hash_file .exists ())
93102
94103 def test_unchanged_hash_no_restart (self ):
95104 """main does nothing when hash has not changed."""
96- with patch ("os.path.exists" , return_value = True ), patch .object (
97- restart_on_ca_change , "hash_file" , return_value = "same-hash"
98- ), patch ("builtins.open" , mock_open (read_data = "same-hash" )), patch (
99- "builtins.print"
100- ) as mock_print :
101- restart_on_ca_change .main ()
102- mock_print .assert_not_called ()
105+ with tempfile .TemporaryDirectory () as tmpdir :
106+ ca_file = Path (tmpdir ) / "ca.crt"
107+ ca_file .write_bytes (b"cert-data" )
108+ # Pre-compute and write the hash
109+ current_hash = restart_on_ca_change .hash_file (ca_file )
110+ hash_file = Path (tmpdir ) / ".ca-hash-previous"
111+ hash_file .write_text (current_hash )
112+ env = {** ENV_VARS , "CA_DIR" : tmpdir }
113+ with patch .dict (os .environ , env ), patch ("builtins.print" ) as mock_print :
114+ restart_on_ca_change .main ()
115+ mock_print .assert_not_called ()
103116
104117 @patch .object (restart_on_ca_change , "trigger_restart" )
105118 def test_changed_hash_triggers_restart (self , mock_restart ):
106119 """main triggers restart when hash has changed."""
107- with patch ("os.path.exists" , return_value = True ), patch .object (
108- restart_on_ca_change , "hash_file" , return_value = "new-hash"
109- ), patch ("builtins.open" , mock_open (read_data = "old-hash" )), patch (
110- "builtins.print"
111- ) as mock_print :
112- restart_on_ca_change .main ()
120+ with tempfile .TemporaryDirectory () as tmpdir :
121+ ca_file = Path (tmpdir ) / "ca.crt"
122+ ca_file .write_bytes (b"cert-data" )
123+ hash_file = Path (tmpdir ) / ".ca-hash-previous"
124+ hash_file .write_text ("old-hash" )
125+ env = {** ENV_VARS , "CA_DIR" : tmpdir }
126+ with patch .dict (os .environ , env ), patch ("builtins.print" ) as mock_print :
127+ restart_on_ca_change .main ()
113128
114129 mock_restart .assert_called_once_with (
115130 "metalk8s-monitoring" , "oauth2-proxy-prometheus"
@@ -119,62 +134,63 @@ def test_changed_hash_triggers_restart(self, mock_restart):
119134 mock_print .call_args [0 ][0 ],
120135 )
121136
122- @patch ("builtins.print" )
123137 @patch .object (
124138 restart_on_ca_change ,
125139 "trigger_restart" ,
126140 side_effect = requests .RequestException ("refused" ),
127141 )
128- @patch .object (restart_on_ca_change , "hash_file" , return_value = "new-hash" )
129- @patch ("os.path.exists" , return_value = True )
130- def test_api_failure_exits_with_error (
131- self , _mock_exists , _mock_hash , _mock_restart , _mock_print
132- ):
142+ def test_api_failure_exits_with_error (self , _mock_restart ):
133143 """main exits with code 1 when the API call fails."""
134- with patch ("builtins.open" , mock_open (read_data = "old-hash" )):
135- with self .assertRaises (SystemExit ) as ctx :
136- restart_on_ca_change .main ()
144+ with tempfile .TemporaryDirectory () as tmpdir :
145+ ca_file = Path (tmpdir ) / "ca.crt"
146+ ca_file .write_bytes (b"cert-data" )
147+ hash_file = Path (tmpdir ) / ".ca-hash-previous"
148+ hash_file .write_text ("old-hash" )
149+ env = {** ENV_VARS , "CA_DIR" : tmpdir }
150+ with patch .dict (os .environ , env ), patch ("builtins.print" ):
151+ with self .assertRaises (SystemExit ) as ctx :
152+ restart_on_ca_change .main ()
137153
138154 self .assertEqual (ctx .exception .code , 1 )
139155
140- @patch ("builtins.print" )
141156 @patch .object (
142157 restart_on_ca_change ,
143158 "trigger_restart" ,
144159 side_effect = requests .RequestException ("refused" ),
145160 )
146- @patch .object (restart_on_ca_change , "hash_file" , return_value = "new-hash" )
147- @patch ("os.path.exists" , return_value = True )
148- def test_hash_not_persisted_on_api_failure (
149- self , _mock_exists , _mock_hash , _mock_restart , _mock_print
150- ):
161+ def test_hash_not_persisted_on_api_failure (self , _mock_restart ):
151162 """Hash file is not updated when the API call fails."""
152- mock_file = mock_open (read_data = "old-hash" )
153-
154- with patch ("builtins.open" , mock_file ):
155- try :
156- restart_on_ca_change .main ()
157- except SystemExit :
158- pass
159-
160- # Only the hash file read should have happened, no write
161- write_calls = mock_file ().write .call_args_list
162- self .assertEqual (len (write_calls ), 0 )
163+ with tempfile .TemporaryDirectory () as tmpdir :
164+ ca_file = Path (tmpdir ) / "ca.crt"
165+ ca_file .write_bytes (b"cert-data" )
166+ hash_file = Path (tmpdir ) / ".ca-hash-previous"
167+ hash_file .write_text ("old-hash" )
168+ env = {** ENV_VARS , "CA_DIR" : tmpdir }
169+ with patch .dict (os .environ , env ), patch ("builtins.print" ):
170+ try :
171+ restart_on_ca_change .main ()
172+ except SystemExit :
173+ pass
174+
175+ # Hash file should still contain the old hash
176+ self .assertEqual (hash_file .read_text (), "old-hash" )
163177
164178
165179class TestTriggerRestart (TestCase ):
166180 """Tests for trigger_restart function."""
167181
168182 @patch .object (restart_on_ca_change .requests , "patch" )
169- @patch (
170- "builtins.open" ,
171- mock_open (read_data = "fake-token" ),
172- )
173183 def test_sends_patch_request (self , mock_patch ):
174184 """trigger_restart sends a PATCH to the K8s API."""
175- restart_on_ca_change .trigger_restart (
176- "metalk8s-monitoring" , "oauth2-proxy-prometheus"
177- )
185+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".token" ) as f :
186+ f .write ("fake-token" )
187+ f .flush ()
188+ with patch .object (
189+ restart_on_ca_change , "SA_TOKEN" , Path (f .name )
190+ ):
191+ restart_on_ca_change .trigger_restart (
192+ "metalk8s-monitoring" , "oauth2-proxy-prometheus"
193+ )
178194
179195 mock_patch .assert_called_once ()
180196 args , kwargs = mock_patch .call_args
0 commit comments