|
1 | 1 | import unittest |
2 | | -from unittest.mock import patch, MagicMock |
| 2 | +from unittest.mock import patch, MagicMock, call |
3 | 3 | import os |
4 | 4 | import sys |
| 5 | +import argparse |
| 6 | +import git |
5 | 7 |
|
6 | 8 | # Add the root directory to the Python path |
7 | 9 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) |
8 | 10 |
|
9 | | -from github_clone import get_starred_repos, clone_repo |
| 11 | +from github_clone import get_paged_data, get_starred_repos, get_org_repos, clone_repo, main |
10 | 12 |
|
11 | 13 | class TestGitHubClone(unittest.TestCase): |
12 | 14 |
|
13 | 15 | @patch('github_clone.requests.get') |
14 | | - def test_get_starred_repos_success(self, mock_get): |
15 | | - # Mock the API response |
| 16 | + def test_get_paged_data_single_page(self, mock_get): |
| 17 | + # Mock the API response for a single page |
16 | 18 | mock_response = MagicMock() |
17 | 19 | mock_response.json.return_value = [{'name': 'repo1'}, {'name': 'repo2'}] |
18 | 20 | mock_response.raise_for_status = MagicMock() |
| 21 | + mock_response.links = {} |
| 22 | + mock_response.headers = {'X-RateLimit-Remaining': '5000', 'X-RateLimit-Reset': '1678886400'} |
19 | 23 | mock_get.return_value = mock_response |
20 | 24 |
|
21 | | - repos = get_starred_repos('testuser', 'testtoken') |
| 25 | + repos = get_paged_data('http://example.com/repos', {'Authorization': 'token test'}) |
22 | 26 | self.assertEqual(len(repos), 2) |
23 | | - self.assertEqual(repos[0]['name'], 'repo1') |
24 | | - mock_get.assert_called_with('https://api.github.com/users/testuser/starred', auth=('testuser', 'testtoken')) |
| 27 | + mock_get.assert_called_once_with('http://example.com/repos', headers={'Authorization': 'token test'}) |
25 | 28 |
|
26 | 29 | @patch('github_clone.requests.get') |
27 | | - def test_get_starred_repos_failure(self, mock_get): |
28 | | - # Mock a failed API response |
29 | | - mock_response = MagicMock() |
30 | | - mock_response.raise_for_status.side_effect = Exception("API Error") |
31 | | - mock_get.return_value = mock_response |
| 30 | + def test_get_paged_data_multiple_pages(self, mock_get): |
| 31 | + # Mock the API response for multiple pages |
| 32 | + mock_response1 = MagicMock() |
| 33 | + mock_response1.json.return_value = [{'name': 'repo1'}] |
| 34 | + mock_response1.raise_for_status = MagicMock() |
| 35 | + mock_response1.links = {'next': {'url': 'http://example.com/repos?page=2'}} |
| 36 | + mock_response1.headers = {'X-RateLimit-Remaining': '5000', 'X-RateLimit-Reset': '1678886400'} |
| 37 | + |
| 38 | + mock_response2 = MagicMock() |
| 39 | + mock_response2.json.return_value = [{'name': 'repo2'}] |
| 40 | + mock_response2.raise_for_status = MagicMock() |
| 41 | + mock_response2.links = {} |
| 42 | + mock_response2.headers = {'X-RateLimit-Remaining': '4999', 'X-RateLimit-Reset': '1678886400'} |
| 43 | + |
| 44 | + mock_get.side_effect = [mock_response1, mock_response2] |
| 45 | + |
| 46 | + repos = get_paged_data('http://example.com/repos', {'Authorization': 'token test'}) |
| 47 | + self.assertEqual(len(repos), 2) |
| 48 | + self.assertEqual(mock_get.call_count, 2) |
| 49 | + mock_get.assert_has_calls([ |
| 50 | + call('http://example.com/repos', headers={'Authorization': 'token test'}), |
| 51 | + call('http://example.com/repos?page=2', headers={'Authorization': 'token test'}) |
| 52 | + ]) |
32 | 53 |
|
33 | | - with self.assertRaises(Exception): |
34 | | - get_starred_repos('testuser', 'testtoken') |
| 54 | + @patch('github_clone.time.sleep') |
| 55 | + @patch('github_clone.requests.get') |
| 56 | + def test_get_paged_data_rate_limiting(self, mock_get, mock_sleep): |
| 57 | + # Mock the API response for rate limiting |
| 58 | + # The time.time() call will be mocked to a fixed value. |
| 59 | + current_time = 1678886390.0 |
| 60 | + reset_time = current_time + 10 |
| 61 | + |
| 62 | + mock_response1 = MagicMock() |
| 63 | + mock_response1.json.return_value = [{'name': 'repo1'}] |
| 64 | + mock_response1.raise_for_status = MagicMock() |
| 65 | + mock_response1.links = {'next': {'url': 'http://example.com/repos?page=2'}} |
| 66 | + mock_response1.headers = {'X-RateLimit-Remaining': '0', 'X-RateLimit-Reset': str(int(reset_time))} |
| 67 | + |
| 68 | + mock_response2 = MagicMock() |
| 69 | + mock_response2.json.return_value = [{'name': 'repo2'}] |
| 70 | + mock_response2.raise_for_status = MagicMock() |
| 71 | + mock_response2.links = {} |
| 72 | + mock_response2.headers = {'X-RateLimit-Remaining': '5000', 'X-RateLimit-Reset': str(int(reset_time) + 3600)} |
| 73 | + |
| 74 | + mock_get.side_effect = [mock_response1, mock_response2] |
| 75 | + |
| 76 | + with patch('time.time', return_value=current_time): |
| 77 | + get_paged_data('http://example.com/repos', {'Authorization': 'token test'}) |
| 78 | + |
| 79 | + mock_sleep.assert_called_once() |
| 80 | + # The sleep duration should be slightly more than 10 seconds |
| 81 | + self.assertAlmostEqual(mock_sleep.call_args[0][0], 10, delta=1) |
| 82 | + |
| 83 | + @patch('github_clone.get_paged_data') |
| 84 | + def test_get_starred_repos(self, mock_get_paged_data): |
| 85 | + get_starred_repos('testuser', 'testtoken') |
| 86 | + mock_get_paged_data.assert_called_with( |
| 87 | + 'https://api.github.com/users/testuser/starred', |
| 88 | + {'Authorization': 'token testtoken'} |
| 89 | + ) |
| 90 | + |
| 91 | + @patch('github_clone.get_paged_data') |
| 92 | + def test_get_org_repos(self, mock_get_paged_data): |
| 93 | + get_org_repos('testorg', 'testtoken') |
| 94 | + mock_get_paged_data.assert_called_with( |
| 95 | + 'https://api.github.com/orgs/testorg/repos', |
| 96 | + {'Authorization': 'token testtoken'} |
| 97 | + ) |
35 | 98 |
|
36 | 99 | @patch('github_clone.git.Repo.clone_from') |
37 | 100 | @patch('github_clone.os.path.exists') |
38 | 101 | def test_clone_repo_new(self, mock_exists, mock_clone_from): |
39 | | - # Mock that the repo does not exist |
40 | 102 | mock_exists.return_value = False |
41 | | - |
42 | 103 | repo_info = {'name': 'new_repo', 'clone_url': 'http://example.com/new_repo.git'} |
43 | | - clone_dir = 'test_dir' |
44 | | - |
45 | | - clone_repo(repo_info, clone_dir) |
46 | | - |
47 | | - repo_dir = os.path.join(clone_dir, repo_info['name']) |
48 | | - mock_exists.assert_called_with(repo_dir) |
49 | | - mock_clone_from.assert_called_with(repo_info['clone_url'], repo_dir) |
| 104 | + clone_repo(repo_info, 'test_dir') |
| 105 | + mock_clone_from.assert_called_with('http://example.com/new_repo.git', os.path.join('test_dir', 'new_repo')) |
50 | 106 |
|
51 | 107 | @patch('github_clone.git.Repo.clone_from') |
52 | 108 | @patch('github_clone.os.path.exists') |
53 | 109 | def test_clone_repo_exists(self, mock_exists, mock_clone_from): |
54 | | - # Mock that the repo already exists |
55 | 110 | mock_exists.return_value = True |
56 | | - |
57 | 111 | repo_info = {'name': 'existing_repo', 'clone_url': 'http://example.com/existing_repo.git'} |
58 | | - clone_dir = 'test_dir' |
59 | | - |
60 | | - clone_repo(repo_info, clone_dir) |
61 | | - |
62 | | - repo_dir = os.path.join(clone_dir, repo_info['name']) |
63 | | - mock_exists.assert_called_with(repo_dir) |
| 112 | + clone_repo(repo_info, 'test_dir') |
64 | 113 | mock_clone_from.assert_not_called() |
65 | 114 |
|
| 115 | + @patch('github_clone.git.Repo.clone_from') |
| 116 | + @patch('github_clone.os.path.exists') |
| 117 | + def test_clone_repo_error(self, mock_exists, mock_clone_from): |
| 118 | + mock_exists.return_value = False |
| 119 | + mock_clone_from.side_effect = git.exc.GitCommandError('clone', 'error') |
| 120 | + repo_info = {'name': 'error_repo', 'clone_url': 'http://example.com/error_repo.git'} |
| 121 | + with patch('builtins.print') as mock_print: |
| 122 | + clone_repo(repo_info, 'test_dir') |
| 123 | + self.assertIn("Error cloning error_repo", mock_print.call_args_list[1][0][0]) |
| 124 | + |
| 125 | + @patch('github_clone.argparse.ArgumentParser.parse_args') |
| 126 | + @patch('github_clone.get_starred_repos') |
| 127 | + @patch('github_clone.clone_repo') |
| 128 | + @patch('github_clone.os.makedirs') |
| 129 | + @patch('github_clone.os.path.exists', return_value=False) |
| 130 | + def test_main_starred(self, mock_exists, mock_makedirs, mock_clone_repo, mock_get_starred_repos, mock_parse_args): |
| 131 | + mock_parse_args.return_value = argparse.Namespace( |
| 132 | + token='testtoken', |
| 133 | + starred='testuser', |
| 134 | + org=None, |
| 135 | + clone_dir='test_clone_dir' |
| 136 | + ) |
| 137 | + mock_get_starred_repos.return_value = [{'name': 'repo1'}, {'name': 'repo2'}] |
| 138 | + |
| 139 | + main() |
| 140 | + |
| 141 | + mock_get_starred_repos.assert_called_with('testuser', 'testtoken') |
| 142 | + self.assertEqual(mock_clone_repo.call_count, 2) |
| 143 | + mock_makedirs.assert_called_with('test_clone_dir') |
| 144 | + |
| 145 | + @patch('github_clone.argparse.ArgumentParser.parse_args') |
| 146 | + @patch('github_clone.get_org_repos') |
| 147 | + @patch('github_clone.clone_repo') |
| 148 | + @patch('github_clone.os.makedirs') |
| 149 | + @patch('github_clone.os.path.exists', return_value=False) |
| 150 | + def test_main_org(self, mock_exists, mock_makedirs, mock_clone_repo, mock_get_org_repos, mock_parse_args): |
| 151 | + mock_parse_args.return_value = argparse.Namespace( |
| 152 | + token='testtoken', |
| 153 | + starred=None, |
| 154 | + org='testorg', |
| 155 | + clone_dir='test_clone_dir' |
| 156 | + ) |
| 157 | + mock_get_org_repos.return_value = [{'name': 'repo1'}, {'name': 'repo2'}] |
| 158 | + |
| 159 | + main() |
| 160 | + |
| 161 | + mock_get_org_repos.assert_called_with('testorg', 'testtoken') |
| 162 | + self.assertEqual(mock_clone_repo.call_count, 2) |
| 163 | + mock_makedirs.assert_called_with('test_clone_dir') |
| 164 | + |
66 | 165 | if __name__ == '__main__': |
67 | 166 | unittest.main() |
0 commit comments