OLD | NEW |
(Empty) | |
| 1 # |
| 2 # Copyright 2015 Google Inc. |
| 3 # |
| 4 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 # you may not use this file except in compliance with the License. |
| 6 # You may obtain a copy of the License at |
| 7 # |
| 8 # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 # |
| 10 # Unless required by applicable law or agreed to in writing, software |
| 11 # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 # See the License for the specific language governing permissions and |
| 14 # limitations under the License. |
| 15 |
| 16 """Tests for apitools.base.py.batch.""" |
| 17 |
| 18 import textwrap |
| 19 |
| 20 import mock |
| 21 from six.moves import http_client |
| 22 from six.moves.urllib import parse |
| 23 import unittest2 |
| 24 |
| 25 from apitools.base.py import batch |
| 26 from apitools.base.py import exceptions |
| 27 from apitools.base.py import http_wrapper |
| 28 |
| 29 |
| 30 class FakeCredentials(object): |
| 31 |
| 32 def __init__(self): |
| 33 self.num_refreshes = 0 |
| 34 |
| 35 def refresh(self, _): |
| 36 self.num_refreshes += 1 |
| 37 |
| 38 |
| 39 class FakeHttp(object): |
| 40 |
| 41 class FakeRequest(object): |
| 42 |
| 43 def __init__(self, credentials=None): |
| 44 if credentials is not None: |
| 45 self.credentials = credentials |
| 46 |
| 47 def __init__(self, credentials=None): |
| 48 self.request = FakeHttp.FakeRequest(credentials=credentials) |
| 49 |
| 50 |
| 51 class FakeService(object): |
| 52 |
| 53 """A service for testing.""" |
| 54 |
| 55 def GetMethodConfig(self, _): |
| 56 return {} |
| 57 |
| 58 def GetUploadConfig(self, _): |
| 59 return {} |
| 60 |
| 61 # pylint: disable=unused-argument |
| 62 def PrepareHttpRequest( |
| 63 self, method_config, request, global_params, upload_config): |
| 64 return global_params['desired_request'] |
| 65 # pylint: enable=unused-argument |
| 66 |
| 67 def ProcessHttpResponse(self, _, http_response): |
| 68 return http_response |
| 69 |
| 70 |
| 71 class BatchTest(unittest2.TestCase): |
| 72 |
| 73 def assertUrlEqual(self, expected_url, provided_url): |
| 74 |
| 75 def parse_components(url): |
| 76 parsed = parse.urlsplit(url) |
| 77 query = parse.parse_qs(parsed.query) |
| 78 return parsed._replace(query=''), query |
| 79 |
| 80 expected_parse, expected_query = parse_components(expected_url) |
| 81 provided_parse, provided_query = parse_components(provided_url) |
| 82 |
| 83 self.assertEqual(expected_parse, provided_parse) |
| 84 self.assertEqual(expected_query, provided_query) |
| 85 |
| 86 def __ConfigureMock(self, mock_request, expected_request, response): |
| 87 |
| 88 if isinstance(response, list): |
| 89 response = list(response) |
| 90 |
| 91 def CheckRequest(_, request, **unused_kwds): |
| 92 self.assertUrlEqual(expected_request.url, request.url) |
| 93 self.assertEqual(expected_request.http_method, request.http_method) |
| 94 if isinstance(response, list): |
| 95 return response.pop(0) |
| 96 else: |
| 97 return response |
| 98 |
| 99 mock_request.side_effect = CheckRequest |
| 100 |
| 101 def testRequestServiceUnavailable(self): |
| 102 mock_service = FakeService() |
| 103 |
| 104 desired_url = 'https://www.example.com' |
| 105 batch_api_request = batch.BatchApiRequest(batch_url=desired_url, |
| 106 retryable_codes=[]) |
| 107 # The request to be added. The actual request sent will be somewhat |
| 108 # larger, as this is added to a batch. |
| 109 desired_request = http_wrapper.Request(desired_url, 'POST', { |
| 110 'content-type': 'multipart/mixed; boundary="None"', |
| 111 'content-length': 80, |
| 112 }, 'x' * 80) |
| 113 |
| 114 with mock.patch.object(http_wrapper, 'MakeRequest', |
| 115 autospec=True) as mock_request: |
| 116 self.__ConfigureMock( |
| 117 mock_request, |
| 118 http_wrapper.Request(desired_url, 'POST', { |
| 119 'content-type': 'multipart/mixed; boundary="None"', |
| 120 'content-length': 419, |
| 121 }, 'x' * 419), |
| 122 http_wrapper.Response({ |
| 123 'status': '200', |
| 124 'content-type': 'multipart/mixed; boundary="boundary"', |
| 125 }, textwrap.dedent("""\ |
| 126 --boundary |
| 127 content-type: text/plain |
| 128 content-id: <id+0> |
| 129 |
| 130 HTTP/1.1 503 SERVICE UNAVAILABLE |
| 131 nope |
| 132 --boundary--"""), None)) |
| 133 |
| 134 batch_api_request.Add( |
| 135 mock_service, 'unused', None, |
| 136 global_params={'desired_request': desired_request}) |
| 137 |
| 138 api_request_responses = batch_api_request.Execute( |
| 139 FakeHttp(), sleep_between_polls=0) |
| 140 |
| 141 self.assertEqual(1, len(api_request_responses)) |
| 142 |
| 143 # Make sure we didn't retry non-retryable code 503. |
| 144 self.assertEqual(1, mock_request.call_count) |
| 145 |
| 146 self.assertTrue(api_request_responses[0].is_error) |
| 147 self.assertIsNone(api_request_responses[0].response) |
| 148 self.assertIsInstance(api_request_responses[0].exception, |
| 149 exceptions.HttpError) |
| 150 |
| 151 def testSingleRequestInBatch(self): |
| 152 mock_service = FakeService() |
| 153 |
| 154 desired_url = 'https://www.example.com' |
| 155 batch_api_request = batch.BatchApiRequest(batch_url=desired_url) |
| 156 # The request to be added. The actual request sent will be somewhat |
| 157 # larger, as this is added to a batch. |
| 158 desired_request = http_wrapper.Request(desired_url, 'POST', { |
| 159 'content-type': 'multipart/mixed; boundary="None"', |
| 160 'content-length': 80, |
| 161 }, 'x' * 80) |
| 162 |
| 163 with mock.patch.object(http_wrapper, 'MakeRequest', |
| 164 autospec=True) as mock_request: |
| 165 self.__ConfigureMock( |
| 166 mock_request, |
| 167 http_wrapper.Request(desired_url, 'POST', { |
| 168 'content-type': 'multipart/mixed; boundary="None"', |
| 169 'content-length': 419, |
| 170 }, 'x' * 419), |
| 171 http_wrapper.Response({ |
| 172 'status': '200', |
| 173 'content-type': 'multipart/mixed; boundary="boundary"', |
| 174 }, textwrap.dedent("""\ |
| 175 --boundary |
| 176 content-type: text/plain |
| 177 content-id: <id+0> |
| 178 |
| 179 HTTP/1.1 200 OK |
| 180 content |
| 181 --boundary--"""), None)) |
| 182 |
| 183 batch_api_request.Add(mock_service, 'unused', None, { |
| 184 'desired_request': desired_request, |
| 185 }) |
| 186 |
| 187 api_request_responses = batch_api_request.Execute(FakeHttp()) |
| 188 |
| 189 self.assertEqual(1, len(api_request_responses)) |
| 190 self.assertEqual(1, mock_request.call_count) |
| 191 |
| 192 self.assertFalse(api_request_responses[0].is_error) |
| 193 |
| 194 response = api_request_responses[0].response |
| 195 self.assertEqual({'status': '200'}, response.info) |
| 196 self.assertEqual('content', response.content) |
| 197 self.assertEqual(desired_url, response.request_url) |
| 198 |
| 199 def testRefreshOnAuthFailure(self): |
| 200 mock_service = FakeService() |
| 201 |
| 202 desired_url = 'https://www.example.com' |
| 203 batch_api_request = batch.BatchApiRequest(batch_url=desired_url) |
| 204 # The request to be added. The actual request sent will be somewhat |
| 205 # larger, as this is added to a batch. |
| 206 desired_request = http_wrapper.Request(desired_url, 'POST', { |
| 207 'content-type': 'multipart/mixed; boundary="None"', |
| 208 'content-length': 80, |
| 209 }, 'x' * 80) |
| 210 |
| 211 with mock.patch.object(http_wrapper, 'MakeRequest', |
| 212 autospec=True) as mock_request: |
| 213 self.__ConfigureMock( |
| 214 mock_request, |
| 215 http_wrapper.Request(desired_url, 'POST', { |
| 216 'content-type': 'multipart/mixed; boundary="None"', |
| 217 'content-length': 419, |
| 218 }, 'x' * 419), [ |
| 219 http_wrapper.Response({ |
| 220 'status': '200', |
| 221 'content-type': 'multipart/mixed; boundary="boundary"', |
| 222 }, textwrap.dedent("""\ |
| 223 --boundary |
| 224 content-type: text/plain |
| 225 content-id: <id+0> |
| 226 |
| 227 HTTP/1.1 401 UNAUTHORIZED |
| 228 Invalid grant |
| 229 |
| 230 --boundary--"""), None), |
| 231 http_wrapper.Response({ |
| 232 'status': '200', |
| 233 'content-type': 'multipart/mixed; boundary="boundary"', |
| 234 }, textwrap.dedent("""\ |
| 235 --boundary |
| 236 content-type: text/plain |
| 237 content-id: <id+0> |
| 238 |
| 239 HTTP/1.1 200 OK |
| 240 content |
| 241 --boundary--"""), None) |
| 242 ]) |
| 243 |
| 244 batch_api_request.Add(mock_service, 'unused', None, { |
| 245 'desired_request': desired_request, |
| 246 }) |
| 247 |
| 248 credentials = FakeCredentials() |
| 249 api_request_responses = batch_api_request.Execute( |
| 250 FakeHttp(credentials=credentials), sleep_between_polls=0) |
| 251 |
| 252 self.assertEqual(1, len(api_request_responses)) |
| 253 self.assertEqual(2, mock_request.call_count) |
| 254 self.assertEqual(1, credentials.num_refreshes) |
| 255 |
| 256 self.assertFalse(api_request_responses[0].is_error) |
| 257 |
| 258 response = api_request_responses[0].response |
| 259 self.assertEqual({'status': '200'}, response.info) |
| 260 self.assertEqual('content', response.content) |
| 261 self.assertEqual(desired_url, response.request_url) |
| 262 |
| 263 def testNoAttempts(self): |
| 264 desired_url = 'https://www.example.com' |
| 265 batch_api_request = batch.BatchApiRequest(batch_url=desired_url) |
| 266 batch_api_request.Add(FakeService(), 'unused', None, { |
| 267 'desired_request': http_wrapper.Request(desired_url, 'POST', { |
| 268 'content-type': 'multipart/mixed; boundary="None"', |
| 269 'content-length': 80, |
| 270 }, 'x' * 80), |
| 271 }) |
| 272 api_request_responses = batch_api_request.Execute(None, max_retries=0) |
| 273 self.assertEqual(1, len(api_request_responses)) |
| 274 self.assertIsNone(api_request_responses[0].response) |
| 275 self.assertIsNone(api_request_responses[0].exception) |
| 276 |
| 277 def _DoTestConvertIdToHeader(self, test_id, expected_result): |
| 278 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 279 self.assertEqual( |
| 280 expected_result % batch_request._BatchHttpRequest__base_id, |
| 281 batch_request._ConvertIdToHeader(test_id)) |
| 282 |
| 283 def testConvertIdSimple(self): |
| 284 self._DoTestConvertIdToHeader('blah', '<%s+blah>') |
| 285 |
| 286 def testConvertIdThatNeedsEscaping(self): |
| 287 self._DoTestConvertIdToHeader('~tilde1', '<%s+%%7Etilde1>') |
| 288 |
| 289 def _DoTestConvertHeaderToId(self, header, expected_id): |
| 290 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 291 self.assertEqual(expected_id, |
| 292 batch_request._ConvertHeaderToId(header)) |
| 293 |
| 294 def testConvertHeaderToIdSimple(self): |
| 295 self._DoTestConvertHeaderToId('<hello+blah>', 'blah') |
| 296 |
| 297 def testConvertHeaderToIdWithLotsOfPlus(self): |
| 298 self._DoTestConvertHeaderToId('<a+++++plus>', 'plus') |
| 299 |
| 300 def _DoTestConvertInvalidHeaderToId(self, invalid_header): |
| 301 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 302 self.assertRaises(exceptions.BatchError, |
| 303 batch_request._ConvertHeaderToId, invalid_header) |
| 304 |
| 305 def testHeaderWithoutAngleBrackets(self): |
| 306 self._DoTestConvertInvalidHeaderToId('1+1') |
| 307 |
| 308 def testHeaderWithoutPlus(self): |
| 309 self._DoTestConvertInvalidHeaderToId('<HEADER>') |
| 310 |
| 311 def testSerializeRequest(self): |
| 312 request = http_wrapper.Request(body='Hello World', headers={ |
| 313 'content-type': 'protocol/version', |
| 314 }) |
| 315 expected_serialized_request = '\n'.join([ |
| 316 'GET HTTP/1.1', |
| 317 'Content-Type: protocol/version', |
| 318 'MIME-Version: 1.0', |
| 319 'content-length: 11', |
| 320 'Host: ', |
| 321 '', |
| 322 'Hello World', |
| 323 ]) |
| 324 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 325 self.assertEqual(expected_serialized_request, |
| 326 batch_request._SerializeRequest(request)) |
| 327 |
| 328 def testSerializeRequestPreservesHeaders(self): |
| 329 # Now confirm that if an additional, arbitrary header is added |
| 330 # that it is successfully serialized to the request. Merely |
| 331 # check that it is included, because the order of the headers |
| 332 # in the request is arbitrary. |
| 333 request = http_wrapper.Request(body='Hello World', headers={ |
| 334 'content-type': 'protocol/version', |
| 335 'key': 'value', |
| 336 }) |
| 337 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 338 self.assertTrue( |
| 339 'key: value\n' in batch_request._SerializeRequest(request)) |
| 340 |
| 341 def testSerializeRequestNoBody(self): |
| 342 request = http_wrapper.Request(body=None, headers={ |
| 343 'content-type': 'protocol/version', |
| 344 }) |
| 345 expected_serialized_request = '\n'.join([ |
| 346 'GET HTTP/1.1', |
| 347 'Content-Type: protocol/version', |
| 348 'MIME-Version: 1.0', |
| 349 'Host: ', |
| 350 '', |
| 351 '', |
| 352 ]) |
| 353 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 354 self.assertEqual(expected_serialized_request, |
| 355 batch_request._SerializeRequest(request)) |
| 356 |
| 357 def testDeserializeRequest(self): |
| 358 serialized_payload = '\n'.join([ |
| 359 'GET HTTP/1.1', |
| 360 'Content-Type: protocol/version', |
| 361 'MIME-Version: 1.0', |
| 362 'content-length: 11', |
| 363 'key: value', |
| 364 'Host: ', |
| 365 '', |
| 366 'Hello World', |
| 367 ]) |
| 368 example_url = 'https://www.example.com' |
| 369 expected_response = http_wrapper.Response({ |
| 370 'content-length': str(len('Hello World')), |
| 371 'Content-Type': 'protocol/version', |
| 372 'key': 'value', |
| 373 'MIME-Version': '1.0', |
| 374 'status': '', |
| 375 'Host': '' |
| 376 }, 'Hello World', example_url) |
| 377 |
| 378 batch_request = batch.BatchHttpRequest(example_url) |
| 379 self.assertEqual( |
| 380 expected_response, |
| 381 batch_request._DeserializeResponse(serialized_payload)) |
| 382 |
| 383 def testNewId(self): |
| 384 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 385 |
| 386 for i in range(100): |
| 387 self.assertEqual(str(i), batch_request._NewId()) |
| 388 |
| 389 def testAdd(self): |
| 390 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 391 |
| 392 for x in range(100): |
| 393 batch_request.Add(http_wrapper.Request(body=str(x))) |
| 394 |
| 395 for key in batch_request._BatchHttpRequest__request_response_handlers: |
| 396 value = batch_request._BatchHttpRequest__request_response_handlers[ |
| 397 key] |
| 398 self.assertEqual(key, value.request.body) |
| 399 self.assertFalse(value.request.url) |
| 400 self.assertEqual('GET', value.request.http_method) |
| 401 self.assertIsNone(value.response) |
| 402 self.assertIsNone(value.handler) |
| 403 |
| 404 def testInternalExecuteWithFailedRequest(self): |
| 405 with mock.patch.object(http_wrapper, 'MakeRequest', |
| 406 autospec=True) as mock_request: |
| 407 self.__ConfigureMock( |
| 408 mock_request, |
| 409 http_wrapper.Request('https://www.example.com', 'POST', { |
| 410 'content-type': 'multipart/mixed; boundary="None"', |
| 411 'content-length': 80, |
| 412 }, 'x' * 80), |
| 413 http_wrapper.Response({'status': '300'}, None, None)) |
| 414 |
| 415 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 416 |
| 417 self.assertRaises( |
| 418 exceptions.HttpError, batch_request._Execute, None) |
| 419 |
| 420 def testInternalExecuteWithNonMultipartResponse(self): |
| 421 with mock.patch.object(http_wrapper, 'MakeRequest', |
| 422 autospec=True) as mock_request: |
| 423 self.__ConfigureMock( |
| 424 mock_request, |
| 425 http_wrapper.Request('https://www.example.com', 'POST', { |
| 426 'content-type': 'multipart/mixed; boundary="None"', |
| 427 'content-length': 80, |
| 428 }, 'x' * 80), |
| 429 http_wrapper.Response({ |
| 430 'status': '200', |
| 431 'content-type': 'blah/blah' |
| 432 }, '', None)) |
| 433 |
| 434 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 435 |
| 436 self.assertRaises( |
| 437 exceptions.BatchError, batch_request._Execute, None) |
| 438 |
| 439 def testInternalExecute(self): |
| 440 with mock.patch.object(http_wrapper, 'MakeRequest', |
| 441 autospec=True) as mock_request: |
| 442 self.__ConfigureMock( |
| 443 mock_request, |
| 444 http_wrapper.Request('https://www.example.com', 'POST', { |
| 445 'content-type': 'multipart/mixed; boundary="None"', |
| 446 'content-length': 583, |
| 447 }, 'x' * 583), |
| 448 http_wrapper.Response({ |
| 449 'status': '200', |
| 450 'content-type': 'multipart/mixed; boundary="boundary"', |
| 451 }, textwrap.dedent("""\ |
| 452 --boundary |
| 453 content-type: text/plain |
| 454 content-id: <id+2> |
| 455 |
| 456 HTTP/1.1 200 OK |
| 457 Second response |
| 458 |
| 459 --boundary |
| 460 content-type: text/plain |
| 461 content-id: <id+1> |
| 462 |
| 463 HTTP/1.1 401 UNAUTHORIZED |
| 464 First response |
| 465 |
| 466 --boundary--"""), None)) |
| 467 |
| 468 test_requests = { |
| 469 '1': batch.RequestResponseAndHandler( |
| 470 http_wrapper.Request(body='first'), None, None), |
| 471 '2': batch.RequestResponseAndHandler( |
| 472 http_wrapper.Request(body='second'), None, None), |
| 473 } |
| 474 |
| 475 batch_request = batch.BatchHttpRequest('https://www.example.com') |
| 476 batch_request._BatchHttpRequest__request_response_handlers = ( |
| 477 test_requests) |
| 478 |
| 479 batch_request._Execute(FakeHttp()) |
| 480 |
| 481 test_responses = ( |
| 482 batch_request._BatchHttpRequest__request_response_handlers) |
| 483 |
| 484 self.assertEqual(http_client.UNAUTHORIZED, |
| 485 test_responses['1'].response.status_code) |
| 486 self.assertEqual(http_client.OK, |
| 487 test_responses['2'].response.status_code) |
| 488 |
| 489 self.assertIn( |
| 490 'First response', test_responses['1'].response.content) |
| 491 self.assertIn( |
| 492 'Second response', test_responses['2'].response.content) |
| 493 |
| 494 def testPublicExecute(self): |
| 495 |
| 496 def LocalCallback(response, exception): |
| 497 self.assertEqual({'status': '418'}, response.info) |
| 498 self.assertEqual('Teapot', response.content) |
| 499 self.assertIsNone(response.request_url) |
| 500 self.assertIsInstance(exception, exceptions.HttpError) |
| 501 |
| 502 global_callback = mock.Mock() |
| 503 batch_request = batch.BatchHttpRequest( |
| 504 'https://www.example.com', global_callback) |
| 505 |
| 506 with mock.patch.object(batch.BatchHttpRequest, '_Execute', |
| 507 autospec=True) as mock_execute: |
| 508 mock_execute.return_value = None |
| 509 |
| 510 test_requests = { |
| 511 '0': batch.RequestResponseAndHandler( |
| 512 None, |
| 513 http_wrapper.Response({'status': '200'}, 'Hello!', None), |
| 514 None), |
| 515 '1': batch.RequestResponseAndHandler( |
| 516 None, |
| 517 http_wrapper.Response({'status': '418'}, 'Teapot', None), |
| 518 LocalCallback), |
| 519 } |
| 520 |
| 521 batch_request._BatchHttpRequest__request_response_handlers = ( |
| 522 test_requests) |
| 523 batch_request.Execute(None) |
| 524 |
| 525 # Global callback was called once per handler. |
| 526 self.assertEqual(len(test_requests), global_callback.call_count) |
OLD | NEW |