gerrit_util_test.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. #!/usr/bin/env vpython3
  2. # coding=utf-8
  3. # Copyright (c) 2019 The Chromium Authors. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. from __future__ import print_function
  7. from __future__ import unicode_literals
  8. import base64
  9. import httplib2
  10. import json
  11. import os
  12. import sys
  13. import unittest
  14. if sys.version_info.major == 2:
  15. from cStringIO import StringIO
  16. import mock
  17. else:
  18. from io import StringIO
  19. from unittest import mock
  20. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  21. import gerrit_util
  22. import gclient_utils
  23. import metrics
  24. import metrics_utils
  25. import subprocess2
  26. class CookiesAuthenticatorTest(unittest.TestCase):
  27. _GITCOOKIES = '\n'.join([
  28. '\t'.join([
  29. 'chromium.googlesource.com',
  30. 'FALSE',
  31. '/',
  32. 'TRUE',
  33. '2147483647',
  34. 'o',
  35. 'git-user.chromium.org=1/chromium-secret',
  36. ]),
  37. '\t'.join([
  38. 'chromium-review.googlesource.com',
  39. 'FALSE',
  40. '/',
  41. 'TRUE',
  42. '2147483647',
  43. 'o',
  44. 'git-user.chromium.org=1/chromium-secret',
  45. ]),
  46. '\t'.join([
  47. '.example.com',
  48. 'FALSE',
  49. '/',
  50. 'TRUE',
  51. '2147483647',
  52. 'o',
  53. 'example-bearer-token',
  54. ]),
  55. '\t'.join([
  56. 'another-path.example.com',
  57. 'FALSE',
  58. '/foo',
  59. 'TRUE',
  60. '2147483647',
  61. 'o',
  62. 'git-example.com=1/another-path-secret',
  63. ]),
  64. '\t'.join([
  65. 'another-key.example.com',
  66. 'FALSE',
  67. '/',
  68. 'TRUE',
  69. '2147483647',
  70. 'not-o',
  71. 'git-example.com=1/another-key-secret',
  72. ]),
  73. '#' + '\t'.join([
  74. 'chromium-review.googlesource.com',
  75. 'FALSE',
  76. '/',
  77. 'TRUE',
  78. '2147483647',
  79. 'o',
  80. 'git-invalid-user.chromium.org=1/invalid-chromium-secret',
  81. ]),
  82. 'Some unrelated line\t that should not be here',
  83. ])
  84. def setUp(self):
  85. mock.patch('gclient_utils.FileRead', return_value=self._GITCOOKIES).start()
  86. mock.patch('os.getenv', return_value={}).start()
  87. mock.patch('os.environ', {'HOME': '$HOME'}).start()
  88. mock.patch('os.path.exists', return_value=True).start()
  89. mock.patch(
  90. 'subprocess2.check_output',
  91. side_effect=[
  92. subprocess2.CalledProcessError(1, ['cmd'], 'cwd', 'out', 'err')],
  93. ).start()
  94. self.addCleanup(mock.patch.stopall)
  95. self.maxDiff = None
  96. def testGetNewPasswordUrl(self):
  97. auth = gerrit_util.CookiesAuthenticator()
  98. self.assertEqual('https://chromium.googlesource.com/new-password',
  99. auth.get_new_password_url('chromium.googlesource.com'))
  100. self.assertEqual(
  101. 'https://chrome-internal.googlesource.com/new-password',
  102. auth.get_new_password_url('chrome-internal-review.googlesource.com'))
  103. def testGetNewPasswordMessage(self):
  104. auth = gerrit_util.CookiesAuthenticator()
  105. self.assertIn(
  106. 'https://chromium.googlesource.com/new-password',
  107. auth.get_new_password_message('chromium-review.googlesource.com'))
  108. self.assertIn(
  109. 'https://chrome-internal.googlesource.com/new-password',
  110. auth.get_new_password_message('chrome-internal.googlesource.com'))
  111. def testGetGitcookiesPath(self):
  112. self.assertEqual(
  113. os.path.expanduser(os.path.join('~', '.gitcookies')),
  114. gerrit_util.CookiesAuthenticator().get_gitcookies_path())
  115. subprocess2.check_output.side_effect = [b'http.cookiefile']
  116. self.assertEqual(
  117. 'http.cookiefile',
  118. gerrit_util.CookiesAuthenticator().get_gitcookies_path())
  119. subprocess2.check_output.assert_called_with(
  120. ['git', 'config', '--path', 'http.cookiefile'])
  121. os.getenv.return_value = 'git-cookies-path'
  122. self.assertEqual(
  123. 'git-cookies-path',
  124. gerrit_util.CookiesAuthenticator().get_gitcookies_path())
  125. os.getenv.assert_called_with('GIT_COOKIES_PATH')
  126. def testGitcookies(self):
  127. auth = gerrit_util.CookiesAuthenticator()
  128. self.assertEqual(auth.gitcookies, {
  129. 'chromium.googlesource.com':
  130. ('git-user.chromium.org', '1/chromium-secret'),
  131. 'chromium-review.googlesource.com':
  132. ('git-user.chromium.org', '1/chromium-secret'),
  133. '.example.com':
  134. ('', 'example-bearer-token'),
  135. })
  136. def testGetAuthHeader(self):
  137. expected_chromium_header = (
  138. 'Basic Z2l0LXVzZXIuY2hyb21pdW0ub3JnOjEvY2hyb21pdW0tc2VjcmV0')
  139. auth = gerrit_util.CookiesAuthenticator()
  140. self.assertEqual(
  141. expected_chromium_header,
  142. auth.get_auth_header('chromium.googlesource.com'))
  143. self.assertEqual(
  144. expected_chromium_header,
  145. auth.get_auth_header('chromium-review.googlesource.com'))
  146. self.assertEqual(
  147. 'Bearer example-bearer-token',
  148. auth.get_auth_header('some-review.example.com'))
  149. def testGetAuthEmail(self):
  150. auth = gerrit_util.CookiesAuthenticator()
  151. self.assertEqual(
  152. 'user@chromium.org',
  153. auth.get_auth_email('chromium.googlesource.com'))
  154. self.assertEqual(
  155. 'user@chromium.org',
  156. auth.get_auth_email('chromium-review.googlesource.com'))
  157. self.assertIsNone(auth.get_auth_email('some-review.example.com'))
  158. class GceAuthenticatorTest(unittest.TestCase):
  159. def setUp(self):
  160. super(GceAuthenticatorTest, self).setUp()
  161. mock.patch('httplib2.Http').start()
  162. mock.patch('os.getenv', return_value=None).start()
  163. mock.patch('gerrit_util.time_sleep').start()
  164. mock.patch('gerrit_util.time_time').start()
  165. self.addCleanup(mock.patch.stopall)
  166. # GceAuthenticator has class variables that cache the results. Build a new
  167. # class for every test to avoid inter-test dependencies.
  168. class GceAuthenticator(gerrit_util.GceAuthenticator):
  169. pass
  170. self.GceAuthenticator = GceAuthenticator
  171. def testIsGce_EnvVarSkip(self, *_mocks):
  172. os.getenv.return_value = '1'
  173. self.assertFalse(self.GceAuthenticator.is_gce())
  174. os.getenv.assert_called_once_with('SKIP_GCE_AUTH_FOR_GIT')
  175. def testIsGce_Error(self):
  176. httplib2.Http().request.side_effect = httplib2.HttpLib2Error
  177. self.assertFalse(self.GceAuthenticator.is_gce())
  178. def testIsGce_500(self):
  179. httplib2.Http().request.return_value = (mock.Mock(status=500), None)
  180. self.assertFalse(self.GceAuthenticator.is_gce())
  181. last_call = gerrit_util.time_sleep.mock_calls[-1]
  182. self.assertLessEqual(last_call, mock.call(43.0))
  183. def testIsGce_FailsThenSucceeds(self):
  184. response = mock.Mock(status=200)
  185. response.get.return_value = 'Google'
  186. httplib2.Http().request.side_effect = [
  187. (mock.Mock(status=500), None),
  188. (response, 'who cares'),
  189. ]
  190. self.assertTrue(self.GceAuthenticator.is_gce())
  191. def testIsGce_MetadataFlavorIsNotGoogle(self):
  192. response = mock.Mock(status=200)
  193. response.get.return_value = None
  194. httplib2.Http().request.return_value = (response, 'who cares')
  195. self.assertFalse(self.GceAuthenticator.is_gce())
  196. response.get.assert_called_once_with('metadata-flavor')
  197. def testIsGce_ResultIsCached(self):
  198. response = mock.Mock(status=200)
  199. response.get.return_value = 'Google'
  200. httplib2.Http().request.side_effect = [(response, 'who cares')]
  201. self.assertTrue(self.GceAuthenticator.is_gce())
  202. self.assertTrue(self.GceAuthenticator.is_gce())
  203. httplib2.Http().request.assert_called_once()
  204. def testGetAuthHeader_Error(self):
  205. httplib2.Http().request.side_effect = httplib2.HttpLib2Error
  206. self.assertIsNone(self.GceAuthenticator().get_auth_header(''))
  207. def testGetAuthHeader_500(self):
  208. httplib2.Http().request.return_value = (mock.Mock(status=500), None)
  209. self.assertIsNone(self.GceAuthenticator().get_auth_header(''))
  210. def testGetAuthHeader_Non200(self):
  211. httplib2.Http().request.return_value = (mock.Mock(status=403), None)
  212. self.assertIsNone(self.GceAuthenticator().get_auth_header(''))
  213. def testGetAuthHeader_OK(self):
  214. httplib2.Http().request.return_value = (
  215. mock.Mock(status=200),
  216. '{"expires_in": 125, "token_type": "TYPE", "access_token": "TOKEN"}')
  217. gerrit_util.time_time.return_value = 0
  218. self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
  219. def testGetAuthHeader_Cache(self):
  220. httplib2.Http().request.return_value = (
  221. mock.Mock(status=200),
  222. '{"expires_in": 125, "token_type": "TYPE", "access_token": "TOKEN"}')
  223. gerrit_util.time_time.return_value = 0
  224. self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
  225. self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
  226. httplib2.Http().request.assert_called_once()
  227. def testGetAuthHeader_CacheOld(self):
  228. httplib2.Http().request.return_value = (
  229. mock.Mock(status=200),
  230. '{"expires_in": 125, "token_type": "TYPE", "access_token": "TOKEN"}')
  231. gerrit_util.time_time.side_effect = [0, 100, 200]
  232. self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
  233. self.assertEqual('TYPE TOKEN', self.GceAuthenticator().get_auth_header(''))
  234. self.assertEqual(2, len(httplib2.Http().request.mock_calls))
  235. class GerritUtilTest(unittest.TestCase):
  236. def setUp(self):
  237. super(GerritUtilTest, self).setUp()
  238. mock.patch('gerrit_util.LOGGER').start()
  239. mock.patch('gerrit_util.time_sleep').start()
  240. mock.patch('metrics.collector').start()
  241. mock.patch(
  242. 'metrics_utils.extract_http_metrics',
  243. return_value='http_metrics').start()
  244. self.addCleanup(mock.patch.stopall)
  245. def testQueryString(self):
  246. self.assertEqual('', gerrit_util._QueryString([]))
  247. self.assertEqual(
  248. 'first%20param%2B', gerrit_util._QueryString([], 'first param+'))
  249. self.assertEqual(
  250. 'key:val+foo:bar',
  251. gerrit_util._QueryString([('key', 'val'), ('foo', 'bar')]))
  252. self.assertEqual(
  253. 'first%20param%2B+key:val+foo:bar',
  254. gerrit_util._QueryString(
  255. [('key', 'val'), ('foo', 'bar')], 'first param+'))
  256. @mock.patch('gerrit_util.Authenticator')
  257. def testCreateHttpConn_Basic(self, mockAuth):
  258. mockAuth.get().get_auth_header.return_value = None
  259. conn = gerrit_util.CreateHttpConn('host.example.com', 'foo/bar')
  260. self.assertEqual('host.example.com', conn.req_host)
  261. self.assertEqual({
  262. 'uri': 'https://host.example.com/foo/bar',
  263. 'method': 'GET',
  264. 'headers': {},
  265. 'body': None,
  266. }, conn.req_params)
  267. @mock.patch('gerrit_util.Authenticator')
  268. def testCreateHttpConn_Authenticated(self, mockAuth):
  269. mockAuth.get().get_auth_header.return_value = 'Bearer token'
  270. conn = gerrit_util.CreateHttpConn(
  271. 'host.example.com', 'foo/bar', headers={'header': 'value'})
  272. self.assertEqual('host.example.com', conn.req_host)
  273. self.assertEqual({
  274. 'uri': 'https://host.example.com/a/foo/bar',
  275. 'method': 'GET',
  276. 'headers': {'Authorization': 'Bearer token', 'header': 'value'},
  277. 'body': None,
  278. }, conn.req_params)
  279. @mock.patch('gerrit_util.Authenticator')
  280. def testCreateHttpConn_Body(self, mockAuth):
  281. mockAuth.get().get_auth_header.return_value = None
  282. conn = gerrit_util.CreateHttpConn(
  283. 'host.example.com', 'foo/bar', body={'l': [1, 2, 3], 'd': {'k': 'v'}})
  284. self.assertEqual('host.example.com', conn.req_host)
  285. self.assertEqual({
  286. 'uri': 'https://host.example.com/foo/bar',
  287. 'method': 'GET',
  288. 'headers': {'Content-Type': 'application/json'},
  289. 'body': '{"d": {"k": "v"}, "l": [1, 2, 3]}',
  290. }, conn.req_params)
  291. def testReadHttpResponse_200(self):
  292. conn = mock.Mock()
  293. conn.req_params = {'uri': 'uri', 'method': 'method'}
  294. conn.request.return_value = (mock.Mock(status=200), b'content\xe2\x9c\x94')
  295. content = gerrit_util.ReadHttpResponse(conn)
  296. self.assertEqual('content✔', content.getvalue())
  297. metrics.collector.add_repeated.assert_called_once_with(
  298. 'http_requests', 'http_metrics')
  299. def testReadHttpResponse_AuthenticationIssue(self):
  300. for status in (302, 401, 403):
  301. response = mock.Mock(status=status)
  302. response.get.return_value = None
  303. conn = mock.Mock(req_params={'uri': 'uri', 'method': 'method'})
  304. conn.request.return_value = (response, b'')
  305. with mock.patch('sys.stdout', StringIO()):
  306. with self.assertRaises(gerrit_util.GerritError) as cm:
  307. gerrit_util.ReadHttpResponse(conn)
  308. self.assertEqual(status, cm.exception.http_status)
  309. self.assertIn(
  310. 'Your Gerrit credentials might be misconfigured',
  311. sys.stdout.getvalue())
  312. def testReadHttpResponse_ClientError(self):
  313. conn = mock.Mock(req_params={'uri': 'uri', 'method': 'method'})
  314. conn.request.return_value = (mock.Mock(status=404), b'')
  315. with self.assertRaises(gerrit_util.GerritError) as cm:
  316. gerrit_util.ReadHttpResponse(conn)
  317. self.assertEqual(404, cm.exception.http_status)
  318. def readHttpResponse_ServerErrorHelper(self, status):
  319. conn = mock.Mock(req_params={'uri': 'uri', 'method': 'method'})
  320. conn.request.return_value = (mock.Mock(status=status), b'')
  321. with self.assertRaises(gerrit_util.GerritError) as cm:
  322. gerrit_util.ReadHttpResponse(conn)
  323. self.assertEqual(status, cm.exception.http_status)
  324. self.assertEqual(gerrit_util.TRY_LIMIT, len(conn.request.mock_calls))
  325. last_call = gerrit_util.time_sleep.mock_calls[-1]
  326. self.assertLessEqual(last_call, mock.call(422.0))
  327. def testReadHttpResponse_ServerError(self):
  328. self.readHttpResponse_ServerErrorHelper(status=404)
  329. self.readHttpResponse_ServerErrorHelper(status=409)
  330. self.readHttpResponse_ServerErrorHelper(status=429)
  331. self.readHttpResponse_ServerErrorHelper(status=500)
  332. def testReadHttpResponse_ServerErrorAndSuccess(self):
  333. conn = mock.Mock(req_params={'uri': 'uri', 'method': 'method'})
  334. conn.request.side_effect = [
  335. (mock.Mock(status=500), b''),
  336. (mock.Mock(status=200), b'content\xe2\x9c\x94'),
  337. ]
  338. self.assertEqual('content✔', gerrit_util.ReadHttpResponse(conn).getvalue())
  339. self.assertEqual(2, len(conn.request.mock_calls))
  340. gerrit_util.time_sleep.assert_called_once_with(10.0)
  341. def testReadHttpResponse_Expected404(self):
  342. conn = mock.Mock()
  343. conn.req_params = {'uri': 'uri', 'method': 'method'}
  344. conn.request.return_value = (mock.Mock(status=404), b'content\xe2\x9c\x94')
  345. content = gerrit_util.ReadHttpResponse(conn, (404,))
  346. self.assertEqual('', content.getvalue())
  347. @mock.patch('gerrit_util.ReadHttpResponse')
  348. def testReadHttpJsonResponse_NotJSON(self, mockReadHttpResponse):
  349. mockReadHttpResponse.return_value = StringIO('not json')
  350. with self.assertRaises(gerrit_util.GerritError) as cm:
  351. gerrit_util.ReadHttpJsonResponse(None)
  352. self.assertEqual(cm.exception.http_status, 200)
  353. self.assertEqual(
  354. cm.exception.message, '(200) Unexpected json output: not json')
  355. @mock.patch('gerrit_util.ReadHttpResponse')
  356. def testReadHttpJsonResponse_EmptyValue(self, mockReadHttpResponse):
  357. mockReadHttpResponse.return_value = StringIO(')]}\'')
  358. self.assertIsNone(gerrit_util.ReadHttpJsonResponse(None))
  359. @mock.patch('gerrit_util.ReadHttpResponse')
  360. def testReadHttpJsonResponse_JSON(self, mockReadHttpResponse):
  361. expected_value = {'foo': 'bar', 'baz': [1, '2', 3]}
  362. mockReadHttpResponse.return_value = StringIO(
  363. ')]}\'\n' + json.dumps(expected_value))
  364. self.assertEqual(expected_value, gerrit_util.ReadHttpJsonResponse(None))
  365. @mock.patch('gerrit_util.CreateHttpConn')
  366. @mock.patch('gerrit_util.ReadHttpJsonResponse')
  367. def testQueryChanges(self, mockJsonResponse, mockCreateHttpConn):
  368. gerrit_util.QueryChanges(
  369. 'host', [('key', 'val'), ('foo', 'bar baz')], 'first param', limit=500,
  370. o_params=['PARAM_A', 'PARAM_B'], start='start')
  371. mockCreateHttpConn.assert_called_once_with(
  372. 'host',
  373. ('changes/?q=first%20param+key:val+foo:bar+baz'
  374. '&start=start'
  375. '&n=500'
  376. '&o=PARAM_A'
  377. '&o=PARAM_B'))
  378. def testQueryChanges_NoParams(self):
  379. self.assertRaises(RuntimeError, gerrit_util.QueryChanges, 'host', [])
  380. @mock.patch('gerrit_util.QueryChanges')
  381. def testGenerateAllChanges(self, mockQueryChanges):
  382. mockQueryChanges.side_effect = [
  383. # First results page
  384. [
  385. {'_number': '4'},
  386. {'_number': '3'},
  387. {'_number': '2', '_more_changes': True},
  388. ],
  389. # Second results page, there are new changes, so second page includes
  390. # some results from the first page.
  391. [
  392. {'_number': '2'},
  393. {'_number': '1'},
  394. ],
  395. # GenerateAllChanges queries again from the start to get any new
  396. # changes (5 in this case).
  397. [
  398. {'_number': '5'},
  399. {'_number': '4'},
  400. {'_number': '3', '_more_changes': True},
  401. ],
  402. ]
  403. changes = list(gerrit_util.GenerateAllChanges('host', 'params'))
  404. self.assertEqual(
  405. [
  406. {'_number': '4'},
  407. {'_number': '3'},
  408. {'_number': '2', '_more_changes': True},
  409. {'_number': '1'},
  410. {'_number': '5'},
  411. ],
  412. changes)
  413. self.assertEqual(
  414. [
  415. mock.call('host', 'params', None, 500, None, 0),
  416. mock.call('host', 'params', None, 500, None, 3),
  417. mock.call('host', 'params', None, 500, None, 0),
  418. ],
  419. mockQueryChanges.mock_calls)
  420. @mock.patch('gerrit_util.CreateHttpConn')
  421. @mock.patch('gerrit_util.ReadHttpJsonResponse')
  422. def testIsCodeOwnersEnabledOnRepo_Disabled(
  423. self, mockJsonResponse, mockCreateHttpConn):
  424. mockJsonResponse.return_value = {'status': {'disabled': True}}
  425. self.assertFalse(gerrit_util.IsCodeOwnersEnabledOnRepo('host', 'repo'))
  426. @mock.patch('gerrit_util.CreateHttpConn')
  427. @mock.patch('gerrit_util.ReadHttpJsonResponse')
  428. def testIsCodeOwnersEnabledOnRepo_Enabled(
  429. self, mockJsonResponse, mockCreateHttpConn):
  430. mockJsonResponse.return_value = {'status': {}}
  431. self.assertTrue(gerrit_util.IsCodeOwnersEnabledOnRepo('host', 'repo'))
  432. if __name__ == '__main__':
  433. unittest.main()