Skip to content

Commit 75a929e

Browse files
authored
Merge pull request #144 from github/test-coverage
chore: Extract functions and add test coverage
2 parents a716356 + 9316709 commit 75a929e

File tree

3 files changed

+265
-30
lines changed

3 files changed

+265
-30
lines changed

open_contrib_pr.py

+89-30
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,31 @@
99
import env
1010
import github3
1111

12-
if __name__ == "__main__":
12+
13+
def get_repos_json(gh_actor, repos_json_location, token, endpoint):
14+
"""
15+
Get the list of repositories from the JSON file.
16+
17+
Args:
18+
gh_actor (str): The GitHub actor (username).
19+
repos_json_location (str): The location of the JSON file containing the repositories.
20+
token (str): The GitHub personal access token.
21+
endpoint (str): The GitHub endpoint.
22+
23+
Returns:
24+
dict: A dictionary containing the list of repositories.
25+
"""
26+
os.system(f"git clone https://{gh_actor}:{token}@{endpoint}/{repos_json_location}")
27+
with open(str(repos_json_location), "r", encoding="utf-8") as repos_file:
28+
innersource_repos = json.loads(repos_file.read())
29+
return innersource_repos
30+
31+
32+
def main(): # pragma: no cover
33+
"""
34+
Automatically open a pull request for repositories that have no CONTRIBUTING.md
35+
file from a list of repositories in a JSON file
36+
"""
1337
env_vars = env.get_env_vars()
1438
gh_actor = env_vars.gh_actor
1539
organization = env_vars.organization
@@ -44,9 +68,7 @@
4468
os.system(f"git config --global user.email 'no-reply@{endpoint}'")
4569

4670
# Get innersource repos from organization
47-
os.system(f"git clone https://{gh_actor}:{token}@{endpoint}/{repos_json_location}")
48-
with open(str(repos_json_location), "r", encoding="utf-8") as repos_file:
49-
innersource_repos = json.loads(repos_file.read())
71+
innersource_repos = get_repos_json(gh_actor, repos_json_location, token, endpoint)
5072

5173
for repo in innersource_repos:
5274
print(repo["name"])
@@ -56,15 +78,15 @@
5678
continue
5779
except KeyError:
5880
# clone the repo
59-
repo_full_name = repo["full_name"]
60-
repo_name = repo["name"]
61-
os.system(
62-
f"git clone https://{gh_actor}:{token}@{endpoint}/{repo_full_name}"
63-
)
81+
repo_name = clone_repository(gh_actor, token, endpoint, repo)
82+
if not repo_name:
83+
continue
84+
6485
# checkout a branch called contributing-doc
65-
BRANCH_NAME = "contributing-doc"
86+
branch_name = "contributing-doc"
6687
os.chdir(f"{repo_name}")
67-
os.system(f"git checkout -b {BRANCH_NAME}")
88+
os.system(f"git checkout -b {branch_name}")
89+
6890
# copy, customize, and git add the template file
6991
os.system("cp /action/workspace/CONTRIBUTING-template.md CONTRIBUTING.md")
7092
os.system(f"sed -i 's/Project-Name/{repo_name}/g' CONTRIBUTING.md")
@@ -74,32 +96,69 @@
7496
"git commit -m'Request to add a document outlining how to contribute'"
7597
)
7698
# git push the branch
77-
os.system(f"git push -u origin {BRANCH_NAME}")
99+
os.system(f"git push -u origin {branch_name}")
78100
# open a PR from that branch to the default branch
79101
default_branch = repo["default_branch"]
80102
# create the pull request
81-
repository_object = github_connection.repository(organization, repo_name)
82-
try:
83-
repository_object.create_pull(
84-
title=pr_title,
85-
body=pr_body,
86-
head=BRANCH_NAME,
87-
base=default_branch,
88-
)
89-
except github3.exceptions.UnprocessableEntity:
90-
print("Pull request already exists")
91-
except github3.exceptions.ForbiddenError:
92-
print("Pull request failed")
93-
except github3.exceptions.NotFoundError:
94-
print("Pull request failed")
95-
except github3.exceptions.ConnectionError:
96-
print("Pull request failed")
97-
except Exception as e: # pylint: disable=broad-exception-caught
98-
print(e)
103+
create_pull_request(
104+
organization,
105+
pr_body,
106+
pr_title,
107+
github_connection,
108+
repo_name,
109+
branch_name,
110+
default_branch,
111+
)
99112
# Clean up repository dir
100113
os.chdir("../")
101114
os.system(f"rm -rf {repo_name}")
102115

103116
# rate limit to 20 repos per hour
104117
print("Waiting 3 minutes so as not to exceed API limits")
105118
sleep(180)
119+
120+
121+
def create_pull_request(
122+
organization,
123+
pr_body,
124+
pr_title,
125+
github_connection,
126+
repo_name,
127+
branch_name,
128+
default_branch,
129+
):
130+
"""Create a pull request."""
131+
repository_object = github_connection.repository(organization, repo_name)
132+
try:
133+
repository_object.create_pull(
134+
title=pr_title,
135+
body=pr_body,
136+
head=branch_name,
137+
base=default_branch,
138+
)
139+
except github3.exceptions.UnprocessableEntity:
140+
print("Pull request already exists")
141+
except github3.exceptions.ForbiddenError:
142+
print("Pull request failed")
143+
except github3.exceptions.NotFoundError:
144+
print("Pull request failed")
145+
except github3.exceptions.ConnectionError:
146+
print("Pull request failed")
147+
except Exception as e: # pylint: disable=broad-exception-caught
148+
print(e)
149+
150+
151+
def clone_repository(gh_actor, token, endpoint, repo):
152+
"""Clone the repository and return the name of the repository."""
153+
repo_full_name = repo["full_name"]
154+
repo_name = repo["name"]
155+
try:
156+
os.system(f"git clone https://{gh_actor}:{token}@{endpoint}/{repo_full_name}")
157+
except OSError as e:
158+
print(f"Failed to clone repository: {e}")
159+
return None
160+
return repo_name
161+
162+
163+
if __name__ == "__main__":
164+
main() # pragma: no cover

test_auth.py

+37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from unittest.mock import MagicMock, patch
55

66
import auth
7+
import requests
78

89

910
class TestAuth(unittest.TestCase):
@@ -91,6 +92,42 @@ def test_get_github_app_installation_token(self, mock_post):
9192

9293
self.assertEqual(result, dummy_token)
9394

95+
@patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token"))
96+
@patch("auth.requests.post")
97+
def test_get_github_app_installation_token_request_failure(self, mock_post):
98+
"""
99+
Test the get_github_app_installation_token function returns None when the request fails.
100+
"""
101+
# Mock the post request to raise a RequestException
102+
mock_post.side_effect = requests.exceptions.RequestException("Request failed")
103+
104+
# Call the function with test data
105+
result = auth.get_github_app_installation_token(
106+
ghe="https://api.github.com",
107+
gh_app_id=12345,
108+
gh_app_private_key_bytes=b"private_key",
109+
gh_app_installation_id=678910,
110+
)
111+
112+
# Assert that the result is None
113+
self.assertIsNone(result)
114+
115+
@patch("github3.login")
116+
def test_auth_to_github_invalid_credentials(self, mock_login):
117+
"""
118+
Test the auth_to_github function raises correct ValueError
119+
when credentials are present but incorrect.
120+
"""
121+
mock_login.return_value = None
122+
with self.assertRaises(ValueError) as context_manager:
123+
auth.auth_to_github("not_a_valid_token", "", "", b"", "", False)
124+
125+
the_exception = context_manager.exception
126+
self.assertEqual(
127+
str(the_exception),
128+
"Unable to authenticate to GitHub",
129+
)
130+
94131

95132
if __name__ == "__main__":
96133
unittest.main()

test_open_contrib_pr.py

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
""" Tests for the open_contrib_pr.py functions. """
2+
3+
import unittest
4+
from unittest.mock import MagicMock, mock_open, patch
5+
6+
import github3
7+
from open_contrib_pr import clone_repository, create_pull_request, get_repos_json
8+
9+
10+
class TestOpenContribPR(unittest.TestCase):
11+
"""Test case for the open_contrib_pr module."""
12+
13+
@patch(
14+
"builtins.open",
15+
new_callable=mock_open,
16+
read_data='{"repos": ["repo1", "repo2"]}',
17+
)
18+
@patch("os.system")
19+
def test_get_repos_json(self, mock_system, mock_file):
20+
"""
21+
Test the get_repos_json function.
22+
"""
23+
gh_actor = "test_actor"
24+
repos_json_location = "test_location"
25+
token = "test_token"
26+
endpoint = "test_endpoint"
27+
28+
expected_repos = {"repos": ["repo1", "repo2"]}
29+
30+
result = get_repos_json(gh_actor, repos_json_location, token, endpoint)
31+
32+
mock_system.assert_called_once_with(
33+
f"git clone https://{gh_actor}:{token}@{endpoint}/{repos_json_location}"
34+
)
35+
mock_file.assert_called_once_with(
36+
str(repos_json_location), "r", encoding="utf-8"
37+
)
38+
self.assertEqual(result, expected_repos)
39+
40+
41+
class TestCloneRepository(unittest.TestCase):
42+
"""Test case for the clone_repository function."""
43+
44+
@patch("os.system")
45+
def test_clone_repository_success(self, mock_system):
46+
"""
47+
Test the clone_repository function when the clone is successful.
48+
"""
49+
mock_system.return_value = 0 # Simulate successful clone
50+
51+
result = clone_repository(
52+
gh_actor="test_actor",
53+
token="test_token",
54+
endpoint="test_endpoint",
55+
repo={"full_name": "test_actor/test_repo", "name": "test_repo"},
56+
)
57+
58+
mock_system.assert_called_once_with(
59+
"git clone https://test_actor:test_token@test_endpoint/test_actor/test_repo"
60+
)
61+
self.assertEqual(result, "test_repo")
62+
63+
@patch("os.system")
64+
def test_clone_repository_failure(self, mock_system):
65+
"""
66+
Test the clone_repository function when the clone fails.
67+
"""
68+
mock_system.side_effect = OSError("Clone failed") # Simulate clone failure
69+
70+
result = clone_repository(
71+
gh_actor="test_actor",
72+
token="test_token",
73+
endpoint="test_endpoint",
74+
repo={"full_name": "test_actor/test_repo", "name": "test_repo"},
75+
)
76+
77+
mock_system.assert_called_once_with(
78+
"git clone https://test_actor:test_token@test_endpoint/test_actor/test_repo"
79+
)
80+
self.assertIsNone(result)
81+
82+
83+
class TestCreatePullRequest(unittest.TestCase):
84+
"""Test case for the create_pull_request function."""
85+
86+
def test_create_pull_request_success(self):
87+
"""
88+
Test the create_pull_request function when the pull request is created successfully.
89+
"""
90+
github_connection = MagicMock()
91+
github_connection.repository.return_value = MagicMock()
92+
93+
create_pull_request(
94+
organization="test_org",
95+
pr_body="Test PR body",
96+
pr_title="Test PR title",
97+
github_connection=github_connection,
98+
repo_name="test_repo",
99+
branch_name="test_branch",
100+
default_branch="main",
101+
)
102+
103+
github_connection.repository.return_value.create_pull.assert_called_once_with(
104+
title="Test PR title", body="Test PR body", head="test_branch", base="main"
105+
)
106+
107+
def test_create_pull_exceptions(self):
108+
"""
109+
Test the create_pull_request function when an exception occurs.
110+
"""
111+
github_connection = MagicMock()
112+
github_connection.repository.return_value = MagicMock()
113+
for exception, message in [
114+
(
115+
github3.exceptions.UnprocessableEntity(MagicMock()),
116+
"Pull request already exists",
117+
),
118+
(github3.exceptions.ForbiddenError(MagicMock()), "Pull request failed"),
119+
(github3.exceptions.NotFoundError(MagicMock()), "Pull request failed"),
120+
(github3.exceptions.ConnectionError(MagicMock()), "Pull request failed"),
121+
]:
122+
github_connection.repository.return_value.create_pull.side_effect = (
123+
exception
124+
)
125+
with patch("builtins.print") as mock_print:
126+
create_pull_request(
127+
organization="test_org",
128+
pr_body="Test PR body",
129+
pr_title="Test PR title",
130+
github_connection=github_connection,
131+
repo_name="test_repo",
132+
branch_name="test_branch",
133+
default_branch="main",
134+
)
135+
mock_print.assert_called_once_with(message)
136+
137+
138+
if __name__ == "__main__":
139+
unittest.main()

0 commit comments

Comments
 (0)