You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
487 lines
17 KiB
Python
487 lines
17 KiB
Python
#!/usr/bin/env vpython3
|
|
# coding=utf-8
|
|
# Copyright (c) 2019 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
|
|
import base64
|
|
import httplib2
|
|
import json
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
if sys.version_info.major == 2:
|
|
from cStringIO import StringIO
|
|
import mock
|
|
else:
|
|
from io import StringIO
|
|
from unittest import mock
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
import gerrit_util
|
|
import gclient_utils
|
|
import metrics
|
|
import metrics_utils
|
|
import subprocess2
|
|
|
|
|
|
class CookiesAuthenticatorTest(unittest.TestCase):
|
|
_GITCOOKIES = '\n'.join([
|
|
'\t'.join([
|
|
'chromium.googlesource.com',
|
|
'FALSE',
|
|
'/',
|
|
'TRUE',
|
|
'2147483647',
|
|
'o',
|
|
'git-user.chromium.org=1/chromium-secret',
|
|
]),
|
|
'\t'.join([
|
|
'chromium-review.googlesource.com',
|
|
'FALSE',
|
|
'/',
|
|
'TRUE',
|
|
'2147483647',
|
|
'o',
|
|
'git-user.chromium.org=1/chromium-secret',
|
|
]),
|
|
'\t'.join([
|
|
'.example.com',
|
|
'FALSE',
|
|
'/',
|
|
'TRUE',
|
|
'2147483647',
|
|
'o',
|
|
'example-bearer-token',
|
|
]),
|
|
'\t'.join([
|
|
'another-path.example.com',
|
|
'FALSE',
|
|
'/foo',
|
|
'TRUE',
|
|
'2147483647',
|
|
'o',
|
|
'git-example.com=1/another-path-secret',
|
|
]),
|
|
'\t'.join([
|
|
'another-key.example.com',
|
|
'FALSE',
|
|
'/',
|
|
'TRUE',
|
|
'2147483647',
|
|
'not-o',
|
|
'git-example.com=1/another-key-secret',
|
|
]),
|
|
'#' + '\t'.join([
|
|
'chromium-review.googlesource.com',
|
|
'FALSE',
|
|
'/',
|
|
'TRUE',
|
|
'2147483647',
|
|
'o',
|
|
'git-invalid-user.chromium.org=1/invalid-chromium-secret',
|
|
]),
|
|
'Some unrelated line\t that should not be here',
|
|
])
|
|
|
|
def setUp(self):
|
|
mock.patch('gclient_utils.FileRead', return_value=self._GITCOOKIES).start()
|
|
mock.patch('os.getenv', return_value={}).start()
|
|
mock.patch('os.environ', {'HOME': '$HOME'}).start()
|
|
mock.patch('os.path.exists', return_value=True).start()
|
|
mock.patch(
|
|
'subprocess2.check_output',
|
|
side_effect=[
|
|
subprocess2.CalledProcessError(1, ['cmd'], 'cwd', 'out', 'err')],
|
|
).start()
|
|
self.addCleanup(mock.patch.stopall)
|
|
self.maxDiff = None
|
|
|
|
def testGetNewPasswordUrl(self):
|
|
auth = gerrit_util.CookiesAuthenticator()
|
|
self.assertEqual(
|
|
'https://chromium-review.googlesource.com/new-password',
|
|
auth.get_new_password_url('chromium.googlesource.com'))
|
|
self.assertEqual(
|
|
'https://chrome-internal-review.googlesource.com/new-password',
|
|
auth.get_new_password_url('chrome-internal-review.googlesource.com'))
|
|
|
|
def testGetNewPasswordMessage(self):
|
|
auth = gerrit_util.CookiesAuthenticator()
|
|
self.assertIn(
|
|
'https://chromium-review.googlesource.com/new-password',
|
|
auth.get_new_password_message('chromium-review.googlesource.com'))
|
|
self.assertIn(
|
|
'https://chrome-internal-review.googlesource.com/new-password',
|
|
auth.get_new_password_message('chrome-internal.googlesource.com'))
|
|
|
|
def testGetGitcookiesPath(self):
|
|
self.assertEqual(
|
|
os.path.expanduser(os.path.join('~', '.gitcookies')),
|
|
gerrit_util.CookiesAuthenticator().get_gitcookies_path())
|
|
|
|
subprocess2.check_output.side_effect = [b'http.cookiefile']
|
|
self.assertEqual(
|
|
'http.cookiefile',
|
|
gerrit_util.CookiesAuthenticator().get_gitcookies_path())
|
|
subprocess2.check_output.assert_called_with(
|
|
['git', 'config', '--path', 'http.cookiefile'])
|
|
|
|
os.getenv.return_value = 'git-cookies-path'
|
|
self.assertEqual(
|
|
'git-cookies-path',
|
|
gerrit_util.CookiesAuthenticator().get_gitcookies_path())
|
|
os.getenv.assert_called_with('GIT_COOKIES_PATH')
|
|
|
|
def testGitcookies(self):
|
|
auth = gerrit_util.CookiesAuthenticator()
|
|
self.assertEqual(auth.gitcookies, {
|
|
'chromium.googlesource.com':
|
|
('git-user.chromium.org', '1/chromium-secret'),
|
|
'chromium-review.googlesource.com':
|
|
('git-user.chromium.org', '1/chromium-secret'),
|
|
'.example.com':
|
|
('', 'example-bearer-token'),
|
|
})
|
|
|
|
def testGetAuthHeader(self):
|
|
expected_chromium_header = (
|
|
'Basic Z2l0LXVzZXIuY2hyb21pdW0ub3JnOjEvY2hyb21pdW0tc2VjcmV0')
|
|
|
|
auth = gerrit_util.CookiesAuthenticator()
|
|
self.assertEqual(
|
|
expected_chromium_header,
|
|
auth.get_auth_header('chromium.googlesource.com'))
|
|
self.assertEqual(
|
|
expected_chromium_header,
|
|
auth.get_auth_header('chromium-review.googlesource.com'))
|
|
self.assertEqual(
|
|
'Bearer example-bearer-token',
|
|
auth.get_auth_header('some-review.example.com'))
|
|
|
|
def testGetAuthEmail(self):
|
|
auth = gerrit_util.CookiesAuthenticator()
|
|
self.assertEqual(
|
|
'user@chromium.org',
|
|
auth.get_auth_email('chromium.googlesource.com'))
|
|
self.assertEqual(
|
|
'user@chromium.org',
|
|
auth.get_auth_email('chromium-review.googlesource.com'))
|
|
self.assertIsNone(auth.get_auth_email('some-review.example.com'))
|
|
|
|
|
|
class GceAuthenticatorTest(unittest.TestCase):
|
|
def setUp(self):
|
|
super(GceAuthenticatorTest, self).setUp()
|
|
mock.patch('httplib2.Http').start()
|
|
mock.patch('os.getenv', return_value=None).start()
|
|
mock.patch('gerrit_util.time_sleep').start()
|
|
mock.patch('gerrit_util.time_time').start()
|
|
self.addCleanup(mock.patch.stopall)
|
|
# GceAuthenticator has class variables that cache the results. Build a new
|
|
# class for every test to avoid inter-test dependencies.
|
|
class GceAuthenticator(gerrit_util.GceAuthenticator):
|
|
pass
|
|
self.GceAuthenticator = GceAuthenticator
|
|
|
|
def testIsGce_EnvVarSkip(self, *_mocks):
|
|
os.getenv.return_value = '1'
|
|
self.assertFalse(self.GceAuthenticator.is_gce())
|
|
os.getenv.assert_called_once_with('SKIP_GCE_AUTH_FOR_GIT')
|
|
|
|
def testIsGce_Error(self):
|
|
httplib2.Http().request.side_effect = httplib2.HttpLib2Error
|
|
self.assertFalse(self.GceAuthenticator.is_gce())
|
|
|
|
def testIsGce_500(self):
|
|
httplib2.Http().request.return_value = (mock.Mock(status=500), None)
|
|
self.assertFalse(self.GceAuthenticator.is_gce())
|
|
last_call = gerrit_util.time_sleep.mock_calls[-1]
|
|
self.assertLessEqual(last_call, mock.call(43.0))
|
|
|
|
def testIsGce_FailsThenSucceeds(self):
|
|
response = mock.Mock(status=200)
|
|
response.get.return_value = 'Google'
|
|
httplib2.Http().request.side_effect = [
|
|
(mock.Mock(status=500), None),
|
|
(response, 'who cares'),
|
|
]
|
|
self.assertTrue(self.GceAuthenticator.is_gce())
|
|
|
|
def testIsGce_MetadataFlavorIsNotGoogle(self):
|
|
response = mock.Mock(status=200)
|
|
response.get.return_value = None
|
|
httplib2.Http().request.return_value = (response, 'who cares')
|
|
self.assertFalse(self.GceAuthenticator.is_gce())
|
|
response.get.assert_called_once_with('metadata-flavor')
|
|
|
|
def testIsGce_ResultIsCached(self):
|
|
response = mock.Mock(status=200)
|
|
response.get.return_value = 'Google'
|
|
httplib2.Http().request.side_effect = [(response, 'who cares')]
|
|
self.assertTrue(self.GceAuthenticator.is_gce())
|
|
self.assertTrue(self.GceAuthenticator.is_gce())
|
|
httplib2.Http().request.assert_called_once()
|
|
|
|
def testGetAuthHeader_Error(self):
|
|
httplib2.Http().request.side_effect = httplib2.HttpLib2Error
|
|
self.assertIsNone(self.GceAuthenticator().get_auth_header(''))
|
|
|
|
def testGetAuthHeader_500(self):
|
|
httplib2.Http().request.return_value = (mock.Mock(status=500), None)
|
|
self.assertIsNone(self.GceAuthenticator().get_auth_header(''))
|
|
|
|
def testGetAuthHeader_Non200(self):
|
|
httplib2.Http().request.return_value = (mock.Mock(status=403), None)
|
|
self.assertIsNone(self.GceAuthenticator().get_auth_header(''))
|
|
|
|
def testGetAuthHeader_OK(self):
|
|
httplib2.Http().request.return_value = (
|
|
mock.Mock(status=200),
|
|
'{"expires_in": 125, "token_type": "TYPE", "access_token": "TOKEN"}')
|
|
gerrit_util.time_time.return_value = 0
|
|
self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
|
|
|
|
def testGetAuthHeader_Cache(self):
|
|
httplib2.Http().request.return_value = (
|
|
mock.Mock(status=200),
|
|
'{"expires_in": 125, "token_type": "TYPE", "access_token": "TOKEN"}')
|
|
gerrit_util.time_time.return_value = 0
|
|
self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
|
|
self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
|
|
httplib2.Http().request.assert_called_once()
|
|
|
|
def testGetAuthHeader_CacheOld(self):
|
|
httplib2.Http().request.return_value = (
|
|
mock.Mock(status=200),
|
|
'{"expires_in": 125, "token_type": "TYPE", "access_token": "TOKEN"}')
|
|
gerrit_util.time_time.side_effect = [0, 100, 200]
|
|
self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
|
|
self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
|
|
self.assertEqual(2, len(httplib2.Http().request.mock_calls))
|
|
|
|
|
|
class GerritUtilTest(unittest.TestCase):
|
|
def setUp(self):
|
|
super(GerritUtilTest, self).setUp()
|
|
mock.patch('gerrit_util.LOGGER').start()
|
|
mock.patch('gerrit_util.time_sleep').start()
|
|
mock.patch('metrics.collector').start()
|
|
mock.patch(
|
|
'metrics_utils.extract_http_metrics',
|
|
return_value='http_metrics').start()
|
|
self.addCleanup(mock.patch.stopall)
|
|
|
|
def testQueryString(self):
|
|
self.assertEqual('', gerrit_util._QueryString([]))
|
|
self.assertEqual(
|
|
'first%20param%2B', gerrit_util._QueryString([], 'first param+'))
|
|
self.assertEqual(
|
|
'key:val+foo:bar',
|
|
gerrit_util._QueryString([('key', 'val'), ('foo', 'bar')]))
|
|
self.assertEqual(
|
|
'first%20param%2B+key:val+foo:bar',
|
|
gerrit_util._QueryString(
|
|
[('key', 'val'), ('foo', 'bar')], 'first param+'))
|
|
|
|
@mock.patch('gerrit_util.Authenticator')
|
|
def testCreateHttpConn_Basic(self, mockAuth):
|
|
mockAuth.get().get_auth_header.return_value = None
|
|
conn = gerrit_util.CreateHttpConn('host.example.com', 'foo/bar')
|
|
self.assertEqual('host.example.com', conn.req_host)
|
|
self.assertEqual({
|
|
'uri': 'https://host.example.com/foo/bar',
|
|
'method': 'GET',
|
|
'headers': {},
|
|
'body': None,
|
|
}, conn.req_params)
|
|
|
|
@mock.patch('gerrit_util.Authenticator')
|
|
def testCreateHttpConn_Authenticated(self, mockAuth):
|
|
mockAuth.get().get_auth_header.return_value = 'Bearer token'
|
|
conn = gerrit_util.CreateHttpConn(
|
|
'host.example.com', 'foo/bar', headers={'header': 'value'})
|
|
self.assertEqual('host.example.com', conn.req_host)
|
|
self.assertEqual({
|
|
'uri': 'https://host.example.com/a/foo/bar',
|
|
'method': 'GET',
|
|
'headers': {'Authorization': 'Bearer token', 'header': 'value'},
|
|
'body': None,
|
|
}, conn.req_params)
|
|
|
|
@mock.patch('gerrit_util.Authenticator')
|
|
def testCreateHttpConn_Body(self, mockAuth):
|
|
mockAuth.get().get_auth_header.return_value = None
|
|
conn = gerrit_util.CreateHttpConn(
|
|
'host.example.com', 'foo/bar', body={'l': [1, 2, 3], 'd': {'k': 'v'}})
|
|
self.assertEqual('host.example.com', conn.req_host)
|
|
self.assertEqual({
|
|
'uri': 'https://host.example.com/foo/bar',
|
|
'method': 'GET',
|
|
'headers': {'Content-Type': 'application/json'},
|
|
'body': '{"d": {"k": "v"}, "l": [1, 2, 3]}',
|
|
}, conn.req_params)
|
|
|
|
def testReadHttpResponse_200(self):
|
|
conn = mock.Mock()
|
|
conn.req_params = {'uri': 'uri', 'method': 'method'}
|
|
conn.request.return_value = (mock.Mock(status=200), b'content\xe2\x9c\x94')
|
|
|
|
content = gerrit_util.ReadHttpResponse(conn)
|
|
self.assertEqual('content✔', content.getvalue())
|
|
metrics.collector.add_repeated.assert_called_once_with(
|
|
'http_requests', 'http_metrics')
|
|
|
|
def testReadHttpResponse_AuthenticationIssue(self):
|
|
for status in (302, 401, 403):
|
|
response = mock.Mock(status=status)
|
|
response.get.return_value = None
|
|
conn = mock.Mock(req_params={'uri': 'uri', 'method': 'method'})
|
|
conn.request.return_value = (response, b'')
|
|
|
|
with mock.patch('sys.stdout', StringIO()):
|
|
with self.assertRaises(gerrit_util.GerritError) as cm:
|
|
gerrit_util.ReadHttpResponse(conn)
|
|
|
|
self.assertEqual(status, cm.exception.http_status)
|
|
self.assertIn(
|
|
'Your Gerrit credentials might be misconfigured',
|
|
sys.stdout.getvalue())
|
|
|
|
def testReadHttpResponse_ClientError(self):
|
|
conn = mock.Mock(req_params={'uri': 'uri', 'method': 'method'})
|
|
conn.request.return_value = (mock.Mock(status=404), b'')
|
|
|
|
with self.assertRaises(gerrit_util.GerritError) as cm:
|
|
gerrit_util.ReadHttpResponse(conn)
|
|
|
|
self.assertEqual(404, cm.exception.http_status)
|
|
|
|
def readHttpResponse_ServerErrorHelper(self, status):
|
|
conn = mock.Mock(req_params={'uri': 'uri', 'method': 'method'})
|
|
conn.request.return_value = (mock.Mock(status=status), b'')
|
|
|
|
with self.assertRaises(gerrit_util.GerritError) as cm:
|
|
gerrit_util.ReadHttpResponse(conn)
|
|
|
|
self.assertEqual(status, cm.exception.http_status)
|
|
self.assertEqual(gerrit_util.TRY_LIMIT, len(conn.request.mock_calls))
|
|
last_call = gerrit_util.time_sleep.mock_calls[-1]
|
|
self.assertLessEqual(last_call, mock.call(422.0))
|
|
|
|
def testReadHttpResponse_ServerError(self):
|
|
self.readHttpResponse_ServerErrorHelper(status=404)
|
|
self.readHttpResponse_ServerErrorHelper(status=409)
|
|
self.readHttpResponse_ServerErrorHelper(status=429)
|
|
self.readHttpResponse_ServerErrorHelper(status=500)
|
|
|
|
def testReadHttpResponse_ServerErrorAndSuccess(self):
|
|
conn = mock.Mock(req_params={'uri': 'uri', 'method': 'method'})
|
|
conn.request.side_effect = [
|
|
(mock.Mock(status=500), b''),
|
|
(mock.Mock(status=200), b'content\xe2\x9c\x94'),
|
|
]
|
|
|
|
self.assertEqual('content✔', gerrit_util.ReadHttpResponse(conn).getvalue())
|
|
self.assertEqual(2, len(conn.request.mock_calls))
|
|
gerrit_util.time_sleep.assert_called_once_with(10.0)
|
|
|
|
def testReadHttpResponse_Expected404(self):
|
|
conn = mock.Mock()
|
|
conn.req_params = {'uri': 'uri', 'method': 'method'}
|
|
conn.request.return_value = (mock.Mock(status=404), b'content\xe2\x9c\x94')
|
|
|
|
content = gerrit_util.ReadHttpResponse(conn, (404,))
|
|
self.assertEqual('', content.getvalue())
|
|
|
|
@mock.patch('gerrit_util.ReadHttpResponse')
|
|
def testReadHttpJsonResponse_NotJSON(self, mockReadHttpResponse):
|
|
mockReadHttpResponse.return_value = StringIO('not json')
|
|
with self.assertRaises(gerrit_util.GerritError) as cm:
|
|
gerrit_util.ReadHttpJsonResponse(None)
|
|
self.assertEqual(cm.exception.http_status, 200)
|
|
self.assertEqual(
|
|
cm.exception.message, '(200) Unexpected json output: not json')
|
|
|
|
@mock.patch('gerrit_util.ReadHttpResponse')
|
|
def testReadHttpJsonResponse_EmptyValue(self, mockReadHttpResponse):
|
|
mockReadHttpResponse.return_value = StringIO(')]}\'')
|
|
self.assertIsNone(gerrit_util.ReadHttpJsonResponse(None))
|
|
|
|
@mock.patch('gerrit_util.ReadHttpResponse')
|
|
def testReadHttpJsonResponse_JSON(self, mockReadHttpResponse):
|
|
expected_value = {'foo': 'bar', 'baz': [1, '2', 3]}
|
|
mockReadHttpResponse.return_value = StringIO(
|
|
')]}\'\n' + json.dumps(expected_value))
|
|
self.assertEqual(expected_value, gerrit_util.ReadHttpJsonResponse(None))
|
|
|
|
@mock.patch('gerrit_util.CreateHttpConn')
|
|
@mock.patch('gerrit_util.ReadHttpJsonResponse')
|
|
def testQueryChanges(self, mockJsonResponse, mockCreateHttpConn):
|
|
gerrit_util.QueryChanges(
|
|
'host', [('key', 'val'), ('foo', 'bar')], 'first param', limit=500,
|
|
o_params=['PARAM_A', 'PARAM_B'], start='start')
|
|
mockCreateHttpConn.assert_called_once_with(
|
|
'host',
|
|
('changes/?q=first%20param+key:val+foo:bar'
|
|
'&start=start'
|
|
'&n=500'
|
|
'&o=PARAM_A'
|
|
'&o=PARAM_B'))
|
|
|
|
def testQueryChanges_NoParams(self):
|
|
self.assertRaises(RuntimeError, gerrit_util.QueryChanges, 'host', [])
|
|
|
|
@mock.patch('gerrit_util.QueryChanges')
|
|
def testGenerateAllChanges(self, mockQueryChanges):
|
|
mockQueryChanges.side_effect = [
|
|
# First results page
|
|
[
|
|
{'_number': '4'},
|
|
{'_number': '3'},
|
|
{'_number': '2', '_more_changes': True},
|
|
],
|
|
# Second results page, there are new changes, so second page includes
|
|
# some results from the first page.
|
|
[
|
|
{'_number': '2'},
|
|
{'_number': '1'},
|
|
],
|
|
# GenerateAllChanges queries again from the start to get any new
|
|
# changes (5 in this case).
|
|
[
|
|
{'_number': '5'},
|
|
{'_number': '4'},
|
|
{'_number': '3', '_more_changes': True},
|
|
|
|
],
|
|
]
|
|
|
|
changes = list(gerrit_util.GenerateAllChanges('host', 'params'))
|
|
self.assertEqual(
|
|
[
|
|
{'_number': '4'},
|
|
{'_number': '3'},
|
|
{'_number': '2', '_more_changes': True},
|
|
{'_number': '1'},
|
|
{'_number': '5'},
|
|
],
|
|
changes)
|
|
self.assertEqual(
|
|
[
|
|
mock.call('host', 'params', None, 500, None, 0),
|
|
mock.call('host', 'params', None, 500, None, 3),
|
|
mock.call('host', 'params', None, 500, None, 0),
|
|
],
|
|
mockQueryChanges.mock_calls)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|