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