@@ -22,39 +22,6 @@ class PathTestModel(BaseModel):
2222# --- Test Cases ---
2323
2424
25- def test_writable_directory_success_exists (tmp_path : Path ):
26- """Test validation succeeds for an existing, writable directory."""
27- existing_dir = tmp_path / "existing_test_dir"
28- existing_dir .mkdir ()
29-
30- # Validation should succeed and return the custom type instance
31- model = PathTestModel (writable_dir = existing_dir ) # type: ignore
32-
33- assert isinstance (model .writable_dir , EnsureWritableDirectory )
34- assert Path (str (model .writable_dir )).resolve () == existing_dir .resolve ()
35- assert model .writable_dir .is_dir () # Test __getattr__ functionality
36-
37-
38- def test_writable_directory_success_create_new (tmp_path : Path ):
39- """Test validation succeeds and creates a new directory.
40-
41- This test ensures that if the provided path does not exist, it is
42- automatically created with the necessary parent directories.
43- """
44- new_dir = tmp_path / "non_existent" / "deep" / "path"
45-
46- # Assert precondition: Path does not exist
47- assert not new_dir .exists ()
48-
49- # Validation should trigger auto-creation
50- model = PathTestModel (writable_dir = new_dir ) # type: ignore
51-
52- # Assert postcondition: Path now exists and is a directory
53- assert model .writable_dir .exists ()
54- assert model .writable_dir .is_dir ()
55- assert Path (str (model .writable_dir )).resolve () == new_dir .resolve ()
56-
57-
5825def test_writable_directory_path_expansion (monkeypatch , tmp_path : Path ):
5926 """Test that the path correctly expands the user home directory (~)."""
6027 # 1. Setup Mock Home Directory
@@ -85,82 +52,6 @@ def test_writable_directory_failure_not_a_directory(tmp_path: Path):
8552 assert "Path exists but is not a directory" in excinfo .value .errors ()[0 ]["msg" ]
8653
8754
88- def test_writable_directory_failure_not_writable (tmp_path : Path , monkeypatch ):
89- """Test validation fails when the directory lacks write permission.
90-
91- This test is robust against being run by the root user by patching
92- ``os.access`` to simulate a write permission failure.
93- """
94- read_only_dir = tmp_path / "read_only_dir"
95- read_only_dir .mkdir ()
96-
97- _original_os_access = os .access
98-
99- # Patch os.access() to always report write permission is MISSING on this directory
100- def fake_access (path , mode ):
101- # If we are checking the specific read_only directory for WRITE
102- # permission, fail it.
103- if path == read_only_dir .resolve () and mode == os .W_OK :
104- return False
105-
106- # Otherwise, call the safely stored original function.
107- return _original_os_access (path , mode )
108-
109- monkeypatch .setattr (os , "access" , fake_access )
110-
111- # The actual chmod is now primarily symbolic, the mock forces the logic path
112- read_only_dir .chmod (0o444 )
113-
114- try :
115- with pytest .raises (ValidationError ) as excinfo :
116- PathTestModel (writable_dir = read_only_dir ) # type: ignore
117-
118- assert (
119- "Insufficient permissions for directory" in excinfo .value .errors ()[0 ]["msg" ]
120- )
121- assert "WRITE" in excinfo .value .errors ()[0 ]["msg" ]
122- finally :
123- # Restore permissions to ensure cleanup (Crucial for CI)
124- read_only_dir .chmod (0o777 )
125-
126-
127- def test_writable_directory_failure_not_executable (tmp_path : Path , monkeypatch ):
128- """Test validation fails when the directory lacks execute permission.
129-
130- This test is robust against being run by the root user by patching
131- ``os.access`` to simulate an execute/search permission failure.
132- """
133- no_exec_dir = tmp_path / "no_exec_dir"
134- no_exec_dir .mkdir ()
135-
136- _original_os_access = os .access
137-
138- # Patch os.access() to always report execute permission is MISSING
139- def fake_access (path , mode ):
140- if path == no_exec_dir .resolve () and mode == os .X_OK :
141- return False # Force failure on execute check
142-
143- # Otherwise, call the safely stored original function.
144- return _original_os_access (path , mode )
145-
146- monkeypatch .setattr (os , "access" , fake_access )
147-
148- # The actual chmod is now primarily symbolic, the mock forces the logic path
149- no_exec_dir .chmod (0o666 )
150-
151- try :
152- with pytest .raises (ValidationError ) as excinfo :
153- PathTestModel (writable_dir = no_exec_dir ) # type: ignore
154-
155- assert (
156- "Insufficient permissions for directory" in excinfo .value .errors ()[0 ]["msg" ]
157- )
158- assert "EXECUTE" in excinfo .value .errors ()[0 ]["msg" ]
159- finally :
160- # Restore permissions
161- no_exec_dir .chmod (0o777 )
162-
163-
16455def test_writable_directory_failure_mkdir_os_error (monkeypatch , tmp_path : Path ):
16556 """Test that an OSError during directory creation is handled.
16657
@@ -184,7 +75,7 @@ def mock_mkdir(*args, **kwargs):
18475 # Assert that the error is correctly wrapped in a ValueError/ValidationError
18576 error_msg = excinfo .value .errors ()[0 ]["msg" ]
18677 assert "Value error" in error_msg
187- assert "Could not create directory" in error_msg
78+ assert "Failed to create directory" in error_msg
18879 assert "Simulated permission denied" in error_msg
18980
19081
@@ -229,3 +120,76 @@ def test_fspath_protocol_compatibility(
229120 result = path_consumer (custom_path_obj )
230121 expected = expected_factory (test_dir )
231122 assert result == expected
123+
124+
125+ def test_writable_directory_failure_parent_not_writable (tmp_path : Path , monkeypatch ):
126+ """Test validation fails when the parent directory is not writable.
127+
128+ This simulates the scenario where a user tries to create a directory
129+ in a protected root folder (like /data).
130+ """
131+ # 1. Setup a "protected" parent and a target child
132+ protected_parent = tmp_path / "protected_parent"
133+ protected_parent .mkdir ()
134+ target_dir = protected_parent / "new_child_dir"
135+
136+ # 2. Mock os.access to report the parent as NOT writable
137+ _original_os_access = os .access
138+
139+ def fake_access (path , mode ):
140+ # Resolve path to ensure comparison works on all OSs
141+ if str (path ) == str (protected_parent .resolve ()) and mode == os .W_OK :
142+ return False
143+ return _original_os_access (path , mode )
144+
145+ monkeypatch .setattr (os , "access" , fake_access )
146+
147+ # 3. Action & Assertions
148+ with pytest .raises (ValidationError ) as excinfo :
149+ PathTestModel (writable_dir = target_dir ) # type: ignore
150+
151+ error_msg = excinfo .value .errors ()[0 ]["msg" ]
152+ assert "Cannot create directory" in error_msg
153+ assert "Permission denied: Parent directory" in error_msg
154+ assert str (protected_parent .resolve ()) in error_msg
155+
156+
157+ @pytest .mark .parametrize (
158+ "permission_to_fail, expected_missing_perm" ,
159+ [
160+ (os .R_OK , "READ" ),
161+ (os .W_OK , "WRITE" ),
162+ (os .X_OK , "EXECUTE" ),
163+ ],
164+ ids = ["missing_read" , "missing_write" , "missing_execute" ],
165+ )
166+ def test_writable_directory_permission_failures (
167+ tmp_path : Path , monkeypatch , permission_to_fail , expected_missing_perm
168+ ):
169+ """Test validation fails when a specific permission is missing.
170+
171+ This test is robust against being run by the root user by patching
172+ ``os.access`` to simulate a specific permission failure (R, W, or X).
173+ """
174+ test_dir = tmp_path / "permission_test_dir"
175+ test_dir .mkdir ()
176+
177+ original_os_access = os .access
178+
179+ # Patch os.access() to fail only the specified permission check for our test directory.
180+ def fake_access (path , mode ):
181+ # Resolve path to ensure comparison works reliably across OSs
182+ if Path (path ).resolve () == test_dir .resolve () and mode == permission_to_fail :
183+ return False
184+
185+ # For all other checks, or other paths, use the original behavior.
186+ return original_os_access (path , mode )
187+
188+ monkeypatch .setattr (os , "access" , fake_access )
189+
190+ with pytest .raises (ValidationError ) as excinfo :
191+ PathTestModel (writable_dir = test_dir ) # type: ignore
192+
193+ error_msg = excinfo .value .errors ()[0 ]["msg" ]
194+ assert "Insufficient permissions for directory" in error_msg
195+ assert f"Missing: { expected_missing_perm } " in error_msg
0 commit comments