spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / examples / hooks / test_fix_file_permissions.py
278 lines · 9.7 KBRaw
1#!/usr/bin/env python3
2"""
3Tests for the fix_file_permissions_example.py hook.
4
5Run these tests with:
6 python3 examples/hooks/test_fix_file_permissions.py
7
8Or with pytest:
9 pytest examples/hooks/test_fix_file_permissions.py -v
10"""
11
12import json
13import os
14import stat
15import subprocess
16import sys
17import tempfile
18import unittest
19from pathlib import Path
20
21# Path to the hook script
22HOOK_SCRIPT = Path(__file__).parent / "fix_file_permissions_example.py"
23
24
25class TestFixFilePermissionsHook(unittest.TestCase):
26 """Test cases for the file permissions fix hook."""
27
28 def setUp(self):
29 """Set up test fixtures."""
30 self.temp_dir = tempfile.mkdtemp()
31 self.test_file = os.path.join(self.temp_dir, "test_file.txt")
32
33 def tearDown(self):
34 """Clean up test files."""
35 if os.path.exists(self.test_file):
36 os.remove(self.test_file)
37 if os.path.exists(self.temp_dir):
38 os.rmdir(self.temp_dir)
39
40 def run_hook(self, tool_name: str, file_path: str) -> subprocess.CompletedProcess:
41 """Run the hook script with given input."""
42 input_data = {
43 "tool_name": tool_name,
44 "tool_input": {"file_path": file_path},
45 "session_id": "test-session",
46 "cwd": os.getcwd(),
47 }
48
49 result = subprocess.run(
50 [sys.executable, str(HOOK_SCRIPT)],
51 input=json.dumps(input_data),
52 capture_output=True,
53 text=True,
54 )
55 return result
56
57 def create_file_with_permissions(self, path: str, mode: int) -> None:
58 """Create a test file with specific permissions."""
59 with open(path, "w") as f:
60 f.write("test content")
61 os.chmod(path, mode)
62
63 def get_file_permissions(self, path: str) -> int:
64 """Get the permission bits of a file."""
65 return stat.S_IMODE(os.stat(path).st_mode)
66
67 def test_fixes_restrictive_permissions_with_umask_022(self):
68 """Test that 0600 permissions are fixed to 0644 with umask 022."""
69 # Save and set umask
70 old_umask = os.umask(0o022)
71 try:
72 # Create file with restrictive permissions (simulating Write tool bug)
73 self.create_file_with_permissions(self.test_file, 0o600)
74 self.assertEqual(self.get_file_permissions(self.test_file), 0o600)
75
76 # Run the hook
77 result = self.run_hook("Write", self.test_file)
78 self.assertEqual(result.returncode, 0)
79
80 # Check permissions were fixed
81 expected_mode = 0o644 # 0666 & ~0022
82 self.assertEqual(
83 self.get_file_permissions(self.test_file),
84 expected_mode,
85 f"Expected {oct(expected_mode)}, got {oct(self.get_file_permissions(self.test_file))}",
86 )
87 finally:
88 os.umask(old_umask)
89
90 def test_fixes_restrictive_permissions_with_umask_002(self):
91 """Test that 0600 permissions are fixed to 0664 with umask 002."""
92 # Save and set umask
93 old_umask = os.umask(0o002)
94 try:
95 # Create file with restrictive permissions
96 self.create_file_with_permissions(self.test_file, 0o600)
97 self.assertEqual(self.get_file_permissions(self.test_file), 0o600)
98
99 # Run the hook
100 result = self.run_hook("Write", self.test_file)
101 self.assertEqual(result.returncode, 0)
102
103 # Check permissions were fixed
104 expected_mode = 0o664 # 0666 & ~0002
105 self.assertEqual(
106 self.get_file_permissions(self.test_file),
107 expected_mode,
108 f"Expected {oct(expected_mode)}, got {oct(self.get_file_permissions(self.test_file))}",
109 )
110 finally:
111 os.umask(old_umask)
112
113 def test_preserves_permissions_matching_umask(self):
114 """Test that permissions already matching umask are not changed."""
115 old_umask = os.umask(0o022)
116 try:
117 # Create file with correct permissions already
118 self.create_file_with_permissions(self.test_file, 0o644)
119
120 # Run the hook
121 result = self.run_hook("Write", self.test_file)
122 self.assertEqual(result.returncode, 0)
123
124 # Permissions should be unchanged
125 self.assertEqual(self.get_file_permissions(self.test_file), 0o644)
126 finally:
127 os.umask(old_umask)
128
129 def test_respects_umask_077(self):
130 """Test that umask 077 results in 0600 (no change needed)."""
131 old_umask = os.umask(0o077)
132 try:
133 # Create file with 0600 permissions
134 self.create_file_with_permissions(self.test_file, 0o600)
135
136 # Run the hook
137 result = self.run_hook("Write", self.test_file)
138 self.assertEqual(result.returncode, 0)
139
140 # With umask 077, 0600 is correct - should remain unchanged
141 self.assertEqual(self.get_file_permissions(self.test_file), 0o600)
142 finally:
143 os.umask(old_umask)
144
145 def test_handles_edit_tool(self):
146 """Test that the hook also works for the Edit tool."""
147 old_umask = os.umask(0o022)
148 try:
149 self.create_file_with_permissions(self.test_file, 0o600)
150
151 result = self.run_hook("Edit", self.test_file)
152 self.assertEqual(result.returncode, 0)
153
154 self.assertEqual(self.get_file_permissions(self.test_file), 0o644)
155 finally:
156 os.umask(old_umask)
157
158 def test_ignores_other_tools(self):
159 """Test that the hook ignores non-Write/Edit tools."""
160 old_umask = os.umask(0o022)
161 try:
162 self.create_file_with_permissions(self.test_file, 0o600)
163
164 result = self.run_hook("Read", self.test_file)
165 self.assertEqual(result.returncode, 0)
166
167 # Permissions should be unchanged for Read tool
168 self.assertEqual(self.get_file_permissions(self.test_file), 0o600)
169 finally:
170 os.umask(old_umask)
171
172 def test_handles_nonexistent_file(self):
173 """Test that the hook handles non-existent files gracefully."""
174 result = self.run_hook("Write", "/nonexistent/path/file.txt")
175 self.assertEqual(result.returncode, 0)
176
177 def test_handles_empty_file_path(self):
178 """Test that the hook handles empty file path gracefully."""
179 input_data = {
180 "tool_name": "Write",
181 "tool_input": {},
182 "session_id": "test-session",
183 }
184
185 result = subprocess.run(
186 [sys.executable, str(HOOK_SCRIPT)],
187 input=json.dumps(input_data),
188 capture_output=True,
189 text=True,
190 )
191 self.assertEqual(result.returncode, 0)
192
193 def test_handles_invalid_json(self):
194 """Test that the hook handles invalid JSON input gracefully."""
195 result = subprocess.run(
196 [sys.executable, str(HOOK_SCRIPT)],
197 input="not valid json",
198 capture_output=True,
199 text=True,
200 )
201 # Should exit 0 even with invalid input (don't block the workflow)
202 self.assertEqual(result.returncode, 0)
203 self.assertIn("Invalid JSON", result.stderr)
204
205 def test_handles_directory_path(self):
206 """Test that the hook ignores directory paths."""
207 result = self.run_hook("Write", self.temp_dir)
208 self.assertEqual(result.returncode, 0)
209
210 def test_outputs_system_message_on_fix(self):
211 """Test that the hook outputs a systemMessage when fixing permissions."""
212 old_umask = os.umask(0o022)
213 try:
214 self.create_file_with_permissions(self.test_file, 0o600)
215
216 result = self.run_hook("Write", self.test_file)
217 self.assertEqual(result.returncode, 0)
218
219 # Check that stdout contains the systemMessage JSON
220 if result.stdout.strip():
221 output = json.loads(result.stdout)
222 self.assertIn("systemMessage", output)
223 self.assertIn("Fixed file permissions", output["systemMessage"])
224 finally:
225 os.umask(old_umask)
226
227
228class TestCalculateFilePermissions(unittest.TestCase):
229 """Test the calculate_file_permissions function directly."""
230
231 def test_umask_022(self):
232 """Test permission calculation with umask 022."""
233 # Import the function from the hook script
234 import importlib.util
235
236 spec = importlib.util.spec_from_file_location("hook", HOOK_SCRIPT)
237 hook = importlib.util.module_from_spec(spec)
238 spec.loader.exec_module(hook)
239
240 result = hook.calculate_file_permissions(0o022)
241 self.assertEqual(result, 0o644)
242
243 def test_umask_002(self):
244 """Test permission calculation with umask 002."""
245 import importlib.util
246
247 spec = importlib.util.spec_from_file_location("hook", HOOK_SCRIPT)
248 hook = importlib.util.module_from_spec(spec)
249 spec.loader.exec_module(hook)
250
251 result = hook.calculate_file_permissions(0o002)
252 self.assertEqual(result, 0o664)
253
254 def test_umask_077(self):
255 """Test permission calculation with umask 077."""
256 import importlib.util
257
258 spec = importlib.util.spec_from_file_location("hook", HOOK_SCRIPT)
259 hook = importlib.util.module_from_spec(spec)
260 spec.loader.exec_module(hook)
261
262 result = hook.calculate_file_permissions(0o077)
263 self.assertEqual(result, 0o600)
264
265 def test_umask_000(self):
266 """Test permission calculation with umask 000."""
267 import importlib.util
268
269 spec = importlib.util.spec_from_file_location("hook", HOOK_SCRIPT)
270 hook = importlib.util.module_from_spec(spec)
271 spec.loader.exec_module(hook)
272
273 result = hook.calculate_file_permissions(0o000)
274 self.assertEqual(result, 0o666)
275
276
277if __name__ == "__main__":
278 unittest.main()