@@ -42,7 +42,9 @@ def test_resolve_local_path_without_local_file_root():
4242@pytest .mark .parametrize (
4343 "path, expected_error" ,
4444 [
45- pytest .param ("/etc/passwd" , "must be relative" , id = "absolute" ),
45+ pytest .param (
46+ "/etc/passwd" , "escapes the configured root" , id = "absolute-outside"
47+ ),
4648 pytest .param (
4749 "../outside.txt" , "escapes the configured root" , id = "dotdot"
4850 ),
@@ -68,6 +70,31 @@ def test_resolve_local_path_rejects(tmp_path, path, expected_error):
6870 )
6971
7072
73+ def test_resolve_local_path_accepts_an_absolute_path_inside_the_root (tmp_path ):
74+ """Test _resolve_local_path accepts an absolute path that lands in the root."""
75+ root = tmp_path / "root"
76+ root .mkdir ()
77+
78+ resolved = storage_tool ._resolve_local_path (
79+ str (root / "report.csv" ), GCSToolSettings (local_file_root = str (root ))
80+ )
81+
82+ assert resolved == str (root .resolve () / "report.csv" )
83+
84+
85+ def test_resolve_local_path_keeps_the_root_out_of_the_message (tmp_path ):
86+ """Test the rejection does not disclose the configured root, which the model reads."""
87+ root = tmp_path / "root"
88+ root .mkdir ()
89+
90+ with pytest .raises (ValueError ) as excinfo :
91+ storage_tool ._resolve_local_path (
92+ "/etc/passwd" , GCSToolSettings (local_file_root = str (root ))
93+ )
94+
95+ assert str (root .resolve ()) not in str (excinfo .value )
96+
97+
7198def test_list_objects ():
7299 """Test list_objects function."""
73100 with mock .patch .object (
@@ -259,8 +286,10 @@ def test_create_object_from_file_without_local_file_root():
259286 mock_blob .upload_from_filename .assert_not_called ()
260287
261288
262- def test_create_object_from_file_rejects_absolute_path (tmp_path ):
263- """Test create_object refuses an absolute source_file_path."""
289+ def test_create_object_from_file_rejects_absolute_path_outside_the_root (
290+ tmp_path ,
291+ ):
292+ """Test create_object refuses a source_file_path outside the configured root."""
264293 with mock .patch .object (
265294 client , "get_gcs_client" , autospec = True
266295 ) as mock_get_client :
@@ -271,19 +300,53 @@ def test_create_object_from_file_rejects_absolute_path(tmp_path):
271300 mock_blob = mock .MagicMock ()
272301 mock_bucket .blob .return_value = mock_blob
273302
303+ root = tmp_path / "root"
304+ root .mkdir ()
305+
274306 creds = mock .create_autospec (Credentials , instance = True )
275307 result = storage_tool .create_object (
276308 bucket_name = "test-bucket" ,
277309 object_name = "test-object" ,
278310 source_file_path = str (tmp_path / "secrets.json" ),
279311 credentials = creds ,
280- settings = GCSToolSettings (local_file_root = str (tmp_path )),
312+ settings = GCSToolSettings (local_file_root = str (root )),
281313 )
282314 assert result ["status" ] == "ERROR"
283- assert "must be relative " in result ["error_details" ]
315+ assert "escapes the configured root " in result ["error_details" ]
284316 mock_blob .upload_from_filename .assert_not_called ()
285317
286318
319+ def test_create_object_from_file_accepts_absolute_path_inside_the_root (
320+ tmp_path ,
321+ ):
322+ """Test create_object uploads from an absolute path under the configured root."""
323+ with mock .patch .object (
324+ client , "get_gcs_client" , autospec = True
325+ ) as mock_get_client :
326+ mock_client = mock .MagicMock ()
327+ mock_get_client .return_value = mock_client
328+ mock_bucket = mock .MagicMock ()
329+ mock_client .get_bucket .return_value = mock_bucket
330+ mock_blob = mock .MagicMock ()
331+ mock_bucket .blob .return_value = mock_blob
332+
333+ source = tmp_path / "report.csv"
334+ source .write_text ("a,b\n 1,2\n " )
335+
336+ creds = mock .create_autospec (Credentials , instance = True )
337+ result = storage_tool .create_object (
338+ bucket_name = "test-bucket" ,
339+ object_name = "test-object" ,
340+ source_file_path = str (source ),
341+ credentials = creds ,
342+ settings = GCSToolSettings (local_file_root = str (tmp_path )),
343+ )
344+ assert result ["status" ] == "SUCCESS"
345+ mock_blob .upload_from_filename .assert_called_once_with (
346+ str (source .resolve ())
347+ )
348+
349+
287350def test_create_object_no_data ():
288351 """Test create_object function when neither data nor source_file_path is provided."""
289352 with mock .patch .object (
0 commit comments