Skip to content

Commit 1be0247

Browse files
committed
tests: improve github integration test coverage
1 parent 3d95750 commit 1be0247

File tree

5 files changed

+249
-10
lines changed

5 files changed

+249
-10
lines changed

intbot/core/integrations/github.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def fetch_github_item_details(item_id):
8484

8585

8686
class GithubProjectV2Item:
87+
# NOTE: This might be something for pydantic schemas in the future
88+
8789
def __init__(self, content: dict):
8890
self.content = content
8991

@@ -107,30 +109,30 @@ def content_type(self):
107109
return self.content["projects_v2_item"]["content_type"]
108110

109111
def node_id(self):
110-
# NOTE(artcz): This is more relevant, because of how the graphql query
111-
# above is constructed.
112+
# NOTE(artcz): This is relevant, because of how the graphql query above
113+
# is constructed.
112114
# Using node_id, which is an id of a ProjectV2Item we can get both
113115
# DraftIssue and Issue from one query.
114-
# If we use the content_node_id we need to adjust the query as that ID
115-
# points us directly either an Issue or DraftIssue
116+
# If we use the content_node_id we would probably need two separate
117+
# ways of getting that data.
116118
return self.content["projects_v2_item"]["node_id"]
117119

118-
def content_node_id(self):
119-
return self.content["projects_v2_item"]["content_node_id"]
120-
121120
def changes(self) -> dict:
122121
if "changes" in self.content:
123122
fv = self.content["changes"]["field_value"]
124123
field_name = fv["field_name"]
125124
field_type = fv["field_type"]
125+
126126
if field_type == "date":
127127
changed_from = (
128128
fv["from"].split("T")[0] if fv["from"] is not None else "None"
129129
)
130130
changed_to = fv["to"].split("T")[0] if fv["to"] is not None else "None"
131+
131132
elif field_type == "single_select":
132133
changed_from = fv["from"]["name"] if fv["from"] is not None else "None"
133134
changed_to = fv["to"]["name"] if fv["to"] is not None else "None"
135+
134136
else:
135137
changed_from = "None"
136138
changed_to = "None"
@@ -152,8 +154,9 @@ def as_discord_message(self, github_object: GithubDraftIssue | GithubIssue) -> s
152154
changes = self.changes()
153155

154156
if changes:
155-
details = "**{field}** of **{obj}** from **{from}** to **{to}**".format
156-
details = details(**{"obj": github_object.as_discord_message(), **changes})
157+
details = "**{field}** of **{obj}** from **{from}** to **{to}**".format(
158+
**{"obj": github_object.as_discord_message(), **changes}
159+
)
157160

158161
else:
159162
details = github_object.as_discord_message()
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import pytest
2+
import respx
3+
from core.integrations.github import (
4+
GITHUB_API_URL,
5+
GithubProjectV2Item,
6+
fetch_github_item_details,
7+
parse_github_webhook,
8+
)
9+
from httpx import Response
10+
11+
12+
def test_parse_github_webhook_raises_value_error_for_unsupported():
13+
headers = {"X-Github-Event": "random_event"}
14+
content = {}
15+
16+
with pytest.raises(ValueError):
17+
parse_github_webhook(headers, content)
18+
19+
20+
@pytest.fixture
21+
def mocked_github_response():
22+
def _mock_response(item_type, content_data):
23+
return {"data": {"node": {"content": content_data}}}
24+
25+
return _mock_response
26+
27+
28+
@pytest.fixture
29+
def sample_content():
30+
return {
31+
"sender": {"login": "testuser", "html_url": "https://github.com/testuser"},
32+
"projects_v2_item": {
33+
"content_type": "Issue",
34+
"node_id": "test_node_id",
35+
},
36+
}
37+
38+
39+
@respx.mock
40+
def test_fetch_github_item_details(mocked_github_response):
41+
mocked_response = mocked_github_response(
42+
"Issue",
43+
{
44+
"id": "test_issue_id",
45+
"title": "Test Issue",
46+
"url": "https://github.com/test/repo/issues/1",
47+
},
48+
)
49+
respx.post(GITHUB_API_URL).mock(return_value=Response(200, json=mocked_response))
50+
51+
result = fetch_github_item_details("test_node_id")
52+
53+
assert result["id"] == "test_issue_id"
54+
assert result["title"] == "Test Issue"
55+
assert result["url"] == "https://github.com/test/repo/issues/1"
56+
57+
58+
@respx.mock
59+
def test_github_project_created_event(mocked_github_response, sample_content):
60+
sample_content["action"] = "created"
61+
mocked_response = mocked_github_response(
62+
"Issue",
63+
{
64+
"id": "test_issue_id",
65+
"title": "Test Issue",
66+
"url": "https://github.com/test/repo/issues/1",
67+
},
68+
)
69+
respx.post(GITHUB_API_URL).mock(return_value=Response(200, json=mocked_response))
70+
71+
parser = GithubProjectV2Item(sample_content)
72+
message = parser.as_str()
73+
74+
assert message == (
75+
"[@testuser](https://github.com/testuser) created "
76+
"[Test Issue](https://github.com/test/repo/issues/1)"
77+
)
78+
79+
80+
@respx.mock
81+
def test_github_project_edited_event_for_status_change(
82+
mocked_github_response, sample_content
83+
):
84+
sample_content["action"] = "edited"
85+
sample_content["changes"] = {
86+
"field_value": {
87+
"field_name": "Status",
88+
"field_type": "single_select",
89+
"from": {"name": "To Do"},
90+
"to": {"name": "In Progress"},
91+
}
92+
}
93+
mocked_response = mocked_github_response(
94+
"Issue",
95+
{
96+
"id": "test_issue_id",
97+
"title": "Test Issue",
98+
"url": "https://github.com/test/repo/issues/1",
99+
},
100+
)
101+
respx.post(GITHUB_API_URL).mock(return_value=Response(200, json=mocked_response))
102+
103+
parser = GithubProjectV2Item(sample_content)
104+
message = parser.as_str()
105+
106+
assert message == (
107+
"[@testuser](https://github.com/testuser) changed **Status** of "
108+
"**[Test Issue](https://github.com/test/repo/issues/1)** "
109+
"from **To Do** to **In Progress**"
110+
)
111+
112+
113+
@respx.mock
114+
def test_github_project_edited_event_for_date_change(
115+
mocked_github_response, sample_content
116+
):
117+
sample_content["action"] = "edited"
118+
sample_content["changes"] = {
119+
"field_value": {
120+
"field_name": "Deadline",
121+
"field_type": "date",
122+
"from": "2024-01-01T10:20:30",
123+
"to": "2025-01-05T20:30:10",
124+
}
125+
}
126+
mocked_response = mocked_github_response(
127+
"Issue",
128+
{
129+
"id": "test_issue_id",
130+
"title": "Test Issue",
131+
"url": "https://github.com/test/repo/issues/1",
132+
},
133+
)
134+
respx.post(GITHUB_API_URL).mock(return_value=Response(200, json=mocked_response))
135+
136+
parser = GithubProjectV2Item(sample_content)
137+
message = parser.as_str()
138+
139+
assert message == (
140+
"[@testuser](https://github.com/testuser) changed **Deadline** of "
141+
"**[Test Issue](https://github.com/test/repo/issues/1)** "
142+
"from **2024-01-01** to **2025-01-05**"
143+
)
144+
145+
146+
@respx.mock
147+
def test_github_project_draft_issue_event(mocked_github_response, sample_content):
148+
sample_content["action"] = "created"
149+
sample_content["projects_v2_item"]["content_type"] = "DraftIssue"
150+
mocked_response = mocked_github_response(
151+
"DraftIssue",
152+
{
153+
"id": "draft_issue_id",
154+
"title": "Draft Title",
155+
},
156+
)
157+
respx.post(GITHUB_API_URL).mock(return_value=Response(200, json=mocked_response))
158+
159+
parser = GithubProjectV2Item(sample_content)
160+
message = parser.as_str()
161+
162+
assert message == "[@testuser](https://github.com/testuser) created Draft Title"
163+
164+
165+
def test_github_project_unsupported_action(sample_content):
166+
sample_content["action"] = "unsupported_action"
167+
168+
parser = GithubProjectV2Item(sample_content)
169+
170+
with pytest.raises(ValueError, match="Action unsupported unsupported_action"):
171+
parser.action()
172+
173+
174+
@respx.mock
175+
def test_github_project_edited_event_no_changes(mocked_github_response, sample_content):
176+
sample_content["action"] = "edited"
177+
mocked_response = mocked_github_response(
178+
"Issue",
179+
{
180+
"id": "test_issue_id",
181+
"title": "Test Issue",
182+
"url": "https://github.com/test/repo/issues/1",
183+
},
184+
)
185+
respx.post(GITHUB_API_URL).mock(return_value=Response(200, json=mocked_response))
186+
187+
parser = GithubProjectV2Item(sample_content)
188+
message = parser.as_str()
189+
190+
assert message == (
191+
"[@testuser](https://github.com/testuser) changed "
192+
"[Test Issue](https://github.com/test/repo/issues/1)"
193+
)
194+
195+
196+
@respx.mock
197+
def test_fetch_github_item_details_api_error():
198+
respx.post(GITHUB_API_URL).mock(
199+
return_value=Response(500, json={"message": "Internal Server Error"})
200+
)
201+
202+
with pytest.raises(Exception, match="GitHub API error: 500 - .*"):
203+
fetch_github_item_details("test_node_id")

intbot/tests/test_tasks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ def test_process_webhook_fails_if_unsupported_source():
4141
result = process_webhook.enqueue(str(wh.uuid))
4242

4343
assert result.status == ResultStatus.FAILED
44-
assert result._exception_class == ValueError
4544
assert result.traceback.endswith("ValueError: Unsupported source asdf\n")
4645

4746

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,19 @@ dependencies = [
2121
"django-stubs>=5.1.1",
2222
"pdbpp>=0.10.3",
2323
"pytest-cov>=6.0.0",
24+
"pytest-socket>=0.7.0",
25+
"respx>=0.22.0",
2426
]
2527

2628
[tool.pytest.ini_options]
2729
pythonpath = [
2830
"intbot"
2931
]
3032

33+
# Disable attempts of using the internet in tests, but allow connection to the
34+
# database
35+
addopts = "--disable-socket --allow-unix-socket"
36+
3137
[tool.coverage.run]
3238
branch = true
3339
omit = [

uv.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)