Public Access
Post-merge / detect-type (push) Successful in 32s
Post-merge / configure-repo (push) Successful in 40s
Post-merge / sync-wiki (push) Successful in 42s
Post-merge / release (push) Successful in 53s
Post-merge / vikunja (push) Successful in 1m1s
Post-merge / validate-commit-msg (push) Successful in 1m8s
Post-merge / badges (push) Successful in 1m19s
Post-merge / publish (push) Failing after 38s
Build Images / detect-type (push) Successful in 33s
Build Images / build-and-push (push) Failing after 2m54s
Build Images / cleanup (push) Has been skipped
584 lines
23 KiB
Python
584 lines
23 KiB
Python
"""Unit tests for devx.tools.build_image."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from click import ClickException
|
|
from click.testing import CliRunner
|
|
|
|
import devx.tools.build_image as build_image
|
|
from devx.tools.build_image import (
|
|
ImageSpec,
|
|
build_full_tag,
|
|
load_manifest,
|
|
push_image,
|
|
registry_login,
|
|
)
|
|
from devx.tools.build_image import (
|
|
build_image as do_build,
|
|
)
|
|
from devx.tools.clean_images import select_for_deletion, sort_versions_by_date
|
|
|
|
|
|
class TestImageSpec:
|
|
def test_from_dict_minimal(self) -> None:
|
|
spec = ImageSpec.from_dict({"name": "ci-base", "dockerfile": "docker/ci-base/Dockerfile"})
|
|
assert spec.name == "ci-base"
|
|
assert spec.dockerfile == "docker/ci-base/Dockerfile"
|
|
assert spec.context == "."
|
|
assert spec.tags == ["latest"]
|
|
|
|
def test_from_dict_full(self) -> None:
|
|
spec = ImageSpec.from_dict(
|
|
{
|
|
"name": "ci-quality",
|
|
"dockerfile": "docker/ci-quality/Dockerfile",
|
|
"context": ".",
|
|
"tags": ["latest", "0.19.3"],
|
|
}
|
|
)
|
|
assert spec.name == "ci-quality"
|
|
assert spec.dockerfile == "docker/ci-quality/Dockerfile"
|
|
assert spec.context == "."
|
|
assert spec.tags == ["latest", "0.19.3"]
|
|
|
|
def test_from_dict_missing_name(self) -> None:
|
|
with pytest.raises(ValueError, match="missing 'name'"):
|
|
ImageSpec.from_dict({"dockerfile": "Dockerfile"})
|
|
|
|
def test_from_dict_missing_dockerfile(self) -> None:
|
|
with pytest.raises(ValueError, match="missing 'dockerfile'"):
|
|
ImageSpec.from_dict({"name": "ci-base"})
|
|
|
|
def test_from_dict_tags_not_list(self) -> None:
|
|
with pytest.raises(ValueError, match="tags.*must be a list"):
|
|
ImageSpec.from_dict(
|
|
{
|
|
"name": "ci-base",
|
|
"dockerfile": "Dockerfile",
|
|
"tags": "latest",
|
|
}
|
|
)
|
|
|
|
def test_from_dict_empty_tags_defaults_to_latest(self) -> None:
|
|
spec = ImageSpec.from_dict(
|
|
{
|
|
"name": "ci-base",
|
|
"dockerfile": "Dockerfile",
|
|
"tags": [],
|
|
}
|
|
)
|
|
assert spec.tags == ["latest"]
|
|
|
|
|
|
class TestBuildFullTag:
|
|
def test_no_registry(self) -> None:
|
|
assert build_full_tag(None, "ci-base", "latest") == "ci-base:latest"
|
|
|
|
def test_with_registry(self) -> None:
|
|
assert build_full_tag("git.example.com", "ci-base", "0.1.0") == "git.example.com/ci-base:0.1.0"
|
|
|
|
def test_with_registry_and_path(self) -> None:
|
|
assert (
|
|
build_full_tag("git.example.com", "oblachno/ci-base", "latest") == "git.example.com/oblachno/ci-base:latest"
|
|
)
|
|
|
|
|
|
class TestLoadManifest:
|
|
def test_load_valid_manifest(self, tmp_path: Path) -> None:
|
|
manifest = tmp_path / "images.json"
|
|
manifest.write_text(
|
|
json.dumps(
|
|
[
|
|
{"name": "ci-base", "dockerfile": "docker/ci-base/Dockerfile"},
|
|
{"name": "ci-quality", "dockerfile": "docker/ci-quality/Dockerfile", "tags": ["latest", "1.0"]},
|
|
]
|
|
)
|
|
)
|
|
specs = load_manifest(manifest)
|
|
assert len(specs) == 2
|
|
assert specs[0].name == "ci-base"
|
|
assert specs[1].tags == ["latest", "1.0"]
|
|
|
|
def test_load_missing_file(self, tmp_path: Path) -> None:
|
|
with pytest.raises(ClickException, match="not found"):
|
|
load_manifest(tmp_path / "nonexistent.json")
|
|
|
|
def test_load_not_a_list(self, tmp_path: Path) -> None:
|
|
manifest = tmp_path / "images.json"
|
|
manifest.write_text(json.dumps({"name": "ci-base"}))
|
|
with pytest.raises(ClickException, match="must be a JSON list"):
|
|
load_manifest(manifest)
|
|
|
|
|
|
class TestRegistryLogin:
|
|
def test_success(self) -> None:
|
|
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
|
assert registry_login("git.example.com", "user", "token") is True
|
|
assert mock_run.call_args.args[0] == [
|
|
"docker",
|
|
"login",
|
|
"git.example.com",
|
|
"-u",
|
|
"user",
|
|
"--password-stdin",
|
|
]
|
|
assert mock_run.call_args.kwargs["input"] == "token"
|
|
|
|
def test_failure(self) -> None:
|
|
mock_result = MagicMock(returncode=1, stderr="auth failed", stdout="")
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
|
assert registry_login("git.example.com", "user", "bad") is False
|
|
|
|
def test_dry_run(self) -> None:
|
|
with patch("devx.tools.build_image.subprocess.run") as mock_run:
|
|
assert registry_login("git.example.com", "user", "token", dry_run=True) is True
|
|
mock_run.assert_not_called()
|
|
|
|
|
|
class TestBuildImage:
|
|
def test_success(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile), context=".", tags=["latest"])
|
|
mock_result = MagicMock(returncode=0)
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
|
assert do_build(spec) is True
|
|
|
|
def test_dockerfile_not_found(self) -> None:
|
|
spec = ImageSpec(name="ci-base", dockerfile="nonexistent/Dockerfile")
|
|
assert do_build(spec) is False
|
|
|
|
def test_build_failure(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile))
|
|
mock_result = MagicMock(returncode=1)
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
|
assert do_build(spec) is False
|
|
|
|
def test_dry_run(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile), tags=["latest", "1.0"])
|
|
with patch("devx.tools.build_image.subprocess.run") as mock_run:
|
|
assert do_build(spec, dry_run=True) is True
|
|
mock_run.assert_not_called()
|
|
|
|
def test_with_registry(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile), tags=["latest"])
|
|
mock_result = MagicMock(returncode=0)
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
|
assert do_build(spec, registry="git.example.com") is True
|
|
cmd = mock_run.call_args.args[0]
|
|
assert "-t" in cmd
|
|
idx = cmd.index("-t")
|
|
assert cmd[idx + 1] == "git.example.com/ci-base:latest"
|
|
|
|
def test_pull_flag(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
spec = ImageSpec(name="ci-base", dockerfile=str(dockerfile))
|
|
mock_result = MagicMock(returncode=0)
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
|
assert do_build(spec, pull=True) is True
|
|
cmd = mock_run.call_args.args[0]
|
|
assert "--pull" in cmd
|
|
|
|
|
|
class TestPushImage:
|
|
def test_success(self) -> None:
|
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
|
|
mock_result = MagicMock(returncode=0, stderr="", stdout="")
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result) as mock_run:
|
|
assert push_image(spec, "git.example.com") is True
|
|
assert mock_run.call_count == 2
|
|
|
|
def test_partial_failure(self) -> None:
|
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest", "1.0"])
|
|
results = [
|
|
MagicMock(returncode=0, stderr="", stdout=""),
|
|
MagicMock(returncode=1, stderr="push failed", stdout=""),
|
|
]
|
|
with patch("devx.tools.build_image.subprocess.run", side_effect=results):
|
|
assert push_image(spec, "git.example.com") is False
|
|
|
|
def test_dry_run(self) -> None:
|
|
spec = ImageSpec(name="ci-base", dockerfile="Dockerfile", tags=["latest"])
|
|
with patch("devx.tools.build_image.subprocess.run") as mock_run:
|
|
assert push_image(spec, "git.example.com", dry_run=True) is True
|
|
mock_run.assert_not_called()
|
|
|
|
|
|
class TestSortVersions:
|
|
def test_sort_by_created_at_desc(self) -> None:
|
|
versions = [
|
|
{"version": "0.1.0", "created_at": "2025-01-01T00:00:00Z"},
|
|
{"version": "0.3.0", "created_at": "2025-03-01T00:00:00Z"},
|
|
{"version": "0.2.0", "created_at": "2025-02-01T00:00:00Z"},
|
|
]
|
|
result = sort_versions_by_date(versions)
|
|
assert [v["version"] for v in result] == ["0.3.0", "0.2.0", "0.1.0"]
|
|
|
|
def test_sort_fallback_to_version(self) -> None:
|
|
versions = [
|
|
{"version": "0.1.0"},
|
|
{"version": "0.3.0"},
|
|
{"version": "0.2.0"},
|
|
]
|
|
result = sort_versions_by_date(versions)
|
|
assert [v["version"] for v in result] == ["0.3.0", "0.2.0", "0.1.0"]
|
|
|
|
|
|
class TestSelectForDeletion:
|
|
def test_keep_2(self) -> None:
|
|
versions = [
|
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
|
{"version": "0.3.0", "created_at": "2025-03-01"},
|
|
{"version": "0.4.0", "created_at": "2025-04-01"},
|
|
]
|
|
to_delete = select_for_deletion(versions, keep=2)
|
|
assert len(to_delete) == 2
|
|
assert {v["version"] for v in to_delete} == {"0.1.0", "0.2.0"}
|
|
|
|
def test_preserve_latest_tag(self) -> None:
|
|
versions = [
|
|
{"version": "latest", "created_at": "2025-01-01"},
|
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
|
{"version": "0.3.0", "created_at": "2025-03-01"},
|
|
{"version": "0.4.0", "created_at": "2025-04-01"},
|
|
]
|
|
to_delete = select_for_deletion(versions, keep=2)
|
|
deleted_versions = {v["version"] for v in to_delete}
|
|
assert "latest" not in deleted_versions
|
|
# latest is oldest by date but still preserved
|
|
assert "0.2.0" in deleted_versions
|
|
|
|
def test_keep_all(self) -> None:
|
|
versions = [
|
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
|
]
|
|
to_delete = select_for_deletion(versions, keep=2)
|
|
assert len(to_delete) == 0
|
|
|
|
def test_keep_more_than_available(self) -> None:
|
|
versions = [
|
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
|
]
|
|
to_delete = select_for_deletion(versions, keep=5)
|
|
assert len(to_delete) == 0
|
|
|
|
|
|
class TestCleanImagesAPI:
|
|
"""Tests for the clean_images module's API functions."""
|
|
|
|
def test_list_package_versions(self) -> None:
|
|
from devx.tools.clean_images import list_package_versions
|
|
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = [{"version": "0.1.0"}]
|
|
mock_resp.raise_for_status = MagicMock()
|
|
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp) as mock_get:
|
|
versions = list_package_versions(
|
|
"https://git.example.com/api/v1",
|
|
"oblachno-oss",
|
|
"ci-base",
|
|
"token",
|
|
)
|
|
assert versions == [{"version": "0.1.0"}]
|
|
assert "page=1" in mock_get.call_args.args[0]
|
|
|
|
def test_list_package_versions_pagination(self) -> None:
|
|
from devx.tools.clean_images import list_package_versions
|
|
|
|
# First page: 50 items, second page: 3 items, third page: empty
|
|
page1 = [{"version": f"0.{i}.0"} for i in range(50)]
|
|
page2 = [{"version": f"1.{i}.0"} for i in range(3)]
|
|
responses = [
|
|
MagicMock(json=MagicMock(return_value=page1), raise_for_status=MagicMock()),
|
|
MagicMock(json=MagicMock(return_value=page2), raise_for_status=MagicMock()),
|
|
MagicMock(json=MagicMock(return_value=[]), raise_for_status=MagicMock()),
|
|
]
|
|
with patch("devx.tools.clean_images.requests.get", side_effect=responses):
|
|
versions = list_package_versions(
|
|
"https://git.example.com/api/v1",
|
|
"oblachno-oss",
|
|
"ci-base",
|
|
"token",
|
|
)
|
|
assert len(versions) == 53
|
|
|
|
def test_delete_package_version_success(self) -> None:
|
|
from devx.tools.clean_images import delete_package_version
|
|
|
|
mock_resp = MagicMock(status_code=204)
|
|
with patch("devx.tools.clean_images.requests.delete", return_value=mock_resp):
|
|
assert (
|
|
delete_package_version(
|
|
"https://git.example.com/api/v1",
|
|
"oblachno-oss",
|
|
"ci-base",
|
|
"0.1.0",
|
|
"token",
|
|
)
|
|
is True
|
|
)
|
|
|
|
def test_delete_package_version_failure(self) -> None:
|
|
from devx.tools.clean_images import delete_package_version
|
|
|
|
mock_resp = MagicMock(status_code=404)
|
|
with patch("devx.tools.clean_images.requests.delete", return_value=mock_resp):
|
|
assert (
|
|
delete_package_version(
|
|
"https://git.example.com/api/v1",
|
|
"oblachno-oss",
|
|
"ci-base",
|
|
"0.1.0",
|
|
"token",
|
|
)
|
|
is False
|
|
)
|
|
|
|
|
|
class TestCLIBuildImage:
|
|
def test_single_image_build(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
runner = CliRunner()
|
|
mock_result = MagicMock(returncode=0)
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
|
result = runner.invoke(
|
|
build_image.main,
|
|
["--dockerfile", str(dockerfile), "--name", "ci-base", "--tag", "latest"],
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
def test_manifest_build(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
manifest = tmp_path / "images.json"
|
|
manifest.write_text(
|
|
json.dumps(
|
|
[
|
|
{"name": "ci-base", "dockerfile": str(dockerfile)},
|
|
]
|
|
)
|
|
)
|
|
runner = CliRunner()
|
|
mock_result = MagicMock(returncode=0)
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
|
result = runner.invoke(
|
|
build_image.main,
|
|
["--manifest", str(manifest)],
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
def test_missing_dockerfile_and_manifest(self) -> None:
|
|
runner = CliRunner()
|
|
result = runner.invoke(build_image.main, [])
|
|
assert result.exit_code != 0
|
|
assert "manifest" in result.output.lower() or "dockerfile" in result.output.lower()
|
|
|
|
def test_push_without_registry(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
build_image.main,
|
|
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "registry" in result.output.lower()
|
|
|
|
def test_push_without_credentials(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
runner = CliRunner()
|
|
with patch.dict("os.environ", {}, clear=True):
|
|
result = runner.invoke(
|
|
build_image.main,
|
|
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push", "--registry", "git.example.com"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "credential" in result.output.lower() or "token" in result.output.lower()
|
|
|
|
def test_dry_run(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
runner = CliRunner()
|
|
with patch("devx.tools.build_image.subprocess.run") as mock_run:
|
|
result = runner.invoke(
|
|
build_image.main,
|
|
["--dockerfile", str(dockerfile), "--name", "ci-base", "--dry-run"],
|
|
)
|
|
assert result.exit_code == 0
|
|
mock_run.assert_not_called()
|
|
assert "dry-run" in result.output
|
|
|
|
def test_build_failure_exits_with_error(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
runner = CliRunner()
|
|
mock_result = MagicMock(returncode=1)
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=mock_result):
|
|
result = runner.invoke(
|
|
build_image.main,
|
|
["--dockerfile", str(dockerfile), "--name", "ci-base"],
|
|
)
|
|
assert result.exit_code != 0
|
|
|
|
def test_push_login_failure(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
runner = CliRunner()
|
|
login_result = MagicMock(returncode=1, stderr="auth failed", stdout="")
|
|
with patch.dict("os.environ", {"REPO_TOKEN": "fake", "REGISTRY_USERNAME": "user"}):
|
|
with patch("devx.tools.build_image.subprocess.run", return_value=login_result):
|
|
result = runner.invoke(
|
|
build_image.main,
|
|
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push", "--registry", "git.example.com"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "login" in result.output.lower()
|
|
|
|
def test_push_image_failure(self, tmp_path: Path) -> None:
|
|
dockerfile = tmp_path / "Dockerfile"
|
|
dockerfile.touch()
|
|
runner = CliRunner()
|
|
build_result = MagicMock(returncode=0)
|
|
login_result = MagicMock(returncode=0, stderr="", stdout="")
|
|
push_result = MagicMock(returncode=1, stderr="push failed", stdout="")
|
|
with patch.dict("os.environ", {"REPO_TOKEN": "fake", "REGISTRY_USERNAME": "user"}):
|
|
with patch(
|
|
"devx.tools.build_image.subprocess.run",
|
|
side_effect=[login_result, build_result, push_result],
|
|
):
|
|
result = runner.invoke(
|
|
build_image.main,
|
|
["--dockerfile", str(dockerfile), "--name", "ci-base", "--push", "--registry", "git.example.com"],
|
|
)
|
|
assert result.exit_code != 0
|
|
|
|
|
|
class TestCLICleanImages:
|
|
def test_dry_run(self) -> None:
|
|
from devx.tools.clean_images import main as clean_main
|
|
|
|
runner = CliRunner()
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = [
|
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
|
{"version": "0.3.0", "created_at": "2025-03-01"},
|
|
]
|
|
mock_resp.raise_for_status = MagicMock()
|
|
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
|
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp):
|
|
result = runner.invoke(
|
|
clean_main,
|
|
["--owner", "oblachno-oss", "--name", "ci-base", "--keep", "1", "--dry-run"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "dry-run" in result.output
|
|
assert "0.1.0" in result.output
|
|
|
|
def test_no_token(self) -> None:
|
|
from devx.tools.clean_images import main as clean_main
|
|
|
|
runner = CliRunner()
|
|
with patch.dict("os.environ", {}, clear=True):
|
|
result = runner.invoke(
|
|
clean_main,
|
|
["--owner", "oblachno-oss", "--name", "ci-base"],
|
|
)
|
|
assert result.exit_code != 0
|
|
assert "token" in result.output.lower()
|
|
|
|
def test_no_versions_found(self) -> None:
|
|
from devx.tools.clean_images import main as clean_main
|
|
|
|
runner = CliRunner()
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = []
|
|
mock_resp.raise_for_status = MagicMock()
|
|
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
|
with patch("devx.tools.clean_images.requests.get", return_value=mock_resp):
|
|
result = runner.invoke(
|
|
clean_main,
|
|
["--owner", "oblachno-oss", "--name", "ci-base", "--dry-run"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "No versions" in result.output
|
|
|
|
def test_actual_delete(self) -> None:
|
|
from devx.tools.clean_images import main as clean_main
|
|
|
|
runner = CliRunner()
|
|
list_resp = MagicMock()
|
|
list_resp.json.return_value = [
|
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
|
{"version": "0.3.0", "created_at": "2025-03-01"},
|
|
]
|
|
list_resp.raise_for_status = MagicMock()
|
|
delete_resp = MagicMock(status_code=204)
|
|
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
|
with patch("devx.tools.clean_images.requests.get", return_value=list_resp):
|
|
with patch("devx.tools.clean_images.requests.delete", return_value=delete_resp):
|
|
result = runner.invoke(
|
|
clean_main,
|
|
["--owner", "oblachno-oss", "--name", "ci-base", "--keep", "2"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "Deleted" in result.output
|
|
|
|
def test_list_request_exception(self) -> None:
|
|
import requests as req
|
|
|
|
from devx.tools.clean_images import main as clean_main
|
|
|
|
runner = CliRunner()
|
|
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
|
with patch(
|
|
"devx.tools.clean_images.requests.get",
|
|
side_effect=req.ConnectionError("network down"),
|
|
):
|
|
result = runner.invoke(
|
|
clean_main,
|
|
["--owner", "oblachno-oss", "--name", "ci-base", "--dry-run"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "Failed to list" in result.output
|
|
|
|
def test_delete_failure_in_cli(self) -> None:
|
|
from devx.tools.clean_images import main as clean_main
|
|
|
|
runner = CliRunner()
|
|
list_resp = MagicMock()
|
|
list_resp.json.return_value = [
|
|
{"version": "0.1.0", "created_at": "2025-01-01"},
|
|
{"version": "0.2.0", "created_at": "2025-02-01"},
|
|
{"version": "0.3.0", "created_at": "2025-03-01"},
|
|
]
|
|
list_resp.raise_for_status = MagicMock()
|
|
delete_resp = MagicMock(status_code=500)
|
|
with patch.dict("os.environ", {"REPO_TOKEN": "fake"}):
|
|
with patch("devx.tools.clean_images.requests.get", return_value=list_resp):
|
|
with patch("devx.tools.clean_images.requests.delete", return_value=delete_resp):
|
|
result = runner.invoke(
|
|
clean_main,
|
|
["--owner", "oblachno-oss", "--name", "ci-base", "--keep", "2"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "FAILED" in result.output
|