Refactored github tools to prefer token-based auth
The github workflow still asks for username, password, and 2FA token if need be, but creates a repo token and uses that for further exchanges to save on user input fatigue. Closes-Bug: #1442764 Change-Id: I530a965dc5d5bad080c49b5cf0d38f3f7aee201a
This commit is contained in:
@@ -25,24 +25,27 @@ import string
|
|||||||
import httplib2
|
import httplib2
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubException(Exception):
|
||||||
|
def __init__(self, status_code, message):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "GitHub Exception: %s: %s" % (self.status_code, self.message)
|
||||||
|
|
||||||
|
|
||||||
class GitHubAuth(object):
|
class GitHubAuth(object):
|
||||||
_username = None
|
_github_auth_url = 'https://api.github.com/authorizations'
|
||||||
_password = None
|
_github_repo_hook_url = 'https://api.github.com/repos/%s/hooks'
|
||||||
_onetime_password = None
|
_github_user_key_url = 'https://api.github.com/user/keys'
|
||||||
_token = None
|
_github_repo_regex = r'github\.com[:/](.+?)/(.+?)($|/$|\.git$|\.git/$)'
|
||||||
user_org_name = None
|
|
||||||
full_repo_name = None
|
|
||||||
git_url = None
|
|
||||||
|
|
||||||
_auth_url = 'https://api.github.com/authorizations'
|
def __init__(self, git_url, username=None, password=None, repo_token=None):
|
||||||
|
|
||||||
def __init__(self, git_url, username=None, password=None):
|
|
||||||
self.git_url = git_url
|
self.git_url = git_url
|
||||||
|
|
||||||
user_org_name, repo = '', ''
|
user_org_name, repo = '', ''
|
||||||
|
|
||||||
github_regex = r'github\.com[:/](.+?)/(.+?)($|/$|\.git$|\.git/$)'
|
repo_pat = re.compile(self._github_repo_regex)
|
||||||
repo_pat = re.compile(github_regex)
|
|
||||||
match = repo_pat.search(self.git_url)
|
match = repo_pat.search(self.git_url)
|
||||||
if match:
|
if match:
|
||||||
user_org_name, repo = match.group(1), match.group(2)
|
user_org_name, repo = match.group(1), match.group(2)
|
||||||
@@ -53,11 +56,10 @@ class GitHubAuth(object):
|
|||||||
|
|
||||||
self.full_repo_name = '/'.join([user_org_name, repo])
|
self.full_repo_name = '/'.join([user_org_name, repo])
|
||||||
|
|
||||||
|
self._repo_token = repo_token
|
||||||
self._username = username
|
self._username = username
|
||||||
self._password = password
|
self._password = password
|
||||||
|
self._otp_required = False
|
||||||
# If either is None, ask for them now.
|
|
||||||
self.username, self.password
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def username(self):
|
def username(self):
|
||||||
@@ -75,72 +77,94 @@ class GitHubAuth(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def onetime_password(self):
|
def onetime_password(self):
|
||||||
if self._onetime_password is None:
|
# This is prompted for every time it's needed.
|
||||||
self._onetime_password = getpass.getpass("2FA Token: ")
|
print("Two-Factor Authentication required.")
|
||||||
return self._onetime_password
|
return getpass.getpass("2FA Token: ")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def token(self):
|
def repo_token(self):
|
||||||
if self._token is None:
|
if self._repo_token is None:
|
||||||
self._get_repo_token()
|
self.create_repo_token()
|
||||||
return self._token
|
return self._repo_token
|
||||||
|
|
||||||
def _auth_header(self, use_otp=False):
|
@property
|
||||||
authstring = '%s:%s' % (self.username, self.password)
|
def auth_header(self):
|
||||||
auth = ''
|
|
||||||
try:
|
|
||||||
auth = base64.encodestring(authstring)
|
|
||||||
except TypeError:
|
|
||||||
# Python 3
|
|
||||||
auth = base64.encodestring(bytes(authstring, 'utf-8'))
|
|
||||||
header = {
|
header = {
|
||||||
'Authorization': 'Basic %s' % auth,
|
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
if use_otp:
|
|
||||||
|
# The token on its own should suffice
|
||||||
|
if self._repo_token:
|
||||||
|
header['Authorization'] = 'token %s' % self._repo_token
|
||||||
|
return header
|
||||||
|
|
||||||
|
# This will prompt the user if either name or pass is missing.
|
||||||
|
authstring = '%s:%s' % (self.username, self.password)
|
||||||
|
basic_auth = ''
|
||||||
|
try:
|
||||||
|
basic_auth = base64.encodestring(authstring)
|
||||||
|
except TypeError:
|
||||||
|
# Python 3
|
||||||
|
basic_auth = base64.encodestring(bytes(authstring, 'utf-8'))
|
||||||
|
basic_auth = basic_auth.decode('utf-8')
|
||||||
|
basic_auth = basic_auth.strip()
|
||||||
|
header['Authorization'] = 'Basic %s' % basic_auth
|
||||||
|
|
||||||
|
# This will prompt for the OTP.
|
||||||
|
if self._otp_required:
|
||||||
header['x-github-otp'] = self.onetime_password
|
header['x-github-otp'] = self.onetime_password
|
||||||
|
|
||||||
return header
|
return header
|
||||||
|
|
||||||
def _get_repo_token(self):
|
|
||||||
note = ''.join(random.sample(string.lowercase, 5))
|
|
||||||
auth_info = {
|
|
||||||
'scopes': 'repo',
|
|
||||||
'note': 'Solum-status-%s' % note,
|
|
||||||
}
|
|
||||||
resp, content = httplib2.Http().request(
|
|
||||||
self._auth_url,
|
|
||||||
'POST',
|
|
||||||
headers=self._auth_header(),
|
|
||||||
body=json.dumps(auth_info))
|
|
||||||
if resp.get('status') in ['200', '201']:
|
|
||||||
response_body = json.loads(content)
|
|
||||||
self._token = response_body.get('token')
|
|
||||||
else:
|
|
||||||
print("Error getting repo token.")
|
|
||||||
|
|
||||||
def _send_authed_request(self, url, body_dict):
|
def _send_authed_request(self, url, body_dict):
|
||||||
|
body_text = json.dumps(body_dict)
|
||||||
|
|
||||||
resp, content = httplib2.Http().request(
|
resp, content = httplib2.Http().request(
|
||||||
url,
|
url, 'POST',
|
||||||
'POST',
|
headers=self.auth_header,
|
||||||
headers=self._auth_header(),
|
body=body_text)
|
||||||
body=json.dumps(body_dict))
|
|
||||||
if resp.get('status') in ['401']:
|
if resp.get('status') in ['401']:
|
||||||
if resp.get('x-github-otp', '').startswith('required'):
|
if resp.get('x-github-otp', '').startswith('required'):
|
||||||
print("Two-Factor Authentication required.")
|
self._otp_required = True
|
||||||
resp, content = httplib2.Http().request(
|
resp, content = httplib2.Http().request(
|
||||||
url,
|
url, 'POST',
|
||||||
'POST',
|
headers=self.auth_header,
|
||||||
headers=self._auth_header(use_otp=True),
|
body=body_text)
|
||||||
body=json.dumps(body_dict))
|
|
||||||
|
|
||||||
return resp, content
|
return resp, content
|
||||||
|
|
||||||
|
def create_repo_token(self):
|
||||||
|
print("Creating repo token")
|
||||||
|
note = ''.join(random.sample(string.lowercase, 5))
|
||||||
|
auth_info = {
|
||||||
|
'scopes': ['repo', 'write:public_key', 'write:repo_hook'],
|
||||||
|
'note': 'Solum-status-%s' % note,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, content = self._send_authed_request(
|
||||||
|
self._github_auth_url,
|
||||||
|
auth_info)
|
||||||
|
|
||||||
|
status_code = int(resp.get('status', '500'))
|
||||||
|
response_body = json.loads(content)
|
||||||
|
if status_code in [200, 201]:
|
||||||
|
self._repo_token = response_body.get('token')
|
||||||
|
elif status_code >= 400 and status_code < 600:
|
||||||
|
message = response_body.get('message',
|
||||||
|
'No error message provided.')
|
||||||
|
raise GitHubException(status_code, message)
|
||||||
|
|
||||||
def create_webhook(self, trigger_uri, workflow=None):
|
def create_webhook(self, trigger_uri, workflow=None):
|
||||||
hook_url = ('https://api.github.com/repos/%s/hooks' %
|
hook_url = self._github_repo_hook_url % self.full_repo_name
|
||||||
self.full_repo_name)
|
|
||||||
if workflow is not None:
|
if workflow is not None:
|
||||||
|
# workflow is a list of strings, likely
|
||||||
|
# ['unittest', 'build', 'deploy'].
|
||||||
|
# They're joined with + and appended to the
|
||||||
|
# trigger_uri here.
|
||||||
wf_query = "?workflow=%s" % '+'.join(workflow)
|
wf_query = "?workflow=%s" % '+'.join(workflow)
|
||||||
trigger_uri += wf_query
|
trigger_uri += wf_query
|
||||||
|
|
||||||
hook_info = {
|
hook_info = {
|
||||||
'name': 'web',
|
'name': 'web',
|
||||||
'events': ['pull_request', 'commit_comment'],
|
'events': ['pull_request', 'commit_comment'],
|
||||||
@@ -153,18 +177,13 @@ class GitHubAuth(object):
|
|||||||
if resp.get('status') not in ['200', '201']:
|
if resp.get('status') not in ['200', '201']:
|
||||||
print("Error creating webhook.")
|
print("Error creating webhook.")
|
||||||
|
|
||||||
def add_ssh_key(self, public_key=None, is_private=False):
|
def add_ssh_key(self, public_key=None):
|
||||||
if not public_key:
|
if not public_key:
|
||||||
print("No public key to upload.")
|
print("No public key to upload.")
|
||||||
return
|
return
|
||||||
if is_private:
|
print("Uploading public key to user account.")
|
||||||
print("Uploading public key to user account.")
|
api_url = self._github_user_key_url
|
||||||
else:
|
|
||||||
print("Uploading public key to repository.")
|
|
||||||
api_url = ('https://api.github.com/repos/%s/keys' %
|
|
||||||
self.full_repo_name)
|
|
||||||
if is_private:
|
|
||||||
api_url = 'https://api.github.com/user/keys'
|
|
||||||
key_info = {
|
key_info = {
|
||||||
'title': 'devops@Solum',
|
'title': 'devops@Solum',
|
||||||
'key': public_key,
|
'key': public_key,
|
||||||
|
@@ -707,11 +707,15 @@ Available commands:
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
dest='setup_trigger',
|
dest='setup_trigger',
|
||||||
help="Set up app trigger on git repo")
|
help="Set up app trigger on git repo")
|
||||||
|
|
||||||
|
trigger_help = ("Which of stages build, unittest, deploy to trigger "
|
||||||
|
"from git. For example: "
|
||||||
|
"--trigger-workflow=unittest+build+deploy. "
|
||||||
|
"Implies --setup-trigger.")
|
||||||
self.parser.add_argument('--trigger-workflow',
|
self.parser.add_argument('--trigger-workflow',
|
||||||
default='',
|
default='',
|
||||||
dest='workflow',
|
dest='workflow',
|
||||||
help="Which of stages build, unittest, "
|
help=trigger_help)
|
||||||
"deploy to trigger from git")
|
|
||||||
|
|
||||||
args = self.parser.parse_args()
|
args = self.parser.parse_args()
|
||||||
|
|
||||||
@@ -872,6 +876,24 @@ Available commands:
|
|||||||
args.param_file)
|
args.param_file)
|
||||||
raise exc.CommandError(message=message)
|
raise exc.CommandError(message=message)
|
||||||
|
|
||||||
|
repo_token = plan_definition['artifacts'][0].get('repo_token')
|
||||||
|
gha = github.GitHubAuth(git_url, repo_token=repo_token)
|
||||||
|
|
||||||
|
# Set up a repo token for the user if it hasn't already been created.
|
||||||
|
repo_token = repo_token or gha.repo_token
|
||||||
|
plan_definition['artifacts'][0]['repo_token'] = repo_token
|
||||||
|
|
||||||
|
# If there's a public key defined in the plan, upload it.
|
||||||
|
content = plan_definition['artifacts'][0].get('content')
|
||||||
|
if content:
|
||||||
|
public_key = content.get('public_key', '')
|
||||||
|
private_repo = content.get('private', False)
|
||||||
|
if private_repo and public_key:
|
||||||
|
try:
|
||||||
|
gha.add_ssh_key(public_key=public_key)
|
||||||
|
except github.GitHubException as ghe:
|
||||||
|
raise exc.CommandError(message=str(ghe))
|
||||||
|
|
||||||
plan = self.client.plans.create(yamlutils.dump(plan_definition))
|
plan = self.client.plans.create(yamlutils.dump(plan_definition))
|
||||||
fields = ['uuid', 'name', 'description', 'uri', 'artifacts',
|
fields = ['uuid', 'name', 'description', 'uri', 'artifacts',
|
||||||
'trigger_uri']
|
'trigger_uri']
|
||||||
@@ -880,18 +902,16 @@ Available commands:
|
|||||||
self._print_dict(plan, fields, wrap=72)
|
self._print_dict(plan, fields, wrap=72)
|
||||||
self._show_public_keys(artifacts)
|
self._show_public_keys(artifacts)
|
||||||
|
|
||||||
if artifacts and args.setup_trigger:
|
if args.setup_trigger or args.workflow:
|
||||||
gha = github.GitHubAuth(git_url)
|
|
||||||
content = artifacts[0].content
|
|
||||||
public_key = content.get('public_key', '')
|
|
||||||
private_repo = content.get('private', False)
|
|
||||||
gha.add_ssh_key(public_key=public_key, is_private=private_repo)
|
|
||||||
trigger_uri = vars(plan).get('trigger_uri', '')
|
trigger_uri = vars(plan).get('trigger_uri', '')
|
||||||
if trigger_uri:
|
if trigger_uri:
|
||||||
workflow = None
|
workflow = None
|
||||||
if args.workflow:
|
if args.workflow:
|
||||||
workflow = args.workflow.replace('+', ' ').split(' ')
|
workflow = args.workflow.replace('+', ' ').split(' ')
|
||||||
gha.create_webhook(trigger_uri, workflow=workflow)
|
try:
|
||||||
|
gha.create_webhook(trigger_uri, workflow=workflow)
|
||||||
|
except github.GitHubException as ghe:
|
||||||
|
raise exc.CommandError(message=str(ghe))
|
||||||
|
|
||||||
def deploy(self):
|
def deploy(self):
|
||||||
"""Deploy an application, building any applicable artifacts first."""
|
"""Deploy an application, building any applicable artifacts first."""
|
||||||
|
@@ -23,44 +23,57 @@ from solumclient.tests import base
|
|||||||
|
|
||||||
class TestGitHubAuth(base.TestCase):
|
class TestGitHubAuth(base.TestCase):
|
||||||
fake_repo = "http://github.com/fakeuser/fakerepo.git"
|
fake_repo = "http://github.com/fakeuser/fakerepo.git"
|
||||||
|
fake_trigger = "http://example.com/trigger/1"
|
||||||
fake_username = 'fakeuser'
|
fake_username = 'fakeuser'
|
||||||
fake_password = 'fakepassword'
|
fake_password = 'fakepassword'
|
||||||
|
fake_token = 'faketoken'
|
||||||
|
|
||||||
def test_invalid_repo(self):
|
def test_invalid_repo(self):
|
||||||
self.assertRaises(ValueError,
|
self.assertRaises(ValueError,
|
||||||
github.GitHubAuth,
|
github.GitHubAuth,
|
||||||
"http://example.com")
|
"http://example.com")
|
||||||
|
|
||||||
def test_token_fetched_on_request(self):
|
def test_auth_header_username_password(self):
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
gha = github.GitHubAuth(self.fake_repo,
|
||||||
username=self.fake_username,
|
username=self.fake_username,
|
||||||
password=self.fake_password)
|
password=self.fake_password)
|
||||||
gha._get_repo_token = mock.Mock()
|
# base64.b64encode('fakeuser:fakepassword') yields 'ZmFrZX...'
|
||||||
gha.token
|
expected_auth_header = {
|
||||||
gha._get_repo_token.assert_called_once()
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Basic ZmFrZXVzZXI6ZmFrZXBhc3N3b3Jk',
|
||||||
|
}
|
||||||
|
self.assertEqual(expected_auth_header, gha.auth_header)
|
||||||
|
|
||||||
def test_token_fetched_only_once(self):
|
@mock.patch('getpass.getpass')
|
||||||
|
def test_auth_header_username_password_2fa(self, fake_getpass):
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
gha = github.GitHubAuth(self.fake_repo,
|
||||||
username=self.fake_username,
|
username=self.fake_username,
|
||||||
password=self.fake_password)
|
password=self.fake_password)
|
||||||
|
gha._otp_required = True
|
||||||
|
fake_getpass.return_value = 'fakeonetime'
|
||||||
|
expected_auth_header = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Basic ZmFrZXVzZXI6ZmFrZXBhc3N3b3Jk',
|
||||||
|
'x-github-otp': 'fakeonetime',
|
||||||
|
}
|
||||||
|
self.assertEqual(expected_auth_header, gha.auth_header)
|
||||||
|
|
||||||
def update_token(some_gha):
|
def test_auth_header_repo_token(self):
|
||||||
some_gha._token = 'foo'
|
gha = github.GitHubAuth(self.fake_repo,
|
||||||
gha._get_repo_token = mock.Mock(side_effect=update_token(gha))
|
repo_token=self.fake_token)
|
||||||
for i in range(5):
|
expected_auth_header = {
|
||||||
gha.token
|
'Content-Type': 'application/json',
|
||||||
gha._get_repo_token.assert_called_once()
|
'Authorization': 'token %s' % self.fake_token,
|
||||||
self.assertEqual(gha.token, 'foo')
|
}
|
||||||
|
self.assertEqual(expected_auth_header, gha.auth_header)
|
||||||
|
|
||||||
@mock.patch('httplib2.Http.request')
|
@mock.patch('httplib2.Http.request')
|
||||||
def test_create_webhook(self, fake_request):
|
def test_create_webhook(self, fake_request):
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
gha = github.GitHubAuth(self.fake_repo,
|
||||||
username=self.fake_username,
|
repo_token=self.fake_token)
|
||||||
password=self.fake_password)
|
|
||||||
fake_request.return_value = ({'status': '200'},
|
fake_request.return_value = ({'status': '200'},
|
||||||
'{"token": "foo"}')
|
'{"token": "%s"}' % self.fake_token)
|
||||||
fake_trigger_url = 'http://example.com'
|
gha.create_webhook(self.fake_trigger)
|
||||||
gha.create_webhook(fake_trigger_url)
|
|
||||||
fake_request.assert_called_once_with(
|
fake_request.assert_called_once_with(
|
||||||
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
||||||
'POST',
|
'POST',
|
||||||
@@ -68,7 +81,7 @@ class TestGitHubAuth(base.TestCase):
|
|||||||
body=mock.ANY)
|
body=mock.ANY)
|
||||||
expected_body = {
|
expected_body = {
|
||||||
"config": {
|
"config": {
|
||||||
"url": fake_trigger_url,
|
"url": self.fake_trigger,
|
||||||
"content_type": "json"},
|
"content_type": "json"},
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"events": ["pull_request", "commit_comment"]}
|
"events": ["pull_request", "commit_comment"]}
|
||||||
@@ -82,8 +95,7 @@ class TestGitHubAuth(base.TestCase):
|
|||||||
password=self.fake_password)
|
password=self.fake_password)
|
||||||
fake_request.return_value = ({'status': '200'},
|
fake_request.return_value = ({'status': '200'},
|
||||||
'{"token": "foo"}')
|
'{"token": "foo"}')
|
||||||
fake_trigger_url = 'http://example.com'
|
gha.create_webhook(self.fake_trigger, workflow=['unittest'])
|
||||||
gha.create_webhook(fake_trigger_url, workflow=['unittest'])
|
|
||||||
fake_request.assert_called_once_with(
|
fake_request.assert_called_once_with(
|
||||||
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
||||||
'POST',
|
'POST',
|
||||||
@@ -91,7 +103,7 @@ class TestGitHubAuth(base.TestCase):
|
|||||||
body=mock.ANY)
|
body=mock.ANY)
|
||||||
expected_body = {
|
expected_body = {
|
||||||
"config": {
|
"config": {
|
||||||
"url": fake_trigger_url + "?workflow=unittest",
|
"url": self.fake_trigger + "?workflow=unittest",
|
||||||
"content_type": "json"},
|
"content_type": "json"},
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"events": ["pull_request", "commit_comment"]}
|
"events": ["pull_request", "commit_comment"]}
|
||||||
@@ -105,8 +117,7 @@ class TestGitHubAuth(base.TestCase):
|
|||||||
password=self.fake_password)
|
password=self.fake_password)
|
||||||
fake_request.return_value = ({'status': '200'},
|
fake_request.return_value = ({'status': '200'},
|
||||||
'{"token": "foo"}')
|
'{"token": "foo"}')
|
||||||
fake_trigger_url = 'http://example.com'
|
gha.create_webhook(self.fake_trigger, workflow=['unittest', 'build'])
|
||||||
gha.create_webhook(fake_trigger_url, workflow=['unittest', 'build'])
|
|
||||||
fake_request.assert_called_once_with(
|
fake_request.assert_called_once_with(
|
||||||
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
||||||
'POST',
|
'POST',
|
||||||
@@ -114,7 +125,7 @@ class TestGitHubAuth(base.TestCase):
|
|||||||
body=mock.ANY)
|
body=mock.ANY)
|
||||||
expected_body = {
|
expected_body = {
|
||||||
"config": {
|
"config": {
|
||||||
"url": fake_trigger_url + "?workflow=unittest+build",
|
"url": self.fake_trigger + "?workflow=unittest+build",
|
||||||
"content_type": "json"},
|
"content_type": "json"},
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"events": ["pull_request", "commit_comment"]}
|
"events": ["pull_request", "commit_comment"]}
|
||||||
@@ -122,7 +133,7 @@ class TestGitHubAuth(base.TestCase):
|
|||||||
self.assertEqual(expected_body, actual_body)
|
self.assertEqual(expected_body, actual_body)
|
||||||
|
|
||||||
@mock.patch('httplib2.Http.request')
|
@mock.patch('httplib2.Http.request')
|
||||||
def test_add_ssh_key_public(self, fake_request):
|
def test_add_ssh_key(self, fake_request):
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
gha = github.GitHubAuth(self.fake_repo,
|
||||||
username=self.fake_username,
|
username=self.fake_username,
|
||||||
password=self.fake_password)
|
password=self.fake_password)
|
||||||
@@ -130,24 +141,6 @@ class TestGitHubAuth(base.TestCase):
|
|||||||
'{"token": "foo"}')
|
'{"token": "foo"}')
|
||||||
fake_pub_key = 'foo'
|
fake_pub_key = 'foo'
|
||||||
gha.add_ssh_key(public_key=fake_pub_key)
|
gha.add_ssh_key(public_key=fake_pub_key)
|
||||||
fake_request.assert_called_once_with(
|
|
||||||
'https://api.github.com/repos/fakeuser/fakerepo/keys',
|
|
||||||
'POST',
|
|
||||||
headers=mock.ANY,
|
|
||||||
body=mock.ANY)
|
|
||||||
expected_body = {"key": "foo", "title": "devops@Solum"}
|
|
||||||
actual_body = json.loads(fake_request.call_args[1]['body'])
|
|
||||||
self.assertEqual(expected_body, actual_body)
|
|
||||||
|
|
||||||
@mock.patch('httplib2.Http.request')
|
|
||||||
def test_add_ssh_key_private(self, fake_request):
|
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
|
||||||
username=self.fake_username,
|
|
||||||
password=self.fake_password)
|
|
||||||
fake_request.return_value = ({'status': '200'},
|
|
||||||
'{"token": "foo"}')
|
|
||||||
fake_pub_key = 'foo'
|
|
||||||
gha.add_ssh_key(public_key=fake_pub_key, is_private=True)
|
|
||||||
fake_request.assert_called_once_with(
|
fake_request.assert_called_once_with(
|
||||||
'https://api.github.com/user/keys',
|
'https://api.github.com/user/keys',
|
||||||
'POST',
|
'POST',
|
||||||
|
Reference in New Issue
Block a user