Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(discussions): refactor get_discussions function for pagination support #433

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))