diff --git a/discussions.py b/discussions.py index 9b16090..b3ab1d0 100644 --- a/discussions.py +++ b/discussions.py @@ -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 { @@ -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 diff --git a/test_discussions.py b/test_discussions.py index 22b247f..8145aad 100644 --- a/test_discussions.py +++ b/test_discussions.py @@ -14,54 +14,49 @@ 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 @@ -69,17 +64,77 @@ def test_get_discussions(self, mock_post): 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))