Skip to content

Commit 2a4bfcb

Browse files
authored
Merge pull request #433 from Jalmeida1994/feat(#44)/add-pagination-to-discussions-query
feat(discussions): refactor get_discussions function for pagination support
2 parents dcfd440 + b1273e5 commit 2a4bfcb

File tree

2 files changed

+147
-69
lines changed

2 files changed

+147
-69
lines changed

Diff for: discussions.py

+43-20
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ def get_discussions(token: str, search_query: str, ghe: str):
1616
Args:
1717
token (str): A personal access token for GitHub.
1818
search_query (str): The search query to filter discussions by.
19+
ghe (str): GitHub Enterprise URL if applicable, or None for github.com.
1920
2021
Returns:
2122
list: A list of discussions in the repository that match the search query.
22-
2323
"""
24-
# Construct the GraphQL query
24+
# Construct the GraphQL query with pagination
2525
query = """
26-
query($query: String!) {
27-
search(query: $query, type: DISCUSSION, first: 100) {
26+
query($query: String!, $cursor: String) {
27+
search(query: $query, type: DISCUSSION, first: 100, after: $cursor) {
2828
edges {
2929
node {
3030
... on Discussion {
@@ -41,34 +41,57 @@ def get_discussions(token: str, search_query: str, ghe: str):
4141
}
4242
}
4343
}
44+
pageInfo {
45+
hasNextPage
46+
endCursor
47+
}
4448
}
4549
}
4650
"""
4751

4852
# Remove the type:discussions filter from the search query
4953
search_query = search_query.replace("type:discussions ", "")
50-
# Set the variables for the GraphQL query
51-
variables = {"query": search_query}
5254

5355
# Send the GraphQL request
5456
api_endpoint = f"{ghe}/api" if ghe else "https://api.github.com"
5557
headers = {"Authorization": f"Bearer {token}"}
56-
response = requests.post(
57-
f"{api_endpoint}/graphql",
58-
json={"query": query, "variables": variables},
59-
headers=headers,
60-
timeout=60,
61-
)
6258

63-
# Check for errors in the GraphQL response
64-
if response.status_code != 200 or "errors" in response.json():
65-
raise ValueError("GraphQL query failed")
59+
discussions = []
60+
cursor = None
6661

67-
data = response.json()["data"]
62+
while True:
63+
# Set the variables for the GraphQL query
64+
variables = {"query": search_query, "cursor": cursor}
6865

69-
# Extract the discussions from the GraphQL response
70-
discussions = []
71-
for edge in data["search"]["edges"]:
72-
discussions.append(edge["node"])
66+
# Send the GraphQL request
67+
response = requests.post(
68+
f"{api_endpoint}/graphql",
69+
json={"query": query, "variables": variables},
70+
headers=headers,
71+
timeout=60,
72+
)
73+
74+
# Check for errors in the GraphQL response
75+
if response.status_code != 200:
76+
raise ValueError(
77+
f"GraphQL query failed with status code {response.status_code}"
78+
)
79+
80+
response_json = response.json()
81+
if "errors" in response_json:
82+
raise ValueError(f"GraphQL query failed: {response_json['errors']}")
83+
84+
data = response_json["data"]
85+
86+
# Extract the discussions from the current page
87+
for edge in data["search"]["edges"]:
88+
discussions.append(edge["node"])
89+
90+
# Check if there are more pages
91+
page_info = data["search"]["pageInfo"]
92+
if not page_info["hasNextPage"]:
93+
break
94+
95+
cursor = page_info["endCursor"]
7396

7497
return discussions

Diff for: test_discussions.py

+104-49
Original file line numberDiff line numberDiff line change
@@ -14,72 +14,127 @@
1414
class TestGetDiscussions(unittest.TestCase):
1515
"""A class to test the get_discussions function in the discussions module."""
1616

17-
@patch("requests.post")
18-
def test_get_discussions(self, mock_post):
19-
"""Test the get_discussions function with a successful GraphQL response.
20-
21-
This test mocks a successful GraphQL response and checks that the
22-
function returns the expected discussions.
23-
24-
"""
25-
# Mock the GraphQL response
26-
mock_response = {
17+
def _create_mock_response(
18+
self, discussions, has_next_page=False, end_cursor="cursor123"
19+
):
20+
"""Helper method to create a mock GraphQL response."""
21+
return {
2722
"data": {
2823
"search": {
29-
"edges": [
30-
{
31-
"node": {
32-
"title": "Discussion 1",
33-
"url": "https://github.com/user/repo/discussions/1",
34-
"createdAt": "2021-01-01T00:00:00Z",
35-
"comments": {
36-
"nodes": [{"createdAt": "2021-01-01T00:01:00Z"}]
37-
},
38-
"answerChosenAt": None,
39-
"closedAt": None,
40-
}
41-
},
42-
{
43-
"node": {
44-
"title": "Discussion 2",
45-
"url": "https://github.com/user/repo/discussions/2",
46-
"createdAt": "2021-01-02T00:00:00Z",
47-
"comments": {
48-
"nodes": [{"createdAt": "2021-01-02T00:01:00Z"}]
49-
},
50-
"answerChosenAt": "2021-01-03T00:00:00Z",
51-
"closedAt": "2021-01-04T00:00:00Z",
52-
}
53-
},
54-
]
24+
"edges": [{"node": discussion} for discussion in discussions],
25+
"pageInfo": {"hasNextPage": has_next_page, "endCursor": end_cursor},
5526
}
5627
}
5728
}
29+
30+
@patch("requests.post")
31+
def test_get_discussions_single_page(self, mock_post):
32+
"""Test the get_discussions function with a single page of results."""
33+
# Mock data for two discussions
34+
mock_discussions = [
35+
{
36+
"title": "Discussion 1",
37+
"url": "https://github.com/user/repo/discussions/1",
38+
"createdAt": "2021-01-01T00:00:00Z",
39+
"comments": {"nodes": [{"createdAt": "2021-01-01T00:01:00Z"}]},
40+
"answerChosenAt": None,
41+
"closedAt": None,
42+
},
43+
{
44+
"title": "Discussion 2",
45+
"url": "https://github.com/user/repo/discussions/2",
46+
"createdAt": "2021-01-02T00:00:00Z",
47+
"comments": {"nodes": [{"createdAt": "2021-01-02T00:01:00Z"}]},
48+
"answerChosenAt": "2021-01-03T00:00:00Z",
49+
"closedAt": "2021-01-04T00:00:00Z",
50+
},
51+
]
52+
5853
mock_post.return_value.status_code = 200
59-
mock_post.return_value.json.return_value = mock_response
60-
mock_ghe = ""
54+
mock_post.return_value.json.return_value = self._create_mock_response(
55+
mock_discussions, has_next_page=False
56+
)
6157

62-
# Call the function with mock arguments
6358
discussions = get_discussions(
64-
"token", "repo:user/repo type:discussions query", mock_ghe
59+
"token", "repo:user/repo type:discussions query", ""
6560
)
6661

6762
# Check that the function returns the expected discussions
6863
self.assertEqual(len(discussions), 2)
6964
self.assertEqual(discussions[0]["title"], "Discussion 1")
7065
self.assertEqual(discussions[1]["title"], "Discussion 2")
7166

67+
# Verify only one API call was made
68+
self.assertEqual(mock_post.call_count, 1)
69+
7270
@patch("requests.post")
73-
def test_get_discussions_error(self, mock_post):
74-
"""Test the get_discussions function with a failed GraphQL response.
71+
def test_get_discussions_multiple_pages(self, mock_post):
72+
"""Test the get_discussions function with multiple pages of results."""
73+
# Mock data for pagination
74+
page1_discussions = [
75+
{
76+
"title": "Discussion 1",
77+
"url": "https://github.com/user/repo/discussions/1",
78+
"createdAt": "2021-01-01T00:00:00Z",
79+
"comments": {"nodes": [{"createdAt": "2021-01-01T00:01:00Z"}]},
80+
"answerChosenAt": None,
81+
"closedAt": None,
82+
}
83+
]
84+
85+
page2_discussions = [
86+
{
87+
"title": "Discussion 2",
88+
"url": "https://github.com/user/repo/discussions/2",
89+
"createdAt": "2021-01-02T00:00:00Z",
90+
"comments": {"nodes": [{"createdAt": "2021-01-02T00:01:00Z"}]},
91+
"answerChosenAt": None,
92+
"closedAt": None,
93+
}
94+
]
7595

76-
This test mocks a failed GraphQL response and checks that the function raises a ValueError.
96+
# Configure mock to return different responses for each call
97+
mock_post.return_value.status_code = 200
98+
mock_post.return_value.json.side_effect = [
99+
self._create_mock_response(
100+
page1_discussions, has_next_page=True, end_cursor="cursor123"
101+
),
102+
self._create_mock_response(page2_discussions, has_next_page=False),
103+
]
104+
105+
discussions = get_discussions(
106+
"token", "repo:user/repo type:discussions query", ""
107+
)
77108

78-
"""
79-
# Mock a failed GraphQL response
109+
# Check that all discussions were returned
110+
self.assertEqual(len(discussions), 2)
111+
self.assertEqual(discussions[0]["title"], "Discussion 1")
112+
self.assertEqual(discussions[1]["title"], "Discussion 2")
113+
114+
# Verify that two API calls were made
115+
self.assertEqual(mock_post.call_count, 2)
116+
117+
@patch("requests.post")
118+
def test_get_discussions_error_status_code(self, mock_post):
119+
"""Test the get_discussions function with a failed HTTP response."""
80120
mock_post.return_value.status_code = 500
81-
mock_ghe = ""
82121

83-
# Call the function with mock arguments and check that it raises an error
84-
with self.assertRaises(ValueError):
85-
get_discussions("token", "repo:user/repo type:discussions query", mock_ghe)
122+
with self.assertRaises(ValueError) as context:
123+
get_discussions("token", "repo:user/repo type:discussions query", "")
124+
125+
self.assertIn(
126+
"GraphQL query failed with status code 500", str(context.exception)
127+
)
128+
129+
@patch("requests.post")
130+
def test_get_discussions_graphql_error(self, mock_post):
131+
"""Test the get_discussions function with GraphQL errors in response."""
132+
mock_post.return_value.status_code = 200
133+
mock_post.return_value.json.return_value = {
134+
"errors": [{"message": "GraphQL Error"}]
135+
}
136+
137+
with self.assertRaises(ValueError) as context:
138+
get_discussions("token", "repo:user/repo type:discussions query", "")
139+
140+
self.assertIn("GraphQL query failed:", str(context.exception))

0 commit comments

Comments
 (0)