diff --git a/test-requirements.txt b/test-requirements.txt index f7ee86ed41..6ea23dc281 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,3 +6,4 @@ PyMySQL psycopg2-binary beautifulsoup4 graphene +requests_mock diff --git a/tests/base.py b/tests/base.py index f721e01555..fd4ca25858 100644 --- a/tests/base.py +++ b/tests/base.py @@ -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'): + # ('org', 'project2'): , + # ('org2', 'project1'): , ... } + # + # 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): diff --git a/tests/fakegithub.py b/tests/fakegithub.py index 7406fe7dfb..d5252bd445 100644 --- a/tests/fakegithub.py +++ b/tests/fakegithub.py @@ -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):