Skip to content

Commit ab0eebb

Browse files
committed
Refactor test fixtures to use shared create_args helper
Uses the real parse_args() function to get CLI defaults, so when new arguments are added they're automatically available to all tests. Changes: - Add tests/conftest.py with create_args fixture - Update 8 test files to use shared fixture - Remove duplicate _create_mock_args methods - Remove redundant @pytest.fixture mock_args definitions This eliminates the need to update multiple test files when adding new CLI arguments.
1 parent fce4abb commit ab0eebb

File tree

9 files changed

+158
-355
lines changed

9 files changed

+158
-355
lines changed

tests/conftest.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Shared pytest fixtures for github-backup tests."""
2+
3+
import pytest
4+
5+
from github_backup.github_backup import parse_args
6+
7+
8+
@pytest.fixture
9+
def create_args():
10+
"""Factory fixture that creates args with real CLI defaults.
11+
12+
Uses the actual argument parser so new CLI args are automatically
13+
available with their defaults - no test updates needed.
14+
15+
Usage:
16+
def test_something(self, create_args):
17+
args = create_args(include_releases=True, user="myuser")
18+
"""
19+
def _create(**overrides):
20+
# Use real parser to get actual defaults
21+
args = parse_args(["testuser"])
22+
for key, value in overrides.items():
23+
setattr(args, key, value)
24+
return args
25+
return _create

tests/test_all_starred.py

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Tests for --all-starred flag behavior (issue #225)."""
22

33
import pytest
4-
from unittest.mock import Mock, patch
4+
from unittest.mock import patch
55

66
from github_backup import github_backup
77

@@ -12,58 +12,14 @@ class TestAllStarredCloning:
1212
Issue #225: --all-starred should clone starred repos without requiring --repositories.
1313
"""
1414

15-
def _create_mock_args(self, **overrides):
16-
"""Create a mock args object with sensible defaults."""
17-
args = Mock()
18-
args.user = "testuser"
19-
args.output_directory = "/tmp/backup"
20-
args.include_repository = False
21-
args.include_everything = False
22-
args.include_gists = False
23-
args.include_starred_gists = False
24-
args.all_starred = False
25-
args.skip_existing = False
26-
args.bare_clone = False
27-
args.lfs_clone = False
28-
args.no_prune = False
29-
args.include_wiki = False
30-
args.include_issues = False
31-
args.include_issue_comments = False
32-
args.include_issue_events = False
33-
args.include_pulls = False
34-
args.include_pull_comments = False
35-
args.include_pull_commits = False
36-
args.include_pull_details = False
37-
args.include_labels = False
38-
args.include_hooks = False
39-
args.include_milestones = False
40-
args.include_security_advisories = False
41-
args.include_releases = False
42-
args.include_assets = False
43-
args.include_attachments = False
44-
args.incremental = False
45-
args.incremental_by_files = False
46-
args.github_host = None
47-
args.prefer_ssh = False
48-
args.token_classic = None
49-
args.token_fine = None
50-
args.as_app = False
51-
args.osx_keychain_item_name = None
52-
args.osx_keychain_item_account = None
53-
54-
for key, value in overrides.items():
55-
setattr(args, key, value)
56-
57-
return args
58-
5915
@patch('github_backup.github_backup.fetch_repository')
6016
@patch('github_backup.github_backup.get_github_repo_url')
61-
def test_all_starred_clones_without_repositories_flag(self, mock_get_url, mock_fetch):
17+
def test_all_starred_clones_without_repositories_flag(self, mock_get_url, mock_fetch, create_args):
6218
"""--all-starred should clone starred repos without --repositories flag.
6319
6420
This is the core fix for issue #225.
6521
"""
66-
args = self._create_mock_args(all_starred=True)
22+
args = create_args(all_starred=True)
6723
mock_get_url.return_value = "https://github.com/otheruser/awesome-project.git"
6824

6925
# A starred repository (is_starred flag set by retrieve_repositories)
@@ -88,9 +44,9 @@ def test_all_starred_clones_without_repositories_flag(self, mock_get_url, mock_f
8844

8945
@patch('github_backup.github_backup.fetch_repository')
9046
@patch('github_backup.github_backup.get_github_repo_url')
91-
def test_starred_repo_not_cloned_without_all_starred_flag(self, mock_get_url, mock_fetch):
47+
def test_starred_repo_not_cloned_without_all_starred_flag(self, mock_get_url, mock_fetch, create_args):
9248
"""Starred repos should NOT be cloned if --all-starred is not set."""
93-
args = self._create_mock_args(all_starred=False)
49+
args = create_args(all_starred=False)
9450
mock_get_url.return_value = "https://github.com/otheruser/awesome-project.git"
9551

9652
starred_repo = {
@@ -111,9 +67,9 @@ def test_starred_repo_not_cloned_without_all_starred_flag(self, mock_get_url, mo
11167

11268
@patch('github_backup.github_backup.fetch_repository')
11369
@patch('github_backup.github_backup.get_github_repo_url')
114-
def test_non_starred_repo_not_cloned_with_only_all_starred(self, mock_get_url, mock_fetch):
70+
def test_non_starred_repo_not_cloned_with_only_all_starred(self, mock_get_url, mock_fetch, create_args):
11571
"""Non-starred repos should NOT be cloned when only --all-starred is set."""
116-
args = self._create_mock_args(all_starred=True)
72+
args = create_args(all_starred=True)
11773
mock_get_url.return_value = "https://github.com/testuser/my-project.git"
11874

11975
# A regular (non-starred) repository
@@ -135,9 +91,9 @@ def test_non_starred_repo_not_cloned_with_only_all_starred(self, mock_get_url, m
13591

13692
@patch('github_backup.github_backup.fetch_repository')
13793
@patch('github_backup.github_backup.get_github_repo_url')
138-
def test_repositories_flag_still_works(self, mock_get_url, mock_fetch):
94+
def test_repositories_flag_still_works(self, mock_get_url, mock_fetch, create_args):
13995
"""--repositories flag should still clone repos as before."""
140-
args = self._create_mock_args(include_repository=True)
96+
args = create_args(include_repository=True)
14197
mock_get_url.return_value = "https://github.com/testuser/my-project.git"
14298

14399
regular_repo = {

tests/test_attachments.py

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,21 @@
44
import os
55
import tempfile
66
from pathlib import Path
7-
from unittest.mock import Mock
7+
from unittest.mock import Mock, patch
88

99
import pytest
1010

1111
from github_backup import github_backup
1212

1313

1414
@pytest.fixture
15-
def attachment_test_setup(tmp_path):
15+
def attachment_test_setup(tmp_path, create_args):
1616
"""Fixture providing setup and helper for attachment download tests."""
17-
from unittest.mock import patch
18-
1917
issue_cwd = tmp_path / "issues"
2018
issue_cwd.mkdir()
2119

22-
# Mock args
23-
args = Mock()
24-
args.as_app = False
25-
args.token_fine = None
26-
args.token_classic = None
27-
args.osx_keychain_item_name = None
28-
args.osx_keychain_item_account = None
29-
args.user = "testuser"
30-
args.repository = "testrepo"
20+
# Create args using shared fixture
21+
args = create_args(user="testuser", repository="testrepo")
3122

3223
repository = {"full_name": "testuser/testrepo"}
3324

@@ -356,9 +347,12 @@ class TestJWTWorkaround:
356347

357348
def test_markdown_api_extracts_jwt_url(self):
358349
"""Markdown API response with JWT URL is extracted correctly."""
359-
from unittest.mock import patch, Mock
360-
361-
html_response = '''<p><a href="https://private-user-images.githubusercontent.com/123/abc.png?jwt=eyJhbGciOiJ"><img src="https://private-user-images.githubusercontent.com/123/abc.png?jwt=eyJhbGciOiJ" alt="img"></a></p>'''
350+
html_response = (
351+
'<p><a href="https://private-user-images.githubusercontent.com'
352+
'/123/abc.png?jwt=eyJhbGciOiJ"><img src="https://private-user-'
353+
'images.githubusercontent.com/123/abc.png?jwt=eyJhbGciOiJ" '
354+
'alt="img"></a></p>'
355+
)
362356

363357
mock_response = Mock()
364358
mock_response.read.return_value = html_response.encode("utf-8")
@@ -370,14 +364,18 @@ def test_markdown_api_extracts_jwt_url(self):
370364
"owner/repo"
371365
)
372366

373-
assert result == "https://private-user-images.githubusercontent.com/123/abc.png?jwt=eyJhbGciOiJ"
367+
expected = (
368+
"https://private-user-images.githubusercontent.com"
369+
"/123/abc.png?jwt=eyJhbGciOiJ"
370+
)
371+
assert result == expected
374372

375373
def test_markdown_api_returns_none_on_http_error(self):
376374
"""HTTP errors return None."""
377-
from unittest.mock import patch
378375
from urllib.error import HTTPError
379376

380-
with patch("github_backup.github_backup.urlopen", side_effect=HTTPError(None, 403, "Forbidden", {}, None)):
377+
error = HTTPError("http://test", 403, "Forbidden", {}, None)
378+
with patch("github_backup.github_backup.urlopen", side_effect=error):
381379
result = github_backup.get_jwt_signed_url_via_markdown_api(
382380
"https://github.com/user-attachments/assets/abc123",
383381
"github_pat_token",
@@ -388,8 +386,6 @@ def test_markdown_api_returns_none_on_http_error(self):
388386

389387
def test_markdown_api_returns_none_when_no_jwt_url(self):
390388
"""Response without JWT URL returns None."""
391-
from unittest.mock import patch, Mock
392-
393389
mock_response = Mock()
394390
mock_response.read.return_value = b"<p>No image here</p>"
395391

@@ -406,32 +402,36 @@ def test_needs_jwt_only_for_fine_grained_private_assets(self):
406402
"""needs_jwt is True only for fine-grained + private + /assets/ URL."""
407403
assets_url = "https://github.com/user-attachments/assets/abc123"
408404
files_url = "https://github.com/user-attachments/files/123/doc.pdf"
405+
token_fine = "github_pat_test"
406+
private = True
407+
public = False
409408

410409
# Fine-grained + private + assets = True
411-
assert (
412-
"github_pat_" is not None
413-
and True # private
410+
needs_jwt = (
411+
token_fine is not None
412+
and private
414413
and "github.com/user-attachments/assets/" in assets_url
415-
) is True
414+
)
415+
assert needs_jwt is True
416416

417417
# Fine-grained + private + files = False
418-
assert (
419-
"github_pat_" is not None
420-
and True
418+
needs_jwt = (
419+
token_fine is not None
420+
and private
421421
and "github.com/user-attachments/assets/" in files_url
422-
) is False
422+
)
423+
assert needs_jwt is False
423424

424425
# Fine-grained + public + assets = False
425-
assert (
426-
"github_pat_" is not None
427-
and False # public
426+
needs_jwt = (
427+
token_fine is not None
428+
and public
428429
and "github.com/user-attachments/assets/" in assets_url
429-
) is False
430+
)
431+
assert needs_jwt is False
430432

431433
def test_jwt_workaround_sets_manifest_flag(self, attachment_test_setup):
432434
"""Successful JWT workaround sets jwt_workaround flag in manifest."""
433-
from unittest.mock import patch, Mock
434-
435435
setup = attachment_test_setup
436436
setup["args"].token_fine = "github_pat_test"
437437
setup["repository"]["private"] = True
@@ -460,8 +460,6 @@ def test_jwt_workaround_sets_manifest_flag(self, attachment_test_setup):
460460

461461
def test_jwt_workaround_failure_uses_skipped_at(self, attachment_test_setup):
462462
"""Failed JWT workaround uses skipped_at instead of downloaded_at."""
463-
from unittest.mock import patch
464-
465463
setup = attachment_test_setup
466464
setup["args"].token_fine = "github_pat_test"
467465
setup["repository"]["private"] = True

tests/test_case_sensitivity.py

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,21 @@
11
"""Tests for case-insensitive username/organization filtering."""
22

33
import pytest
4-
from unittest.mock import Mock
54

65
from github_backup import github_backup
76

87

98
class TestCaseSensitivity:
109
"""Test suite for case-insensitive username matching in filter_repositories."""
1110

12-
def test_filter_repositories_case_insensitive_user(self):
11+
def test_filter_repositories_case_insensitive_user(self, create_args):
1312
"""Should filter repositories case-insensitively for usernames.
1413
1514
Reproduces issue #198 where typing 'iamrodos' fails to match
1615
repositories with owner.login='Iamrodos' (the canonical case from GitHub API).
1716
"""
1817
# Simulate user typing lowercase username
19-
args = Mock()
20-
args.user = "iamrodos" # lowercase (what user typed)
21-
args.repository = None
22-
args.name_regex = None
23-
args.languages = None
24-
args.exclude = None
25-
args.fork = False
26-
args.private = False
27-
args.public = False
28-
args.all = True
29-
args.skip_archived = False
30-
args.starred_skip_size_over = None
18+
args = create_args(user="iamrodos")
3119

3220
# Simulate GitHub API returning canonical case
3321
repos = [
@@ -52,23 +40,12 @@ def test_filter_repositories_case_insensitive_user(self):
5240
assert filtered[0]["name"] == "repo1"
5341
assert filtered[1]["name"] == "repo2"
5442

55-
def test_filter_repositories_case_insensitive_org(self):
43+
def test_filter_repositories_case_insensitive_org(self, create_args):
5644
"""Should filter repositories case-insensitively for organizations.
5745
5846
Tests the example from issue #198 where 'prai-org' doesn't match 'PRAI-Org'.
5947
"""
60-
args = Mock()
61-
args.user = "prai-org" # lowercase (what user typed)
62-
args.repository = None
63-
args.name_regex = None
64-
args.languages = None
65-
args.exclude = None
66-
args.fork = False
67-
args.private = False
68-
args.public = False
69-
args.all = True
70-
args.skip_archived = False
71-
args.starred_skip_size_over = None
48+
args = create_args(user="prai-org")
7249

7350
repos = [
7451
{
@@ -85,20 +62,9 @@ def test_filter_repositories_case_insensitive_org(self):
8562
assert len(filtered) == 1
8663
assert filtered[0]["name"] == "repo1"
8764

88-
def test_filter_repositories_case_variations(self):
65+
def test_filter_repositories_case_variations(self, create_args):
8966
"""Should handle various case combinations correctly."""
90-
args = Mock()
91-
args.user = "TeSt-UsEr" # Mixed case
92-
args.repository = None
93-
args.name_regex = None
94-
args.languages = None
95-
args.exclude = None
96-
args.fork = False
97-
args.private = False
98-
args.public = False
99-
args.all = True
100-
args.skip_archived = False
101-
args.starred_skip_size_over = None
67+
args = create_args(user="TeSt-UsEr")
10268

10369
repos = [
10470
{"name": "repo1", "owner": {"login": "test-user"}, "private": False, "fork": False},

0 commit comments

Comments
 (0)