github: more complete mocking for app setup

This mocks the /app/installations and /installations/repositories
GitHub API calls to better validate the GitHub project initalization
in the driver.

It implements enough that we can use
GithubClientManager:_prime_installation_map() directly and better
tests the token issuing, etc.

Change-Id: I608f1540ef33b1a95595393e546afba308fef66a
This commit is contained in:
Ian Wienand 2021-09-23 16:44:36 +10:00
parent b3eb94e3b2
commit 403d1c2882
3 changed files with 96 additions and 11 deletions

View File

@ -6,3 +6,4 @@ PyMySQL
psycopg2-binary
beautifulsoup4
graphene
requests_mock

View File

@ -67,6 +67,7 @@ import yaml
import paramiko
import prometheus_client.exposition
import sqlalchemy
import requests_mock
from zuul.driver.sql.sqlconnection import DatabaseSession
from zuul import model
@ -2742,6 +2743,8 @@ class FakeGithubClientManager(GithubClientManager):
github_class = tests.fakegithub.FakeGithubClient
github_enterprise_class = tests.fakegithub.FakeGithubEnterpriseClient
log = logging.getLogger("zuul.test.FakeGithubClientManager")
def __init__(self, connection_config):
super().__init__(connection_config)
self.record_clients = False
@ -2769,19 +2772,93 @@ class FakeGithubClientManager(GithubClientManager):
return client
def _prime_installation_map(self):
# Only valid if installed as a github app
if not self.app_id:
return
# simulate one installation per org
orgs = {}
latest_inst_id = 0
for repo in self.github_data.repos:
inst_id = orgs.get(repo[0])
if not inst_id:
latest_inst_id += 1
inst_id = latest_inst_id
orgs[repo[0]] = inst_id
self.installation_map['/'.join(repo)] = inst_id
# github_data.repos is a hash like
# { ('org', 'project1'): <dataobj>
# ('org', 'project2'): <dataobj>,
# ('org2', 'project1'): <dataobj>, ... }
#
# we don't care about the value. index by org, e.g.
#
# {
# 'org': ('project1', 'project2')
# 'org2': ('project1', 'project2')
# }
orgs = defaultdict(list)
project_id = 1
for org, project in self.github_data.repos:
# Each entry is in the format for "repositories" response
# of GET /installation/repositories
orgs[org].append({
'id': project_id,
'name': project,
'full_name': '%s/%s' % (org, project)
# note, lots of other stuff that's not relevant
})
project_id += 1
self.log.debug("GitHub installation mapped to: %s" % orgs)
# Mock response to GET /app/installations
app_json = []
app_projects = []
app_id = 1
for org, projects in orgs.items():
# We respond as if each org is a different app instance
#
# Below we will be sent the app_id in a token to query
# what projects this app exports. Keep the projects in a
# sequential list so we can just look up "projects for app
# X" == app_projects[X]
app_projects.append(projects)
app_json.append(
{
'id': app_id,
# Acutally none of this matters, and there's lots
# more in a real response. Padded out just for
# example sake.
'account': {
'login': org,
'id': 1234,
'type': 'User',
},
'permissions': {
'checks': 'write',
'metadata': 'read',
'contents': 'read'
},
'events': ['push',
'pull_request'
],
'suspended_at': None,
'suspended_by': None,
}
)
app_id += 1
# TODO(ianw) : we could exercise the pagination paths ...
with requests_mock.Mocker() as m:
m.get('%s/app/installations' % self.base_url, json=app_json)
def repositories_callback(request, context):
# FakeGithubSession gives us an auth token "token
# token-X" where "X" corresponds to the app id we want
# the projects for. apps start at id "1", so the projects
# to return for this call are app_projects[token-1]
token = int(request.headers['Authorization'][12:])
projects = app_projects[token - 1]
return {
'total_count': len(projects),
'repositories': projects
}
m.get('%s/installation/repositories?per_page=100' % self.base_url,
json=repositories_callback)
# everything mocked now, call real implementation
super()._prime_installation_map()
class FakeGithubConnection(githubconnection.GithubConnection):

View File

@ -641,8 +641,9 @@ class FakeGithubSession(object):
if re.match(r'.*/app/installations/.*/access_tokens', url):
expiry = (datetime.datetime.now(utc) + datetime.timedelta(
minutes=60)).replace(microsecond=0).isoformat()
install_id = url.split('/')[-2]
data = {
'token': 'fake',
'token': 'token-%s' % install_id,
'expires_at': expiry,
}
return FakeResponse(data, 201)
@ -767,6 +768,12 @@ class FakeGithubData(object):
self.reports = []
self.fail_check_run_creation = False
def __repr__(self):
return ("pull_requests:%s repos:%s reports:%s "
"fail_check_run_creation:%s" % (
self.pull_requests, self.repos, self.reports,
self.fail_check_run_creation))
class FakeGithubClient(object):