11"""Unit tests for the GitLab merge requests collector."""
22
3- from unittest .mock import AsyncMock , patch
3+ from unittest .mock import AsyncMock , MagicMock , patch
44
55import aiohttp
66
@@ -59,14 +59,6 @@ def merge_requests_json(
5959 },
6060 }
6161
62- @staticmethod
63- def merge_request_fields_json (
64- has_approved_field : bool = False ,
65- ) -> dict [str , dict [str , dict [str , list [dict [str , str ]]]]]:
66- """Return the GraphQL merge request fields response JSON."""
67- fields = [{"name" : "approved" }] if has_approved_field else []
68- return {"data" : {"__type" : {"fields" : fields }}}
69-
7062 @staticmethod
7163 def create_entity (
7264 number : int ,
@@ -97,6 +89,15 @@ async def collect_merge_requests(self, execute_mock: AsyncMock):
9789 collector = MetricCollector (session , self .metric )
9890 return await collector .collect ()
9991
92+ @staticmethod
93+ def mock_response (json_value ) -> AsyncMock :
94+ """Create a mock GraphQL response that returns the given JSON from its json() method."""
95+ response = AsyncMock ()
96+ response .json = AsyncMock (return_value = json_value )
97+ # raise_for_status is sync on aiohttp.ClientResponse; override AsyncMock to avoid unawaited coroutines.
98+ response .raise_for_status = MagicMock ()
99+ return response
100+
100101 async def test_merge_requests (self ):
101102 """Test that the number of merge requests can be measured."""
102103 self .set_source_parameter ("merge_request_state" , ["opened" , "closed" , "merged" ])
@@ -110,11 +111,9 @@ async def test_merge_requests(self):
110111 self .merge_request_json (4 , branch = "dev" ), # Excluded because of target branch
111112 ],
112113 )
113- merge_request_fields_response = AsyncMock ()
114- merge_requests_response = AsyncMock ()
115- execute = AsyncMock (side_effect = [merge_request_fields_response , merge_requests_response ])
116- merge_request_fields_response .json = AsyncMock (return_value = self .merge_request_fields_json ())
117- merge_requests_response .json = AsyncMock (return_value = merge_requests_json )
114+ merge_requests_response = self .mock_response (merge_requests_json )
115+ # The first call is the dry-run that detects the approved field, the second is the real first page.
116+ execute = AsyncMock (side_effect = [merge_requests_response , merge_requests_response ])
118117 entities = [self .create_entity (1 )]
119118 response = await self .collect_merge_requests (execute )
120119 self .assert_measurement (response , value = "1" , total = "4" , entities = entities , landing_url = self .LANDING_URL )
@@ -123,13 +122,10 @@ async def test_pagination(self):
123122 """Test that pagination works."""
124123 merge_requests_json1 = self .merge_requests_json ([self .merge_request_json (1 )], count = 2 , has_next_page = True )
125124 merge_requests_json2 = self .merge_requests_json ([self .merge_request_json (2 )], count = 2 )
126- merge_request_fields_response = AsyncMock ()
127- merge_requests_page1 = AsyncMock ()
128- merge_requests_page2 = AsyncMock ()
129- execute = AsyncMock (side_effect = [merge_request_fields_response , merge_requests_page1 , merge_requests_page2 ])
130- merge_request_fields_response .json = AsyncMock (return_value = self .merge_request_fields_json ())
131- merge_requests_page1 .json = AsyncMock (return_value = merge_requests_json1 )
132- merge_requests_page2 .json = AsyncMock (return_value = merge_requests_json2 )
125+ merge_requests_page1 = self .mock_response (merge_requests_json1 )
126+ merge_requests_page2 = self .mock_response (merge_requests_json2 )
127+ # Dry-run, real page 1, page 2.
128+ execute = AsyncMock (side_effect = [merge_requests_page1 , merge_requests_page1 , merge_requests_page2 ])
133129 entities = [self .create_entity (1 ), self .create_entity (2 )]
134130 response = await self .collect_merge_requests (execute )
135131 self .assert_measurement (response , value = "2" , total = "2" , entities = entities , landing_url = self .LANDING_URL )
@@ -140,23 +136,17 @@ async def test_approval(self):
140136 merge_requests_json = self .merge_requests_json (
141137 [self .merge_request_json (1 , approved = True ), self .merge_request_json (2 )],
142138 )
143- merge_request_fields_response = AsyncMock ()
144- merge_requests_response = AsyncMock ()
145- execute = AsyncMock (side_effect = [merge_request_fields_response , merge_requests_response ])
146- merge_request_fields_response .json = AsyncMock (return_value = self .merge_request_fields_json (True ))
147- merge_requests_response .json = AsyncMock (return_value = merge_requests_json )
139+ merge_requests_response = self .mock_response (merge_requests_json )
140+ execute = AsyncMock (side_effect = [merge_requests_response , merge_requests_response ])
148141 entities = [self .create_entity (1 , approved = "yes" )]
149142 response = await self .collect_merge_requests (execute )
150143 self .assert_measurement (response , value = "1" , total = "2" , entities = entities , landing_url = self .LANDING_URL )
151144
152145 async def test_insufficient_permissions (self ):
153146 """Test that the collector returns a helpful error message if no merge request info is returned."""
154147 merge_requests_json = {"data" : {"project" : None }}
155- merge_request_fields_response = AsyncMock ()
156- merge_requests_response = AsyncMock ()
157- execute = AsyncMock (side_effect = [merge_request_fields_response , merge_requests_response ])
158- merge_request_fields_response .json = AsyncMock (return_value = self .merge_request_fields_json (True ))
159- merge_requests_response .json = AsyncMock (return_value = merge_requests_json )
148+ merge_requests_response = self .mock_response (merge_requests_json )
149+ execute = AsyncMock (side_effect = [merge_requests_response , merge_requests_response ])
160150 response = await self .collect_merge_requests (execute )
161151 self .assert_measurement (
162152 response ,
@@ -170,11 +160,8 @@ async def test_ignore_drafts(self):
170160 merge_requests_json = self .merge_requests_json (
171161 [self .merge_request_json (1 , draft = True ), self .merge_request_json (2 )],
172162 )
173- merge_request_fields_response = AsyncMock ()
174- merge_requests_response = AsyncMock ()
175- execute = AsyncMock (side_effect = [merge_request_fields_response , merge_requests_response ])
176- merge_request_fields_response .json = AsyncMock (return_value = self .merge_request_fields_json (True ))
177- merge_requests_response .json = AsyncMock (return_value = merge_requests_json )
163+ merge_requests_response = self .mock_response (merge_requests_json )
164+ execute = AsyncMock (side_effect = [merge_requests_response , merge_requests_response ])
178165 entities = [self .create_entity (2 )]
179166 response = await self .collect_merge_requests (execute )
180167 self .assert_measurement (response , value = "1" , total = "2" , entities = entities , landing_url = self .LANDING_URL )
@@ -185,11 +172,34 @@ async def test_ignore_drafts_without_drafts(self):
185172 merge_requests_json = self .merge_requests_json (
186173 [self .merge_request_json (1 )],
187174 )
188- merge_request_fields_response = AsyncMock ()
189- merge_requests_response = AsyncMock ()
190- execute = AsyncMock (side_effect = [merge_request_fields_response , merge_requests_response ])
191- merge_request_fields_response .json = AsyncMock (return_value = self .merge_request_fields_json (True ))
192- merge_requests_response .json = AsyncMock (return_value = merge_requests_json )
175+ merge_requests_response = self .mock_response (merge_requests_json )
176+ execute = AsyncMock (side_effect = [merge_requests_response , merge_requests_response ])
193177 entities = [self .create_entity (1 )]
194178 response = await self .collect_merge_requests (execute )
195179 self .assert_measurement (response , value = "1" , total = "1" , entities = entities , landing_url = self .LANDING_URL )
180+
181+ async def test_approved_field_unknown_falls_back (self ):
182+ """Test that the collector falls back to a query without the approved field if GitLab rejects it."""
183+ unknown_field_json = {
184+ "errors" : [{"message" : "Field 'approved' doesn't exist on type 'MergeRequest'" }],
185+ }
186+ merge_requests_json = self .merge_requests_json ([self .merge_request_json (1 )])
187+ unknown_field_response = self .mock_response (unknown_field_json )
188+ merge_requests_response = self .mock_response (merge_requests_json )
189+ # Dry-run returns the unknown-field error, then the real first page succeeds without the field.
190+ execute = AsyncMock (side_effect = [unknown_field_response , merge_requests_response ])
191+ entities = [self .create_entity (1 )]
192+ response = await self .collect_merge_requests (execute )
193+ self .assert_measurement (response , value = "1" , total = "1" , entities = entities , landing_url = self .LANDING_URL )
194+
195+ async def test_unrelated_graphql_error_surfaces (self ):
196+ """Test that unrelated GraphQL errors are surfaced as a connection error."""
197+ error_json = {"errors" : [{"message" : "Something else went wrong" }]}
198+ error_response = self .mock_response (error_json )
199+ execute = AsyncMock (side_effect = [error_response , error_response ])
200+ response = await self .collect_merge_requests (execute )
201+ self .assert_measurement (
202+ response ,
203+ landing_url = self .LANDING_URL ,
204+ connection_error = "Something else went wrong" ,
205+ )
0 commit comments