blob: 7004d976b862ac41e2bfa2dc429ebfa9e874a041 [file] [edit]
#!/usr/bin/env vpython3
# Copyright 2013 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
import contextlib
import io
import math
import os
import unittest
# Mutates sys.path.
import test_env
from parameterized import parameterized
# third_party/
from depot_tools import auto_stub
import net_utils
from utils import authenticators
from utils import net
class RetryLoopMockedTest(auto_stub.TestCase):
"""Base class for test cases that mock retry loop."""
def setUp(self):
super(RetryLoopMockedTest, self).setUp()
self._retry_attemps_cls = net.RetryAttempt
self.mock(net, 'sleep_before_retry', self.mocked_sleep_before_retry)
self.mock(net, 'current_time', self.mocked_current_time)
self.mock(net, 'RetryAttempt', self.mocked_retry_attempt)
self.sleeps = []
self.attempts = []
def mocked_sleep_before_retry(self, attempt, max_wait):
self.sleeps.append((attempt, max_wait))
def mocked_current_time(self):
# One attempt is one virtual second.
return float(len(self.attempts))
def mocked_retry_attempt(self, *args, **kwargs):
attempt = self._retry_attemps_cls(*args, **kwargs)
self.attempts.append(attempt)
return attempt
def assertAttempts(self, attempts, max_timeout):
"""Asserts that retry loop executed given number of |attempts|."""
expected = [(i, max_timeout - i) for i in range(attempts)]
actual = [(x.attempt, x.remaining) for x in self.attempts]
self.assertEqual(expected, actual)
def assertSleeps(self, sleeps):
"""Asserts that retry loop slept given number of times."""
self.assertEqual(sleeps, len(self.sleeps))
class RetryLoopTest(RetryLoopMockedTest):
"""Test for retry_loop implementation."""
def test_sleep_before_retry(self):
# Verifies bounds. Because it's using a pseudo-random number generator and
# not a read random source, it's basically guaranteed to never return the
# same value twice consecutively.
a = net.calculate_sleep_before_retry(0, 0)
b = net.calculate_sleep_before_retry(0, 0)
self.assertTrue(a >= math.pow(1.5, -1), a)
self.assertTrue(b >= math.pow(1.5, -1), b)
self.assertTrue(a < 1.5 + math.pow(1.5, -1), a)
self.assertTrue(b < 1.5 + math.pow(1.5, -1), b)
self.assertNotEqual(a, b)
class HttpErrorTest(unittest.TestCase):
def test_description_simple(self):
e = net_utils.make_fake_error(
code=404,
url='/url',
content=b'Response\nBody',
headers={
'Header-A': 'value-a',
'X-Skipped': 'zzz'
})
self.assertEqual(e.description(False), 'Server returned HTTP code 404')
self.assertEqual(e.description(True), '\n'.join([
'Server returned HTTP code 404',
'----------',
'Content-length: 13',
'Header-a: value-a',
'',
'Response',
'Body',
'----------',
]))
def test_description_json_good(self):
e = net_utils.make_fake_error(
code=404,
url='/url',
content=b'{"error": "inner error message"}',
headers={'Content-Type': 'application/json'})
self.assertEqual(
e.description(False),
'Server returned HTTP code 404 - inner error message')
self.assertEqual(e.description(True), '\n'.join([
'Server returned HTTP code 404 - inner error message',
'----------',
'Content-length: 32',
'Content-type: application/json',
'',
'{"error": "inner error message"}',
'----------',
]))
class HttpServiceTest(RetryLoopMockedTest):
"""Tests for HttpService class."""
@staticmethod
def mocked_http_service(
url='http://example.com',
perform_request=None,
authorize=None,
login=None):
class MockedAuthenticator(authenticators.Authenticator):
supports_login = True
def authorize(self, request):
return authorize(request) if authorize else None
def login(self, allow_user_interaction):
return login(allow_user_interaction) if login else False
class MockedRequestEngine:
@net.maybe_inject_user_agent
def perform_request(self, request):
return perform_request(request) if perform_request else None
return net.HttpService(
url,
authenticator=MockedAuthenticator(),
engine=MockedRequestEngine())
def test_request_GET_success(self):
service_url = 'http://example.com'
request_url = '/some_request'
response = b'True'
def mock_perform_request(request):
self.assertTrue(
request.get_full_url().startswith(service_url + request_url))
return net_utils.make_fake_response(response, request.get_full_url())
service = self.mocked_http_service(url=service_url,
perform_request=mock_perform_request)
self.assertEqual(service.request(request_url).read(), response)
self.assertAttempts(1, net.URL_OPEN_TIMEOUT)
def test_request_POST_success(self):
service_url = 'http://example.com'
request_url = '/some_request'
response = b'True'
def mock_perform_request(request):
self.assertTrue(
request.get_full_url().startswith(service_url + request_url))
self.assertEqual(b'', request.body)
return net_utils.make_fake_response(response, request.get_full_url())
service = self.mocked_http_service(url=service_url,
perform_request=mock_perform_request)
self.assertEqual(service.request(request_url, data={}).read(), response)
self.assertAttempts(1, net.URL_OPEN_TIMEOUT)
def test_request_PUT_success(self):
service_url = 'http://example.com'
request_url = '/some_request'
request_body = b'data_body'
response_body = b'True'
content_type = 'application/octet-stream'
def mock_perform_request(request):
self.assertTrue(
request.get_full_url().startswith(service_url + request_url))
self.assertEqual(request_body, request.body)
self.assertEqual(request.method, 'PUT')
self.assertEqual(request.headers['Content-Type'], content_type)
return net_utils.make_fake_response(response_body, request.get_full_url())
service = self.mocked_http_service(url=service_url,
perform_request=mock_perform_request)
response = service.request(request_url,
data=request_body, content_type=content_type, method='PUT')
self.assertEqual(response.read(), response_body)
self.assertAttempts(1, net.URL_OPEN_TIMEOUT)
def test_request_success_after_failure(self):
response = b'True'
attempts = []
def mock_perform_request(request):
attempts.append(request)
if len(attempts) == 1:
raise net.ConnectionError()
return net_utils.make_fake_response(response, request.get_full_url())
service = self.mocked_http_service(perform_request=mock_perform_request)
self.assertEqual(service.request('/', data={}).read(), response)
self.assertAttempts(2, net.URL_OPEN_TIMEOUT)
def test_request_failure_max_attempts_default(self):
def mock_perform_request(_request):
raise net.ConnectionError()
service = self.mocked_http_service(perform_request=mock_perform_request)
self.assertEqual(service.request('/'), None)
self.assertAttempts(net.URL_OPEN_MAX_ATTEMPTS, net.URL_OPEN_TIMEOUT)
def test_request_failure_max_attempts(self):
def mock_perform_request(_request):
raise net.ConnectionError()
service = self.mocked_http_service(perform_request=mock_perform_request)
self.assertEqual(service.request('/', max_attempts=23), None)
self.assertAttempts(23, net.URL_OPEN_TIMEOUT)
def test_request_failure_timeout(self):
def mock_perform_request(_request):
raise net.ConnectionError()
service = self.mocked_http_service(perform_request=mock_perform_request)
self.assertEqual(service.request('/', max_attempts=10000), None)
self.assertAttempts(int(net.URL_OPEN_TIMEOUT) + 1, net.URL_OPEN_TIMEOUT)
def test_request_failure_timeout_default(self):
def mock_perform_request(_request):
raise net.ConnectionError()
service = self.mocked_http_service(perform_request=mock_perform_request)
self.assertEqual(service.request('/', timeout=10.), None)
self.assertAttempts(11, 10.0)
def test_request_HTTP_error_no_retry(self):
count = []
def mock_perform_request(request):
count.append(request)
raise net_utils.make_fake_error(400, request.get_full_url())
service = self.mocked_http_service(perform_request=mock_perform_request)
self.assertEqual(service.request('/', data={}), None)
self.assertEqual(1, len(count))
self.assertAttempts(1, net.URL_OPEN_TIMEOUT)
def test_request_expected_HTTP_error_code(self):
content = b'data'
count = []
def mock_perform_request(request):
count.append(request)
raise net_utils.make_fake_error(
400, request.get_full_url(), content=content)
service = self.mocked_http_service(perform_request=mock_perform_request)
response = service.request('/', data={}, expected_error_codes=(400,))
self.assertEqual(400, response.code)
self.assertEqual(content, response.read())
self.assertEqual(1, len(count))
self.assertAttempts(1, net.URL_OPEN_TIMEOUT)
def test_request_HTTP_error_retry_404_endpoints(self):
response = b'data'
attempts = []
def mock_perform_request(request):
attempts.append(request)
if len(attempts) == 1:
raise net_utils.make_fake_error(404, request.get_full_url(), headers={
'Content-Type': 'application/text; charset=ASCII',
})
return net_utils.make_fake_response(response, request.get_full_url())
service = self.mocked_http_service(perform_request=mock_perform_request)
result = service.request('/_ah/api/foo/v1/bar', )
self.assertEqual(result.read(), response)
self.assertAttempts(2, net.URL_OPEN_TIMEOUT)
@parameterized.expand([(408, ), (429, ), (500, ), (503, ), (599, )])
def test_request_HTTP_error_with_retry(self, status_code):
response = b'response'
attempts = []
def mock_perform_request(request):
attempts.append(request)
if len(attempts) == 1:
raise net_utils.make_fake_error(status_code, request.get_full_url())
return net_utils.make_fake_response(response, request.get_full_url())
service = self.mocked_http_service(perform_request=mock_perform_request)
self.assertTrue(service.request('/', data={}).read(), response)
self.assertAttempts(2, net.URL_OPEN_TIMEOUT)
def test_auth_success(self):
calls = []
response = b'response'
def mock_perform_request(request):
calls.append('request')
if 'login' not in calls:
raise net_utils.make_fake_error(403, request.get_full_url())
return net_utils.make_fake_response(response, request.get_full_url())
def mock_authorize(request):
self.assertTrue(request)
calls.append('authorize')
def mock_login(allow_user_interaction):
self.assertFalse(allow_user_interaction)
calls.append('login')
return True
service = self.mocked_http_service(
perform_request=mock_perform_request,
authorize=mock_authorize,
login=mock_login)
self.assertEqual(service.request('/').read(), response)
self.assertEqual(
['authorize', 'request', 'login', 'authorize', 'request'], calls)
self.assertAttempts(2, net.URL_OPEN_TIMEOUT)
self.assertSleeps(0)
def test_auth_failure(self):
count = []
def mock_perform_request(request):
raise net_utils.make_fake_error(403, request.get_full_url())
def mock_login(allow_user_interaction):
self.assertFalse(allow_user_interaction)
count.append(1)
return False
service = self.mocked_http_service(perform_request=mock_perform_request,
login=mock_login)
self.assertEqual(service.request('/'), None)
self.assertEqual(len(count), 1)
self.assertAttempts(1, net.URL_OPEN_TIMEOUT)
def test_url_read(self):
# Successfully reads the data.
self.mock(net, 'url_open',
lambda url, **_kwargs: net_utils.make_fake_response(b'111', url))
self.assertEqual(net.url_read('https://fake_url.com/test'), b'111')
# Respects url_open connection errors.
self.mock(net, 'url_open', lambda _url, **_kwargs: None)
self.assertIsNone(net.url_read('https://fake_url.com/test'))
# Respects read timeout errors.
def timeouting_http_response(url):
def read_mock(_size=None):
raise net.TimeoutError()
response = net_utils.make_fake_response(b'', url)
self.mock(response, 'read', read_mock)
return response
self.mock(net, 'url_open',
lambda url, **_kwargs: timeouting_http_response(url))
self.assertIsNone(net.url_read('https://fake_url.com/test'))
def test_url_retrieve(self):
# Successfully reads the data.
@contextlib.contextmanager
def fake_open(_filepath, _mode):
yield io.BytesIO()
self.mock(io, 'open', fake_open)
self.mock(net, 'url_open',
lambda url, **_kwargs: net_utils.make_fake_response(b'111', url))
self.assertEqual(
True, net.url_retrieve('filepath', 'https://localhost/test'))
# Respects url_open connection errors.
self.mock(net, 'url_open', lambda _url, **_kwargs: None)
self.assertEqual(
False, net.url_retrieve('filepath', 'https://localhost/test'))
# Respects read timeout errors.
def timeouting_http_response(url):
def iter_content_mock(_size=None):
raise net.TimeoutError()
response = net_utils.make_fake_response(b'', url)
self.mock(response, 'iter_content', iter_content_mock)
return response
removed = []
self.mock(os, 'remove', removed.append)
self.mock(net, 'url_open',
lambda url, **_kwargs: timeouting_http_response(url))
self.assertEqual(
False, net.url_retrieve('filepath', 'https://localhost/test'))
self.assertEqual(['filepath'], removed)
def test_request_has_user_agent(self):
def mock_perform_request(request):
return request
service = self.mocked_http_service(perform_request=mock_perform_request)
user_agent = 'Py/Foo'
net.set_user_agent(user_agent)
request_headers = service.request('/', data={}).headers
self.assertEqual(request_headers['User-Agent'], user_agent)
net.set_user_agent(None)
request_headers = service.request('/', data={}).headers
self.assertNotIn('User-Agent', request_headers)
class TestNetFunctions(auto_stub.TestCase):
def test_fix_url(self):
data = [
('http://foo.com/', 'http://foo.com'),
('https://foo.com/', 'https://foo.com'),
('https://foo.com', 'https://foo.com'),
('https://foo.com/a', 'https://foo.com/a'),
('https://foo.com/a/', 'https://foo.com/a'),
('https://foo.com:8080/a/', 'https://foo.com:8080/a'),
('foo.com', 'https://foo.com'),
('foo.com/', 'https://foo.com'),
('foo.com/a/', 'https://foo.com/a'),
]
for value, expected in data:
self.assertEqual(expected, net.fix_url(value))
if __name__ == '__main__':
test_env.main()