mirror of
https://github.com/josegonzalez/python-github-backup.git
synced 2025-12-15 12:21:09 +01:00
Allow users to skip downloading release assets for specific repositories while still backing up release metadata. Useful for starred repos with large assets (e.g. syncthing with 27GB+). Usage: --skip-assets-on repo1 repo2 owner/repo3 Features: - Space-separated repos (consistent with --exclude) - Case-insensitive matching - Supports both repo name and owner/repo format
321 lines
12 KiB
Python
321 lines
12 KiB
Python
"""Tests for --skip-assets-on flag behavior (issue #135)."""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
|
|
from github_backup import github_backup
|
|
|
|
|
|
class TestSkipAssetsOn:
|
|
"""Test suite for --skip-assets-on flag.
|
|
|
|
Issue #135: Allow skipping asset downloads for specific repositories
|
|
while still backing up release metadata.
|
|
"""
|
|
|
|
def _create_mock_args(self, **overrides):
|
|
"""Create a mock args object with sensible defaults."""
|
|
args = Mock()
|
|
args.user = "testuser"
|
|
args.output_directory = "/tmp/backup"
|
|
args.include_repository = False
|
|
args.include_everything = False
|
|
args.include_gists = False
|
|
args.include_starred_gists = False
|
|
args.all_starred = False
|
|
args.skip_existing = False
|
|
args.bare_clone = False
|
|
args.lfs_clone = False
|
|
args.no_prune = False
|
|
args.include_wiki = False
|
|
args.include_issues = False
|
|
args.include_issue_comments = False
|
|
args.include_issue_events = False
|
|
args.include_pulls = False
|
|
args.include_pull_comments = False
|
|
args.include_pull_commits = False
|
|
args.include_pull_details = False
|
|
args.include_labels = False
|
|
args.include_hooks = False
|
|
args.include_milestones = False
|
|
args.include_releases = True
|
|
args.include_assets = True
|
|
args.skip_assets_on = []
|
|
args.include_attachments = False
|
|
args.incremental = False
|
|
args.incremental_by_files = False
|
|
args.github_host = None
|
|
args.prefer_ssh = False
|
|
args.token_classic = "test-token"
|
|
args.token_fine = None
|
|
args.username = None
|
|
args.password = None
|
|
args.as_app = False
|
|
args.osx_keychain_item_name = None
|
|
args.osx_keychain_item_account = None
|
|
args.skip_prerelease = False
|
|
args.number_of_latest_releases = None
|
|
|
|
for key, value in overrides.items():
|
|
setattr(args, key, value)
|
|
|
|
return args
|
|
|
|
def _create_mock_repository(self, name="test-repo", owner="testuser"):
|
|
"""Create a mock repository object."""
|
|
return {
|
|
"name": name,
|
|
"full_name": f"{owner}/{name}",
|
|
"owner": {"login": owner},
|
|
"private": False,
|
|
"fork": False,
|
|
"has_wiki": False,
|
|
}
|
|
|
|
def _create_mock_release(self, tag="v1.0.0"):
|
|
"""Create a mock release object."""
|
|
return {
|
|
"tag_name": tag,
|
|
"name": tag,
|
|
"prerelease": False,
|
|
"draft": False,
|
|
"assets_url": f"https://api.github.com/repos/testuser/test-repo/releases/{tag}/assets",
|
|
}
|
|
|
|
def _create_mock_asset(self, name="asset.zip"):
|
|
"""Create a mock asset object."""
|
|
return {
|
|
"name": name,
|
|
"url": f"https://api.github.com/repos/testuser/test-repo/releases/assets/{name}",
|
|
}
|
|
|
|
|
|
class TestSkipAssetsOnArgumentParsing(TestSkipAssetsOn):
|
|
"""Tests for --skip-assets-on argument parsing."""
|
|
|
|
def test_skip_assets_on_not_set_defaults_to_none(self):
|
|
"""When --skip-assets-on is not specified, it should default to None."""
|
|
args = github_backup.parse_args(["testuser"])
|
|
assert args.skip_assets_on is None
|
|
|
|
def test_skip_assets_on_single_repo(self):
|
|
"""Single --skip-assets-on should create list with one item."""
|
|
args = github_backup.parse_args(["testuser", "--skip-assets-on", "big-repo"])
|
|
assert args.skip_assets_on == ["big-repo"]
|
|
|
|
def test_skip_assets_on_multiple_repos(self):
|
|
"""Multiple repos can be specified space-separated (like --exclude)."""
|
|
args = github_backup.parse_args(
|
|
[
|
|
"testuser",
|
|
"--skip-assets-on",
|
|
"big-repo",
|
|
"another-repo",
|
|
"owner/third-repo",
|
|
]
|
|
)
|
|
assert args.skip_assets_on == ["big-repo", "another-repo", "owner/third-repo"]
|
|
|
|
|
|
class TestSkipAssetsOnBehavior(TestSkipAssetsOn):
|
|
"""Tests for --skip-assets-on behavior in backup_releases."""
|
|
|
|
@patch("github_backup.github_backup.download_file")
|
|
@patch("github_backup.github_backup.retrieve_data")
|
|
@patch("github_backup.github_backup.mkdir_p")
|
|
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
def test_assets_downloaded_when_not_skipped(
|
|
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
):
|
|
"""Assets should be downloaded when repo is not in skip list."""
|
|
args = self._create_mock_args(skip_assets_on=[])
|
|
repository = self._create_mock_repository(name="normal-repo")
|
|
release = self._create_mock_release()
|
|
asset = self._create_mock_asset()
|
|
|
|
mock_json_dump.return_value = True
|
|
mock_retrieve.side_effect = [
|
|
[release], # First call: get releases
|
|
[asset], # Second call: get assets
|
|
]
|
|
|
|
with patch("os.path.join", side_effect=lambda *args: "/".join(args)):
|
|
github_backup.backup_releases(
|
|
args,
|
|
"/tmp/backup/repositories/normal-repo",
|
|
repository,
|
|
"https://api.github.com/repos/{owner}/{repo}",
|
|
include_assets=True,
|
|
)
|
|
|
|
# download_file should have been called for the asset
|
|
mock_download.assert_called_once()
|
|
|
|
@patch("github_backup.github_backup.download_file")
|
|
@patch("github_backup.github_backup.retrieve_data")
|
|
@patch("github_backup.github_backup.mkdir_p")
|
|
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
def test_assets_skipped_when_repo_name_matches(
|
|
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
):
|
|
"""Assets should be skipped when repo name is in skip list."""
|
|
args = self._create_mock_args(skip_assets_on=["big-repo"])
|
|
repository = self._create_mock_repository(name="big-repo")
|
|
release = self._create_mock_release()
|
|
|
|
mock_json_dump.return_value = True
|
|
mock_retrieve.return_value = [release]
|
|
|
|
github_backup.backup_releases(
|
|
args,
|
|
"/tmp/backup/repositories/big-repo",
|
|
repository,
|
|
"https://api.github.com/repos/{owner}/{repo}",
|
|
include_assets=True,
|
|
)
|
|
|
|
# download_file should NOT have been called
|
|
mock_download.assert_not_called()
|
|
|
|
@patch("github_backup.github_backup.download_file")
|
|
@patch("github_backup.github_backup.retrieve_data")
|
|
@patch("github_backup.github_backup.mkdir_p")
|
|
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
def test_assets_skipped_when_full_name_matches(
|
|
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
):
|
|
"""Assets should be skipped when owner/repo format matches."""
|
|
args = self._create_mock_args(skip_assets_on=["otheruser/big-repo"])
|
|
repository = self._create_mock_repository(name="big-repo", owner="otheruser")
|
|
release = self._create_mock_release()
|
|
|
|
mock_json_dump.return_value = True
|
|
mock_retrieve.return_value = [release]
|
|
|
|
github_backup.backup_releases(
|
|
args,
|
|
"/tmp/backup/repositories/big-repo",
|
|
repository,
|
|
"https://api.github.com/repos/{owner}/{repo}",
|
|
include_assets=True,
|
|
)
|
|
|
|
# download_file should NOT have been called
|
|
mock_download.assert_not_called()
|
|
|
|
@patch("github_backup.github_backup.download_file")
|
|
@patch("github_backup.github_backup.retrieve_data")
|
|
@patch("github_backup.github_backup.mkdir_p")
|
|
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
def test_case_insensitive_matching(
|
|
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
):
|
|
"""Skip matching should be case-insensitive."""
|
|
# User types uppercase, repo name is lowercase
|
|
args = self._create_mock_args(skip_assets_on=["BIG-REPO"])
|
|
repository = self._create_mock_repository(name="big-repo")
|
|
release = self._create_mock_release()
|
|
|
|
mock_json_dump.return_value = True
|
|
mock_retrieve.return_value = [release]
|
|
|
|
github_backup.backup_releases(
|
|
args,
|
|
"/tmp/backup/repositories/big-repo",
|
|
repository,
|
|
"https://api.github.com/repos/{owner}/{repo}",
|
|
include_assets=True,
|
|
)
|
|
|
|
# download_file should NOT have been called (case-insensitive match)
|
|
assert not mock_download.called
|
|
|
|
@patch("github_backup.github_backup.download_file")
|
|
@patch("github_backup.github_backup.retrieve_data")
|
|
@patch("github_backup.github_backup.mkdir_p")
|
|
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
def test_multiple_skip_repos(
|
|
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
):
|
|
"""Multiple repos in skip list should all be skipped."""
|
|
args = self._create_mock_args(skip_assets_on=["repo1", "repo2", "repo3"])
|
|
repository = self._create_mock_repository(name="repo2")
|
|
release = self._create_mock_release()
|
|
|
|
mock_json_dump.return_value = True
|
|
mock_retrieve.return_value = [release]
|
|
|
|
github_backup.backup_releases(
|
|
args,
|
|
"/tmp/backup/repositories/repo2",
|
|
repository,
|
|
"https://api.github.com/repos/{owner}/{repo}",
|
|
include_assets=True,
|
|
)
|
|
|
|
# download_file should NOT have been called
|
|
mock_download.assert_not_called()
|
|
|
|
@patch("github_backup.github_backup.download_file")
|
|
@patch("github_backup.github_backup.retrieve_data")
|
|
@patch("github_backup.github_backup.mkdir_p")
|
|
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
def test_release_metadata_still_saved_when_assets_skipped(
|
|
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
):
|
|
"""Release JSON should still be saved even when assets are skipped."""
|
|
args = self._create_mock_args(skip_assets_on=["big-repo"])
|
|
repository = self._create_mock_repository(name="big-repo")
|
|
release = self._create_mock_release()
|
|
|
|
mock_json_dump.return_value = True
|
|
mock_retrieve.return_value = [release]
|
|
|
|
github_backup.backup_releases(
|
|
args,
|
|
"/tmp/backup/repositories/big-repo",
|
|
repository,
|
|
"https://api.github.com/repos/{owner}/{repo}",
|
|
include_assets=True,
|
|
)
|
|
|
|
# json_dump_if_changed should have been called for release metadata
|
|
mock_json_dump.assert_called_once()
|
|
# But download_file should NOT have been called
|
|
mock_download.assert_not_called()
|
|
|
|
@patch("github_backup.github_backup.download_file")
|
|
@patch("github_backup.github_backup.retrieve_data")
|
|
@patch("github_backup.github_backup.mkdir_p")
|
|
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
def test_non_matching_repo_still_downloads_assets(
|
|
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
):
|
|
"""Repos not in skip list should still download assets."""
|
|
args = self._create_mock_args(skip_assets_on=["other-repo"])
|
|
repository = self._create_mock_repository(name="normal-repo")
|
|
release = self._create_mock_release()
|
|
asset = self._create_mock_asset()
|
|
|
|
mock_json_dump.return_value = True
|
|
mock_retrieve.side_effect = [
|
|
[release], # First call: get releases
|
|
[asset], # Second call: get assets
|
|
]
|
|
|
|
with patch("os.path.join", side_effect=lambda *args: "/".join(args)):
|
|
github_backup.backup_releases(
|
|
args,
|
|
"/tmp/backup/repositories/normal-repo",
|
|
repository,
|
|
"https://api.github.com/repos/{owner}/{repo}",
|
|
include_assets=True,
|
|
)
|
|
|
|
# download_file SHOULD have been called
|
|
mock_download.assert_called_once()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|