Files
Kyle McCormick 11626148d9 refactor: switch from mock to unittest.mock (#34844)
As of Python 3.3, the 3rd-party `mock` package has been subsumed into the
standard `unittest.mock` package. Refactoring tests to use the latter will
allow us to drop `mock` as a dependency, which is currently coming in
transitively through requirements/edx/paver.in.

We don't actually drop the `mock` dependency in this PR. That will happen
naturally in:

* https://github.com/openedx/edx-platform/pull/34830
2024-05-22 13:52:24 -04:00

194 lines
6.4 KiB
Python

"""
Tests for triggering a Jenkins job.
"""
import json
import re
import unittest
from itertools import islice
from unittest.mock import Mock, call, mock_open, patch
import backoff
import ddt
import requests_mock
import scripts.user_retirement.utils.jenkins as jenkins
from scripts.user_retirement.utils.exception import BackendError
BASE_URL = u'https://test-jenkins'
USER_ID = u'foo'
USER_TOKEN = u'12345678901234567890123456789012'
JOB = u'test-job'
TOKEN = u'asdf'
BUILD_NUM = 456
JOBS_URL = u'{}/job/{}/'.format(BASE_URL, JOB)
JOB_URL = u'{}{}'.format(JOBS_URL, BUILD_NUM)
MOCK_BUILD = {u'number': BUILD_NUM, u'url': JOB_URL}
MOCK_JENKINS_DATA = {'jobs': [{'name': JOB, 'url': JOBS_URL, 'color': 'blue'}]}
MOCK_BUILDS_DATA = {
'actions': [
{'parameterDefinitions': [
{'defaultParameterValue': {'value': '0'}, 'name': 'EXIT_CODE', 'type': 'StringParameterDefinition'}
]}
],
'builds': [MOCK_BUILD],
'lastBuild': MOCK_BUILD
}
MOCK_QUEUE_DATA = {
'id': 123,
'task': {'name': JOB, 'url': JOBS_URL},
'executable': {'number': BUILD_NUM, 'url': JOB_URL}
}
MOCK_BUILD_DATA = {
'actions': [{}],
'fullDisplayName': 'foo',
'number': BUILD_NUM,
'result': 'SUCCESS',
'url': JOB_URL,
}
MOCK_CRUMB_DATA = {
'crumbRequestField': 'Jenkins-Crumb',
'crumb': '1234567890'
}
class TestProperties(unittest.TestCase):
"""
Test the Jenkins property-creating methods.
"""
def test_properties_files(self):
learners = [
{
'original_username': 'learnerA'
},
{
'original_username': 'learnerB'
},
]
open_mocker = mock_open()
with patch('scripts.user_retirement.utils.jenkins.open', open_mocker, create=True):
jenkins._recreate_directory = Mock() # pylint: disable=protected-access
jenkins.export_learner_job_properties(learners, "tmpdir")
jenkins._recreate_directory.assert_called_once() # pylint: disable=protected-access
self.assertIn(call('tmpdir/learner_retire_learnera', 'w'), open_mocker.call_args_list)
self.assertIn(call('tmpdir/learner_retire_learnerb', 'w'), open_mocker.call_args_list)
handle = open_mocker()
self.assertIn(call('RETIREMENT_USERNAME=learnerA\n'), handle.write.call_args_list)
self.assertIn(call('RETIREMENT_USERNAME=learnerB\n'), handle.write.call_args_list)
@ddt.ddt
class TestBackoff(unittest.TestCase):
u"""
Test of custom backoff code (wait time generator and max_tries)
"""
@ddt.data(
(2, 1, 1, 2, [1]),
(2, 1, 2, 3, [1, 1]),
(2, 1, 3, 3, [1, 2]),
(2, 100, 90, 2, [90]),
(2, 1, 90, 8, [1, 2, 4, 8, 16, 32, 27]),
(3, 5, 1000, 7, [5, 15, 45, 135, 405, 395]),
(2, 1, 3600, 13, [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 1553]),
)
@ddt.unpack
def test_max_timeout(self, base, factor, timeout, expected_max_tries, expected_waits):
# pylint: disable=protected-access
wait_gen, max_tries = jenkins._backoff_timeout(timeout, base, factor)
self.assertEqual(expected_max_tries, max_tries)
# Use max_tries-1, because we only wait that many times
waits = list(islice(wait_gen(), max_tries - 1))
self.assertEqual(expected_waits, waits)
self.assertEqual(timeout, sum(waits))
def test_backoff_call(self):
# pylint: disable=protected-access
wait_gen, max_tries = jenkins._backoff_timeout(timeout=.36, base=2, factor=.0001)
always_false = Mock(return_value=False)
count_retries = backoff.on_predicate(
wait_gen,
max_tries=max_tries,
on_backoff=print,
jitter=None,
)(always_false.__call__)
count_retries()
self.assertEqual(always_false.call_count, 13)
@ddt.ddt
class TestJenkinsAPI(unittest.TestCase):
"""
Tests for interacting with the Jenkins API
"""
@requests_mock.Mocker()
def test_failure(self, mock):
"""
Test the failure condition when triggering a jenkins job
"""
# Mock all network interactions
mock.get(
re.compile(".*"),
status_code=404,
)
with self.assertRaises(BackendError):
jenkins.trigger_build(BASE_URL, USER_ID, USER_TOKEN, JOB, TOKEN, None, ())
@ddt.data(
(None, ()),
('my cause', ()),
(None, ((u'FOO', u'bar'),)),
(None, ((u'FOO', u'bar'), (u'BAZ', u'biz'))),
('my cause', ((u'FOO', u'bar'),)),
)
@ddt.unpack
@requests_mock.Mocker()
def test_success(self, cause, param, mock):
u"""
Test triggering a jenkins job
"""
def text_callback(request, context):
u""" What to return from the mock. """
# This is the initial call that jenkinsapi uses to
# establish connectivity to Jenkins
# https://test-jenkins/api/python?tree=jobs[name,color,url]
context.status_code = 200
if request.url.startswith(u'https://test-jenkins/api/python'):
return json.dumps(MOCK_JENKINS_DATA)
elif request.url.startswith(u'https://test-jenkins/job/test-job/456'):
return json.dumps(MOCK_BUILD_DATA)
elif request.url.startswith(u'https://test-jenkins/job/test-job'):
return json.dumps(MOCK_BUILDS_DATA)
elif request.url.startswith(u'https://test-jenkins/queue/item/123/api/python'):
return json.dumps(MOCK_QUEUE_DATA)
elif request.url.startswith(u'https://test-jenkins/crumbIssuer/api/python'):
return json.dumps(MOCK_CRUMB_DATA)
else:
# We should never get here, unless the jenkinsapi implementation changes.
# This response will catch that condition.
context.status_code = 500
return None
# Mock all network interactions
mock.get(
re.compile('.*'),
text=text_callback
)
mock.post(
'{}/job/test-job/buildWithParameters'.format(BASE_URL),
status_code=201, # Jenkins responds with a 201 Created on success
headers={'location': '{}/queue/item/123'.format(BASE_URL)}
)
# Make the call to the Jenkins API
result = jenkins.trigger_build(BASE_URL, USER_ID, USER_TOKEN, JOB, TOKEN, cause, param)
self.assertEqual(result, 'SUCCESS')