Skip to content

Commit 3f08634

Browse files
committed
test: add unit tests for github
1 parent 9d93d0f commit 3f08634

5 files changed

Lines changed: 336 additions & 0 deletions

File tree

integrations/github/tests/test_file_editor.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,94 @@ def test_get_request_headers_with_empty_token(self, monkeypatch):
280280
assert "Authorization" not in headers
281281
assert headers["Accept"] == "application/vnd.github.v3+json"
282282
assert headers["User-Agent"] == "Haystack/GitHubFileEditor"
283+
284+
@pytest.mark.parametrize(
285+
"file_b64,original,expected",
286+
[
287+
("SGVsbG8gV29ybGQ=", "missing", "Error: Original string not found in file"),
288+
("SGVsbG8gSGVsbG8=", "Hello", "Error: Original string appears multiple times. Please provide more context"),
289+
],
290+
)
291+
@patch("requests.get")
292+
def test_run_edit_string_edge_cases(self, mock_get, file_b64, original, expected, monkeypatch):
293+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
294+
mock_get.return_value.json.return_value = {"content": file_b64, "sha": "abc123"}
295+
mock_get.return_value.raise_for_status.return_value = None
296+
297+
editor = GitHubFileEditor()
298+
result = editor.run(
299+
command=Command.EDIT,
300+
payload={"path": "f.txt", "original": original, "replacement": "X", "message": "m"},
301+
repo="owner/repo",
302+
branch="main",
303+
)
304+
assert result["result"] == expected
305+
306+
@patch("requests.get")
307+
def test_run_undo_not_last_user(self, mock_get, monkeypatch):
308+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
309+
310+
mock_get.return_value.raise_for_status.return_value = None
311+
mock_get.return_value.json.side_effect = [
312+
[{"author": {"login": "different_user"}, "sha": "sha1"}],
313+
{"login": "another_user"},
314+
]
315+
316+
editor = GitHubFileEditor()
317+
result = editor.run(command=Command.UNDO, payload={"message": "m"}, repo="owner/repo", branch="main")
318+
assert result["result"] == "Error: Last commit was not made by the current user"
319+
320+
@pytest.mark.parametrize(
321+
"kwargs,expected_substring",
322+
[
323+
({"command": Command.EDIT, "payload": {}}, "Error: No repository specified"),
324+
(
325+
{"command": "bogus", "payload": {}, "repo": "owner/repo"},
326+
None,
327+
),
328+
],
329+
)
330+
def test_run_validation_errors(self, kwargs, expected_substring, monkeypatch):
331+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
332+
editor = GitHubFileEditor()
333+
if expected_substring is None:
334+
with pytest.raises(ValueError):
335+
editor.run(**kwargs)
336+
else:
337+
result = editor.run(**kwargs)
338+
assert expected_substring in result["result"]
339+
340+
@patch("requests.get")
341+
def test_run_command_as_string(self, mock_get, monkeypatch):
342+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
343+
mock_get.return_value.json.return_value = {"content": "SGVsbG8=", "sha": "abc"}
344+
mock_get.return_value.raise_for_status.return_value = None
345+
346+
editor = GitHubFileEditor(repo="owner/repo")
347+
with patch.object(editor, "_update_file", return_value=True):
348+
result = editor.run(
349+
command="EDIT",
350+
payload={"path": "f.txt", "original": "Hello", "replacement": "Hi", "message": "m"},
351+
)
352+
assert result["result"] == "Edit successful"
353+
354+
@pytest.mark.parametrize(
355+
"command,payload",
356+
[
357+
(Command.UNDO, {"message": "m"}),
358+
(Command.CREATE, {"path": "n.txt", "content": "x", "message": "m"}),
359+
(Command.DELETE, {"path": "n.txt", "message": "m"}),
360+
],
361+
)
362+
@patch("requests.get")
363+
def test_run_command_error_handling_no_raise(self, mock_get, command, payload, monkeypatch):
364+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
365+
mock_get.side_effect = requests.RequestException("API Error")
366+
367+
editor = GitHubFileEditor(raise_on_failure=False)
368+
with (
369+
patch("requests.put", side_effect=requests.RequestException("API Error")),
370+
patch("requests.delete", side_effect=requests.RequestException("API Error")),
371+
):
372+
result = editor.run(command=command, payload=payload, repo="owner/repo", branch="main")
373+
assert "Error: API Error" in result["result"]

integrations/github/tests/test_pr_creator.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,82 @@ def test_get_request_headers_with_empty_token(self, monkeypatch):
143143
assert "Authorization" not in headers
144144
assert headers["Accept"] == "application/vnd.github.v3+json"
145145
assert headers["User-Agent"] == "Haystack/GitHubPRCreator"
146+
147+
def test_parse_issue_url_invalid(self, monkeypatch):
148+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
149+
pr_creator = GitHubPRCreator()
150+
with pytest.raises(ValueError, match="Invalid GitHub issue URL format"):
151+
pr_creator._parse_issue_url("https://github.com/owner/repo/pull/1")
152+
153+
@patch("requests.get")
154+
def test_run_returns_error_when_fork_not_found(self, mock_get, monkeypatch):
155+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
156+
mock_get.return_value.json.return_value = {"login": "test_user"}
157+
mock_get.return_value.raise_for_status.return_value = None
158+
159+
pr_creator = GitHubPRCreator()
160+
with patch.object(pr_creator, "_check_fork_exists", return_value=False):
161+
result = pr_creator.run(
162+
issue_url="https://github.com/owner/repo/issues/456",
163+
title="Test PR",
164+
branch="feature-branch",
165+
base="main",
166+
)
167+
assert result == {"result": "Error: Fork not found at test_user/repo"}
168+
169+
@pytest.mark.parametrize(
170+
"fork_payload,raises_request,expected",
171+
[
172+
({"fork": True}, False, True),
173+
({"fork": False}, False, False),
174+
({}, False, False),
175+
(None, True, False),
176+
],
177+
)
178+
@patch("requests.get")
179+
def test_check_fork_exists(self, mock_get, fork_payload, raises_request, expected, monkeypatch):
180+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
181+
if raises_request:
182+
mock_get.side_effect = requests.RequestException("boom")
183+
else:
184+
mock_get.return_value.json.return_value = fork_payload
185+
mock_get.return_value.raise_for_status.return_value = None
186+
187+
pr_creator = GitHubPRCreator()
188+
assert pr_creator._check_fork_exists("repo", "test_user") is expected
189+
190+
@pytest.mark.parametrize("raise_on_failure", [True, False])
191+
@patch("requests.post")
192+
def test_create_fork(self, mock_post, raise_on_failure, monkeypatch):
193+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
194+
mock_post.return_value.json.return_value = {"owner": {"login": "test_user"}}
195+
mock_post.return_value.raise_for_status.return_value = None
196+
197+
pr_creator = GitHubPRCreator(raise_on_failure=raise_on_failure)
198+
assert pr_creator._create_fork("owner", "repo") == "test_user"
199+
200+
mock_post.side_effect = requests.RequestException("boom")
201+
if raise_on_failure:
202+
with pytest.raises(RuntimeError, match="Failed to create fork"):
203+
pr_creator._create_fork("owner", "repo")
204+
else:
205+
assert pr_creator._create_fork("owner", "repo") is None
206+
207+
@pytest.mark.parametrize("raise_on_failure", [True, False])
208+
@patch("requests.post")
209+
@patch("requests.get")
210+
def test_create_branch(self, mock_get, mock_post, raise_on_failure, monkeypatch):
211+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
212+
mock_get.return_value.json.return_value = {"object": {"sha": "abc123"}}
213+
mock_get.return_value.raise_for_status.return_value = None
214+
mock_post.return_value.raise_for_status.return_value = None
215+
216+
pr_creator = GitHubPRCreator(raise_on_failure=raise_on_failure)
217+
assert pr_creator._create_branch("owner", "repo", "feature", "main") is True
218+
219+
mock_get.side_effect = requests.RequestException("boom")
220+
if raise_on_failure:
221+
with pytest.raises(RuntimeError, match="Failed to create branch"):
222+
pr_creator._create_branch("owner", "repo", "feature", "main")
223+
else:
224+
assert pr_creator._create_branch("owner", "repo", "feature", "main") is False

integrations/github/tests/test_repo_forker.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,57 @@ def test_get_request_headers_with_empty_token(self, monkeypatch):
272272
assert "Authorization" not in headers
273273
assert headers["Accept"] == "application/vnd.github.v3+json"
274274
assert headers["User-Agent"] == "Haystack/GitHubRepoForker"
275+
276+
@pytest.mark.parametrize(
277+
"status_code,raises,expected",
278+
[
279+
(200, False, True),
280+
(404, False, False),
281+
(None, True, False),
282+
],
283+
)
284+
@patch("requests.get")
285+
def test_check_fork_status(self, mock_get, status_code, raises, expected, monkeypatch):
286+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
287+
if raises:
288+
mock_get.side_effect = requests.RequestException("boom")
289+
else:
290+
mock_get.return_value.status_code = status_code
291+
292+
forker = GitHubRepoForker()
293+
assert forker._check_fork_status("test_user/repo") is expected
294+
295+
def test_get_existing_repository_returns_none_on_exception(self, monkeypatch):
296+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
297+
298+
forker = GitHubRepoForker()
299+
with (
300+
patch.object(forker, "_get_authenticated_user", return_value="test_user"),
301+
patch("requests.get", side_effect=requests.RequestException("boom")),
302+
):
303+
assert forker._get_existing_repository("repo") is None
304+
305+
@pytest.mark.parametrize("succeeds_within_timeout", [True, False])
306+
def test_run_wait_for_completion(self, succeeds_within_timeout, monkeypatch):
307+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
308+
309+
forker = GitHubRepoForker(
310+
wait_for_completion=True,
311+
max_wait_seconds=5 if succeeds_within_timeout else 0,
312+
poll_interval=0,
313+
create_branch=False,
314+
)
315+
with (
316+
patch.object(forker, "_parse_github_url", return_value=("owner", "repo", "123")),
317+
patch.object(forker, "_get_authenticated_user", return_value="test_user"),
318+
patch.object(forker, "_get_existing_repository", return_value=None),
319+
patch.object(forker, "_create_fork", return_value="test_user/repo"),
320+
patch.object(forker, "_check_fork_status", side_effect=[False, True]),
321+
patch("time.sleep", lambda _: None),
322+
):
323+
if succeeds_within_timeout:
324+
result = forker.run(url="https://github.com/owner/repo/issues/123")
325+
assert result == {"repo": "test_user/repo", "issue_branch": None}
326+
else:
327+
with pytest.raises(TimeoutError):
328+
forker.run(url="https://github.com/owner/repo/issues/123")

integrations/github/tests/test_repo_viewer.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,52 @@ def test_get_request_headers_with_empty_token(self, monkeypatch):
186186
assert "Authorization" not in headers
187187
assert headers["Accept"] == "application/vnd.github.v3+json"
188188
assert headers["User-Agent"] == "Haystack/GitHubRepoViewer"
189+
190+
def test_run_no_repo_raises(self, monkeypatch):
191+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
192+
viewer = GitHubRepoViewer()
193+
with pytest.raises(ValueError, match="Repository not provided"):
194+
viewer.run(path="README.md")
195+
196+
@pytest.mark.parametrize(
197+
"file_payload,error_substring",
198+
[
199+
(
200+
{
201+
"name": "big.bin",
202+
"path": "big.bin",
203+
"size": 2_000_000,
204+
"html_url": "https://x",
205+
"content": "",
206+
"encoding": "base64",
207+
},
208+
"exceeds limit",
209+
),
210+
(
211+
{
212+
"name": "raw.txt",
213+
"path": "raw.txt",
214+
"size": 5,
215+
"html_url": "https://x",
216+
"content": "Hello",
217+
"encoding": "utf-8",
218+
},
219+
None,
220+
),
221+
],
222+
)
223+
@patch("requests.get")
224+
def test_run_file_size_and_encoding(self, mock_get, file_payload, error_substring, monkeypatch):
225+
monkeypatch.setenv("GITHUB_TOKEN", "test-token")
226+
mock_get.return_value.json.return_value = file_payload
227+
mock_get.return_value.raise_for_status.return_value = None
228+
229+
viewer = GitHubRepoViewer(raise_on_failure=False)
230+
result = viewer.run(repo="owner/repo", path=file_payload["path"])
231+
doc = result["documents"][0]
232+
if error_substring is None:
233+
assert doc.content == "Hello"
234+
assert doc.meta["type"] == "file_content"
235+
else:
236+
assert doc.meta["type"] == "error"
237+
assert error_substring in doc.content
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
import pytest
5+
from haystack import Document
6+
7+
from haystack_integrations.tools.github.utils import message_handler, serialize_handlers
8+
9+
10+
class TestMessageHandler:
11+
@pytest.mark.parametrize(
12+
"documents,expected",
13+
[
14+
(
15+
[Document(content="error message", meta={"type": "error"})],
16+
"error message",
17+
),
18+
(
19+
[
20+
Document(content="docs", meta={"type": "dir"}),
21+
Document(content="README.md", meta={"type": "file"}),
22+
],
23+
"docsREADME.md",
24+
),
25+
(
26+
[Document(content="print('hi')", meta={"type": "file_content", "path": "main.py"})],
27+
"File Content for main.py\n\nprint('hi')",
28+
),
29+
],
30+
)
31+
def test_message_handler_renders_documents(self, documents, expected):
32+
assert message_handler(documents) == expected
33+
34+
def test_message_handler_truncates_to_max_length(self):
35+
big_content = "x" * 200
36+
doc = Document(content=big_content, meta={"type": "file_content", "path": "main.py"})
37+
result = message_handler([doc], max_length=50)
38+
assert result.endswith("...(large file can't be fully displayed)")
39+
assert len(result) == 50 + len("...(large file can't be fully displayed)")
40+
41+
42+
class TestSerializeHandlers:
43+
@pytest.mark.parametrize(
44+
"outputs_to_state,outputs_to_string,error_match",
45+
[
46+
(
47+
{"documents": {"handler": "not_callable"}},
48+
None,
49+
"outputs_to_state\\[documents\\] is not a callable",
50+
),
51+
(
52+
None,
53+
{"handler": "not_callable"},
54+
"outputs_to_string is not a callable",
55+
),
56+
],
57+
)
58+
def test_serialize_handlers_raises_when_handler_not_callable(
59+
self, outputs_to_state, outputs_to_string, error_match
60+
):
61+
serialized: dict = {}
62+
with pytest.raises(ValueError, match=error_match):
63+
serialize_handlers(serialized, outputs_to_state, outputs_to_string)

0 commit comments

Comments
 (0)