Skip to content

Commit 89061d9

Browse files
committed
Reapply "add test_ReadFileTool.py as unittest"
This reverts commit a6c0b2b.
1 parent a6c0b2b commit 89061d9

1 file changed

Lines changed: 271 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import asyncio
17+
from pathlib import Path
18+
import tempfile
19+
20+
from google.adk.environment._local_environment import LocalEnvironment
21+
from google.adk.tools.environment._tools import ReadFileTool
22+
import pytest
23+
24+
25+
@pytest.fixture
26+
async def env_with_file():
27+
"""Creates a temporary environment with a sample file."""
28+
with tempfile.TemporaryDirectory() as td:
29+
env = LocalEnvironment(working_dir=Path(td))
30+
await env.initialize()
31+
32+
# Create a legitimate file
33+
target = Path(td) / "sample.txt"
34+
target.write_text("line1\nline2\nline3\nline4\nline5\n", encoding="utf-8")
35+
36+
yield env, Path(td)
37+
38+
await env.close()
39+
40+
41+
@pytest.mark.asyncio
42+
async def test_read_file_tool_prevents_shell_injection():
43+
"""Original test — single quote injection via start_line path."""
44+
with tempfile.TemporaryDirectory() as td:
45+
env = LocalEnvironment(working_dir=Path(td))
46+
await env.initialize()
47+
48+
target = Path(td) / "sample.txt"
49+
target.write_text("line1\nline2\nline3\n", encoding="utf-8")
50+
51+
marker = Path(td) / "marker.txt"
52+
injected_path = f"sample.txt'; touch {marker}; echo '"
53+
54+
tool = ReadFileTool(env)
55+
result = await tool.run_async(
56+
args={"path": injected_path, "start_line": 2},
57+
tool_context=None,
58+
)
59+
60+
print(result)
61+
62+
assert not marker.exists(), (
63+
"Shell injection succeeded! marker.txt was created, "
64+
"meaning the path was interpreted as shell syntax."
65+
)
66+
67+
await env.close()
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_shell_injection_via_semicolon():
72+
"""Tests that semicolon injection is blocked."""
73+
with tempfile.TemporaryDirectory() as td:
74+
env = LocalEnvironment(working_dir=Path(td))
75+
await env.initialize()
76+
77+
target = Path(td) / "sample.txt"
78+
target.write_text("line1\nline2\nline3\n", encoding="utf-8")
79+
80+
marker = Path(td) / "marker_semicolon.txt"
81+
# Semicolon injection — tries to run second command
82+
injected_path = f"sample.txt; touch {marker}"
83+
84+
tool = ReadFileTool(env)
85+
result = await tool.run_async(
86+
args={"path": injected_path, "start_line": 2},
87+
tool_context=None,
88+
)
89+
90+
assert (
91+
not marker.exists()
92+
), "Semicolon injection succeeded — marker.txt was created."
93+
94+
await env.close()
95+
96+
97+
@pytest.mark.asyncio
98+
async def test_shell_injection_via_ampersand():
99+
"""Tests that ampersand injection is blocked."""
100+
with tempfile.TemporaryDirectory() as td:
101+
env = LocalEnvironment(working_dir=Path(td))
102+
await env.initialize()
103+
104+
target = Path(td) / "sample.txt"
105+
target.write_text("line1\nline2\nline3\n", encoding="utf-8")
106+
107+
marker = Path(td) / "marker_ampersand.txt"
108+
# Ampersand injection — tries to run command in background
109+
injected_path = f"sample.txt && touch {marker}"
110+
111+
tool = ReadFileTool(env)
112+
result = await tool.run_async(
113+
args={"path": injected_path, "start_line": 2},
114+
tool_context=None,
115+
)
116+
117+
assert (
118+
not marker.exists()
119+
), "Ampersand injection succeeded — marker.txt was created."
120+
121+
await env.close()
122+
123+
124+
@pytest.mark.asyncio
125+
async def test_shell_injection_via_backtick():
126+
"""Tests that backtick command substitution is blocked."""
127+
with tempfile.TemporaryDirectory() as td:
128+
env = LocalEnvironment(working_dir=Path(td))
129+
await env.initialize()
130+
131+
target = Path(td) / "sample.txt"
132+
target.write_text("line1\nline2\nline3\n", encoding="utf-8")
133+
134+
marker = Path(td) / "marker_backtick.txt"
135+
# Backtick injection — tries command substitution
136+
injected_path = f"sample.txt`touch {marker}`"
137+
138+
tool = ReadFileTool(env)
139+
result = await tool.run_async(
140+
args={"path": injected_path, "start_line": 2},
141+
tool_context=None,
142+
)
143+
144+
assert (
145+
not marker.exists()
146+
), "Backtick injection succeeded — marker.txt was created."
147+
148+
await env.close()
149+
150+
151+
@pytest.mark.asyncio
152+
async def test_shell_injection_with_end_line():
153+
"""Tests injection is blocked when end_line triggers the shell path."""
154+
with tempfile.TemporaryDirectory() as td:
155+
env = LocalEnvironment(working_dir=Path(td))
156+
await env.initialize()
157+
158+
target = Path(td) / "sample.txt"
159+
target.write_text("line1\nline2\nline3\n", encoding="utf-8")
160+
161+
marker = Path(td) / "marker_end_line.txt"
162+
injected_path = f"sample.txt'; touch {marker}; echo '"
163+
164+
tool = ReadFileTool(env)
165+
# end_line also triggers the shell path
166+
result = await tool.run_async(
167+
args={"path": injected_path, "end_line": 2},
168+
tool_context=None,
169+
)
170+
171+
assert (
172+
not marker.exists()
173+
), "Shell injection via end_line succeeded — marker.txt was created."
174+
175+
await env.close()
176+
177+
178+
@pytest.mark.asyncio
179+
async def test_read_file_full_content():
180+
"""Tests reading a full file without line range returns all lines."""
181+
with tempfile.TemporaryDirectory() as td:
182+
env = LocalEnvironment(working_dir=Path(td))
183+
await env.initialize()
184+
185+
target = Path(td) / "sample.txt"
186+
target.write_text("line1\nline2\nline3\n", encoding="utf-8")
187+
188+
tool = ReadFileTool(env)
189+
result = await tool.run_async(
190+
args={"path": str(target)},
191+
tool_context=None,
192+
)
193+
194+
assert result["status"] == "ok"
195+
assert "line1" in result["content"]
196+
assert "line2" in result["content"]
197+
assert "line3" in result["content"]
198+
199+
await env.close()
200+
201+
202+
@pytest.mark.asyncio
203+
async def test_read_file_with_valid_start_line():
204+
"""Tests that reading from a valid start_line works correctly."""
205+
with tempfile.TemporaryDirectory() as td:
206+
env = LocalEnvironment(working_dir=Path(td))
207+
await env.initialize()
208+
209+
target = Path(td) / "sample.txt"
210+
target.write_text("line1\nline2\nline3\nline4\nline5\n", encoding="utf-8")
211+
212+
tool = ReadFileTool(env)
213+
result = await tool.run_async(
214+
args={"path": str(target), "start_line": 3},
215+
tool_context=None,
216+
)
217+
218+
assert result["status"] == "ok"
219+
assert "line3" in result["content"]
220+
assert "line4" in result["content"]
221+
assert "line5" in result["content"]
222+
# line1 and line2 should not be in the result
223+
assert "line1" not in result["content"]
224+
assert "line2" not in result["content"]
225+
226+
await env.close()
227+
228+
229+
@pytest.mark.asyncio
230+
async def test_read_file_with_valid_start_and_end_line():
231+
"""Tests that reading a specific line range works correctly."""
232+
with tempfile.TemporaryDirectory() as td:
233+
env = LocalEnvironment(working_dir=Path(td))
234+
await env.initialize()
235+
236+
target = Path(td) / "sample.txt"
237+
target.write_text("line1\nline2\nline3\nline4\nline5\n", encoding="utf-8")
238+
239+
tool = ReadFileTool(env)
240+
result = await tool.run_async(
241+
args={"path": str(target), "start_line": 2, "end_line": 4},
242+
tool_context=None,
243+
)
244+
245+
assert result["status"] == "ok"
246+
assert "line2" in result["content"]
247+
assert "line3" in result["content"]
248+
assert "line4" in result["content"]
249+
assert "line1" not in result["content"]
250+
assert "line5" not in result["content"]
251+
252+
await env.close()
253+
254+
255+
@pytest.mark.asyncio
256+
async def test_read_file_missing_path_returns_error():
257+
"""Tests that missing path returns an error."""
258+
with tempfile.TemporaryDirectory() as td:
259+
env = LocalEnvironment(working_dir=Path(td))
260+
await env.initialize()
261+
262+
tool = ReadFileTool(env)
263+
result = await tool.run_async(
264+
args={},
265+
tool_context=None,
266+
)
267+
268+
assert result["status"] == "error"
269+
assert "path" in result["error"].lower()
270+
271+
await env.close()

0 commit comments

Comments
 (0)