feat: small refactorings
Test and coverage / build (push) Failing after 56s

This commit is contained in:
2026-06-28 01:18:08 +03:00
parent 17aaa41101
commit 998665a4bf
13 changed files with 634 additions and 140 deletions
+111
View File
@@ -0,0 +1,111 @@
import datetime
import tomllib
from pathlib import Path
from src import cli
from src.config import Config, FetchConfig
from src.gitea_api import GiteaApiError
from src.models import GiteaRepository, GiteaUser
def make_repo(repo_id=42, name="repo") -> GiteaRepository:
return GiteaRepository(
ssh_url="ssh://git@example.com/alex/repo.git",
clone_url="https://example.com/alex/repo.git",
name=name,
id=repo_id,
updated_at=datetime.datetime.now(),
owner=GiteaUser(
id=23,
login="alex",
email="alex@example.com",
),
)
def make_config(tmp_path, fetch: FetchConfig | None = None) -> Config:
return Config(
api_token="api-token",
endpoint="https://example.com",
repository_format="{owner}/{name}",
out_dir=str(tmp_path),
fetch=fetch or FetchConfig(ssh_key="/tmp/test_key"),
)
def test_project_defines_console_script():
data = tomllib.loads(Path("pyproject.toml").read_text())
assert data["project"]["scripts"]["gitea-mirror"] == "src.cli:main"
def test_run_returns_failure_when_api_fetch_fails(monkeypatch, tmp_path):
class FakeApi:
def __init__(self, endpoint, api_token):
pass
def get_repositories(self):
raise GiteaApiError("request failed")
monkeypatch.setattr(cli, "GiteaApi", FakeApi)
assert cli.run(make_config(tmp_path)) == 1
def test_run_returns_failure_when_any_repo_fails(monkeypatch, tmp_path):
class FakeApi:
def __init__(self, endpoint, api_token):
pass
def get_repositories(self):
return [
make_repo(repo_id=1, name="repo-1"),
make_repo(repo_id=2, name="repo-2"),
]
calls = []
def fake_process_repo(config, repo):
calls.append(repo.repo_id)
return repo.repo_id == 1
monkeypatch.setattr(cli, "GiteaApi", FakeApi)
monkeypatch.setattr(cli, "process_repo", fake_process_repo)
assert cli.run(make_config(tmp_path)) == 1
assert calls == [1, 2]
def test_run_returns_success_when_all_repos_succeed(monkeypatch, tmp_path):
class FakeApi:
def __init__(self, endpoint, api_token):
pass
def get_repositories(self):
return [make_repo()]
monkeypatch.setattr(cli, "GiteaApi", FakeApi)
monkeypatch.setattr(cli, "process_repo", lambda config, repo: True)
assert cli.run(make_config(tmp_path)) == 0
def test_main_returns_failure_for_invalid_config(monkeypatch):
def fake_read_toml_config(path):
raise RuntimeError("bad")
monkeypatch.setattr(cli, "read_toml_config", fake_read_toml_config)
assert cli.main(["bad.toml"]) == 1
def test_main_uses_config_path(monkeypatch, tmp_path):
config = make_config(tmp_path)
calls = []
monkeypatch.setattr(cli, "read_toml_config", lambda path: config)
monkeypatch.setattr(cli, "setup_logging", lambda logging_config: None)
monkeypatch.setattr(cli, "run", lambda config: calls.append(config) or 0)
assert cli.main(["config.toml"]) == 0
assert calls == [config]
+23
View File
@@ -120,6 +120,29 @@ from src.models import LoggingOutputConfig
'token = "repo-token"',
None,
),
(
'[main]\napi_token = "something"\n'
'format = "{unknown}"\n'
'endpoint = "https://example.com"\n'
'out_dir = "/home/user/repositories"\n'
"\n"
"[fetch]\n"
'ssh_key = "/tmp/no_key"',
None,
),
(
'[main]\napi_token = "something"\n'
'format = "{owner}/{name}"\n'
'endpoint = "https://example.com"\n'
'out_dir = "/home/user/repositories"\n'
"\n"
"[fetch]\n"
'ssh_key = "/tmp/no_key"\n'
"\n"
"[logging]\n"
'level = "INF"',
None,
),
("not valid toml", None),
],
)
+101
View File
@@ -0,0 +1,101 @@
import datetime
import pytest
import requests
from pydantic import ValidationError
from src.gitea_api import GiteaApi, GiteaApiError
def repo_data(repo_id=42):
return {
"id": repo_id,
"ssh_url": "ssh://git@example.com/alex/repo.git",
"clone_url": "https://example.com/alex/repo.git",
"name": "repo",
"updated_at": datetime.datetime.now(datetime.UTC).isoformat(),
"owner": {
"id": 23,
"login": "alex",
"email": "alex@example.com",
},
}
class FakeResponse:
def __init__(self, status_code=200, data=None, json_error=None):
self.status_code = status_code
self._data = data
self._json_error = json_error
def json(self):
if self._json_error:
raise self._json_error
return self._data
class FakeSession:
def __init__(self, responses=None, request_error=None):
self.headers = {}
self.responses = list(responses or [])
self.request_error = request_error
self.calls = []
def get(self, url, params, timeout):
self.calls.append((url, params, timeout))
if self.request_error:
raise self.request_error
return self.responses.pop(0)
def test_get_repositories_fetches_all_pages_with_timeout(monkeypatch):
session = FakeSession(
[
FakeResponse(data=[repo_data(repo_id=1)]),
FakeResponse(data=[repo_data(repo_id=2)]),
FakeResponse(data=[]),
]
)
monkeypatch.setattr(requests, "session", lambda: session)
repos = GiteaApi(
endpoint="https://example.com",
api_token="api-token",
).get_repositories(page_size=1)
assert [repo.repo_id for repo in repos] == [1, 2]
assert session.headers == {"Authorization": "token api-token"}
assert session.calls[0][2] == (5, 30)
def test_get_repositories_raises_on_http_error(monkeypatch):
session = FakeSession([FakeResponse(status_code=500, data=[])])
monkeypatch.setattr(requests, "session", lambda: session)
with pytest.raises(GiteaApiError, match="status 500"):
GiteaApi(
endpoint="https://example.com",
api_token="api-token",
).get_repositories()
def test_get_repositories_raises_on_network_error(monkeypatch):
session = FakeSession(request_error=requests.Timeout("timed out"))
monkeypatch.setattr(requests, "session", lambda: session)
with pytest.raises(GiteaApiError, match="request failed"):
GiteaApi(
endpoint="https://example.com",
api_token="api-token",
).get_repositories()
def test_get_repositories_raises_on_invalid_payload(monkeypatch):
session = FakeSession([FakeResponse(data=[{"id": 1}])])
monkeypatch.setattr(requests, "session", lambda: session)
with pytest.raises((GiteaApiError, ValidationError)):
GiteaApi(
endpoint="https://example.com",
api_token="api-token",
).get_repositories()
+27 -19
View File
@@ -1,19 +1,10 @@
import datetime
import importlib.util
from pathlib import Path
from src import cli
from src.config import Config, FetchConfig
from src.models import GiteaRepository, GiteaUser
def load_mirror_module():
module_path = Path(__file__).parent.parent / "gitea-mirror.py"
spec = importlib.util.spec_from_file_location("gitea_mirror", module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def make_repo() -> GiteaRepository:
return GiteaRepository(
ssh_url="ssh://git@example.com/alex/repo.git",
@@ -40,16 +31,15 @@ def make_config(tmp_path, fetch: FetchConfig) -> Config:
def test_process_repo_clones_with_ssh_url_and_key_env(monkeypatch, tmp_path):
mirror = load_mirror_module()
calls = []
def fake_git_clone(repository_url, repository, env):
calls.append((repository_url, repository, env))
return True
monkeypatch.setattr(mirror, "git_clone", fake_git_clone)
monkeypatch.setattr(cli, "git_clone", fake_git_clone)
mirror.process_repo(
assert cli.process_repo(
config=make_config(tmp_path, FetchConfig(ssh_key="/tmp/test_key")),
repo=make_repo(),
)
@@ -68,16 +58,15 @@ def test_process_repo_clones_with_ssh_url_and_key_env(monkeypatch, tmp_path):
def test_process_repo_clones_with_https_url_and_auth_env(monkeypatch, tmp_path):
mirror = load_mirror_module()
calls = []
def fake_git_clone(repository_url, repository, env):
calls.append((repository_url, repository, env))
return True
monkeypatch.setattr(mirror, "git_clone", fake_git_clone)
monkeypatch.setattr(cli, "git_clone", fake_git_clone)
mirror.process_repo(
assert cli.process_repo(
config=make_config(tmp_path, FetchConfig(user="alex", token="secret")),
repo=make_repo(),
)
@@ -91,7 +80,6 @@ def test_process_repo_clones_with_https_url_and_auth_env(monkeypatch, tmp_path):
def test_process_repo_pulls_existing_repo_with_fetch_env(monkeypatch, tmp_path):
mirror = load_mirror_module()
calls = []
repo_path = tmp_path / "alex" / "repo" / ".git"
repo_path.mkdir(parents=True)
@@ -100,9 +88,9 @@ def test_process_repo_pulls_existing_repo_with_fetch_env(monkeypatch, tmp_path):
calls.append((repository, env))
return True
monkeypatch.setattr(mirror, "git_pull", fake_git_pull)
monkeypatch.setattr(cli, "git_pull", fake_git_pull)
mirror.process_repo(
assert cli.process_repo(
config=make_config(tmp_path, FetchConfig(ssh_key="/tmp/test_key")),
repo=make_repo(),
)
@@ -117,3 +105,23 @@ def test_process_repo_pulls_existing_repo_with_fetch_env(monkeypatch, tmp_path):
},
)
]
def test_process_repo_returns_false_when_clone_fails(monkeypatch, tmp_path):
monkeypatch.setattr(cli, "git_clone", lambda **kwargs: False)
assert not cli.process_repo(
config=make_config(tmp_path, FetchConfig(ssh_key="/tmp/test_key")),
repo=make_repo(),
)
def test_process_repo_returns_false_when_pull_fails(monkeypatch, tmp_path):
repo_path = tmp_path / "alex" / "repo" / ".git"
repo_path.mkdir(parents=True)
monkeypatch.setattr(cli, "git_pull", lambda repository, env: False)
assert not cli.process_repo(
config=make_config(tmp_path, FetchConfig(ssh_key="/tmp/test_key")),
repo=make_repo(),
)
+26 -1
View File
@@ -1,6 +1,7 @@
import pytest
from src.repository_name import is_valid_format
from src.repository_name import get_repository_path, is_valid_format
from tests.test_mirror_process import make_repo
@pytest.mark.parametrize(
@@ -13,3 +14,27 @@ from src.repository_name import is_valid_format
)
def test_name_formatting(name_format, expected):
assert is_valid_format(name_format) == expected
@pytest.mark.parametrize(
"name_format",
[
"../{name}",
"/tmp/{name}",
],
)
def test_repository_path_rejects_paths_outside_out_dir(tmp_path, name_format):
with pytest.raises(ValueError):
get_repository_path(
out_dir=str(tmp_path),
name_format=name_format,
repo=make_repo(),
)
def test_repository_path_returns_path_inside_out_dir(tmp_path):
assert get_repository_path(
out_dir=str(tmp_path),
name_format="{owner}/{name}",
repo=make_repo(),
) == str(tmp_path / "alex" / "repo")