Public Access
DEVX-68: feat: add pre-built Docker runner images and tested image build/push tools
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
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
This commit was merged in pull request #108.
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user