Skip to content

Commit

Permalink
Merge pull request #433 from Jalmeida1994/feat(#44)/add-pagination-to…
Browse files Browse the repository at this point in the history
…-discussions-query

feat(discussions): refactor get_discussions function for pagination support
  • Loading branch information
zkoppert authored Dec 1, 2024
2 parents dcfd440 + b1273e5 commit 2a4bfcb
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 69 deletions.
63 changes: 43 additions & 20 deletions discussions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ def get_discussions(token: str, search_query: str, ghe: str):
Args:
token (str): A personal access token for GitHub.
search_query (str): The search query to filter discussions by.
ghe (str): GitHub Enterprise URL if applicable, or None for github.com.
Returns:
list: A list of discussions in the repository that match the search query.
"""
# Construct the GraphQL query
# Construct the GraphQL query with pagination
query = """
query($query: String!) {
search(query: $query, type: DISCUSSION, first: 100) {
query($query: String!, $cursor: String) {
search(query: $query, type: DISCUSSION, first: 100, after: $cursor) {
edges {
node {
... on Discussion {
Expand All @@ -41,34 +41,57 @@ def get_discussions(token: str, search_query: str, ghe: str):
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
"""

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

# Send the GraphQL request
api_endpoint = f"{ghe}/api" if ghe else "https://api.github.com"
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(
f"{api_endpoint}/graphql",
json={"query": query, "variables": variables},
headers=headers,
timeout=60,
)

# Check for errors in the GraphQL response
if response.status_code != 200 or "errors" in response.json():
raise ValueError("GraphQL query failed")
discussions = []
cursor = None

data = response.json()["data"]
while True:
# Set the variables for the GraphQL query
variables = {"query": search_query, "cursor": cursor}

# Extract the discussions from the GraphQL response
discussions = []
for edge in data["search"]["edges"]:
discussions.append(edge["node"])
# Send the GraphQL request
response = requests.post(
f"{api_endpoint}/graphql",
json={"query": query, "variables": variables},
headers=headers,
timeout=60,
)

# Check for errors in the GraphQL response
if response.status_code != 200:
raise ValueError(
f"GraphQL query failed with status code {response.status_code}"
)

response_json = response.json()
if "errors" in response_json:
raise ValueError(f"GraphQL query failed: {response_json['errors']}")

data = response_json["data"]

# Extract the discussions from the current page
for edge in data["search"]["edges"]:
discussions.append(edge["node"])

# Check if there are more pages
page_info = data["search"]["pageInfo"]
if not page_info["hasNextPage"]:
break

cursor = page_info["endCursor"]

return discussions
153 changes: 104 additions & 49 deletions test_discussions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,72 +14,127 @@
class TestGetDiscussions(unittest.TestCase):
"""A class to test the get_discussions function in the discussions module."""

@patch("requests.post")
def test_get_discussions(self, mock_post):
"""Test the get_discussions function with a successful GraphQL response.
This test mocks a successful GraphQL response and checks that the
function returns the expected discussions.
"""
# Mock the GraphQL response
mock_response = {
def _create_mock_response(
self, discussions, has_next_page=False, end_cursor="cursor123"
):
"""Helper method to create a mock GraphQL response."""
return {
"data": {
"search": {
"edges": [
{
"node": {
"title": "Discussion 1",
"url": "https://github.com/user/repo/discussions/1",
"createdAt": "2021-01-01T00:00:00Z",
"comments": {
"nodes": [{"createdAt": "2021-01-01T00:01:00Z"}]
},
"answerChosenAt": None,
"closedAt": None,
}
},
{
"node": {
"title": "Discussion 2",
"url": "https://github.com/user/repo/discussions/2",
"createdAt": "2021-01-02T00:00:00Z",
"comments": {
"nodes": [{"createdAt": "2021-01-02T00:01:00Z"}]
},
"answerChosenAt": "2021-01-03T00:00:00Z",
"closedAt": "2021-01-04T00:00:00Z",
}
},
]
"edges": [{"node": discussion} for discussion in discussions],
"pageInfo": {"hasNextPage": has_next_page, "endCursor": end_cursor},
}
}
}

@patch("requests.post")
def test_get_discussions_single_page(self, mock_post):
"""Test the get_discussions function with a single page of results."""
# Mock data for two discussions
mock_discussions = [
{
"title": "Discussion 1",
"url": "https://github.com/user/repo/discussions/1",
"createdAt": "2021-01-01T00:00:00Z",
"comments": {"nodes": [{"createdAt": "2021-01-01T00:01:00Z"}]},
"answerChosenAt": None,
"closedAt": None,
},
{
"title": "Discussion 2",
"url": "https://github.com/user/repo/discussions/2",
"createdAt": "2021-01-02T00:00:00Z",
"comments": {"nodes": [{"createdAt": "2021-01-02T00:01:00Z"}]},
"answerChosenAt": "2021-01-03T00:00:00Z",
"closedAt": "2021-01-04T00:00:00Z",
},
]

mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = mock_response
mock_ghe = ""
mock_post.return_value.json.return_value = self._create_mock_response(
mock_discussions, has_next_page=False
)

# Call the function with mock arguments
discussions = get_discussions(
"token", "repo:user/repo type:discussions query", mock_ghe
"token", "repo:user/repo type:discussions query", ""
)

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

# Verify only one API call was made
self.assertEqual(mock_post.call_count, 1)

@patch("requests.post")
def test_get_discussions_error(self, mock_post):
"""Test the get_discussions function with a failed GraphQL response.
def test_get_discussions_multiple_pages(self, mock_post):
"""Test the get_discussions function with multiple pages of results."""
# Mock data for pagination
page1_discussions = [
{
"title": "Discussion 1",
"url": "https://github.com/user/repo/discussions/1",
"createdAt": "2021-01-01T00:00:00Z",
"comments": {"nodes": [{"createdAt": "2021-01-01T00:01:00Z"}]},
"answerChosenAt": None,
"closedAt": None,
}
]

page2_discussions = [
{
"title": "Discussion 2",
"url": "https://github.com/user/repo/discussions/2",
"createdAt": "2021-01-02T00:00:00Z",
"comments": {"nodes": [{"createdAt": "2021-01-02T00:01:00Z"}]},
"answerChosenAt": None,
"closedAt": None,
}
]

This test mocks a failed GraphQL response and checks that the function raises a ValueError.
# Configure mock to return different responses for each call
mock_post.return_value.status_code = 200
mock_post.return_value.json.side_effect = [
self._create_mock_response(
page1_discussions, has_next_page=True, end_cursor="cursor123"
),
self._create_mock_response(page2_discussions, has_next_page=False),
]

discussions = get_discussions(
"token", "repo:user/repo type:discussions query", ""
)

"""
# Mock a failed GraphQL response
# Check that all discussions were returned
self.assertEqual(len(discussions), 2)
self.assertEqual(discussions[0]["title"], "Discussion 1")
self.assertEqual(discussions[1]["title"], "Discussion 2")

# Verify that two API calls were made
self.assertEqual(mock_post.call_count, 2)

@patch("requests.post")
def test_get_discussions_error_status_code(self, mock_post):
"""Test the get_discussions function with a failed HTTP response."""
mock_post.return_value.status_code = 500
mock_ghe = ""

# Call the function with mock arguments and check that it raises an error
with self.assertRaises(ValueError):
get_discussions("token", "repo:user/repo type:discussions query", mock_ghe)
with self.assertRaises(ValueError) as context:
get_discussions("token", "repo:user/repo type:discussions query", "")

self.assertIn(
"GraphQL query failed with status code 500", str(context.exception)
)

@patch("requests.post")
def test_get_discussions_graphql_error(self, mock_post):
"""Test the get_discussions function with GraphQL errors in response."""
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = {
"errors": [{"message": "GraphQL Error"}]
}

with self.assertRaises(ValueError) as context:
get_discussions("token", "repo:user/repo type:discussions query", "")

self.assertIn("GraphQL query failed:", str(context.exception))

0 comments on commit 2a4bfcb

Please sign in to comment.