Files
grm/scripts/tests/test_cleanup_stale_runners.py
T
emilandemo c72dc97f63
Post-merge / detect-and-configure (push) Successful in 1m8s
Post-merge / release-and-maintain (push) Successful in 1m9s
GRM-158: feat: use Gitea mirror for Ansible collection installs
Co-authored-by: emil User <emil.simeonov@tutanota.com>
2026-08-08 23:36:41 +00:00

218 lines
8.5 KiB
Python

"""Tests for cleanup_stale_runners.py."""
from __future__ import annotations
import time
from unittest.mock import MagicMock, patch
from scripts.cleanup_stale_runners import (
_api_request,
delete_runner,
list_runners,
main,
)
class TestListRunners:
"""Tests for list_runners()."""
@patch("scripts.cleanup_stale_runners._api_request")
def test_returns_list_of_runners(self, mock_req: MagicMock) -> None:
mock_req.return_value = [{"id": 1, "name": "runner-1"}, {"id": 2, "name": "runner-2"}]
result = list_runners("https://git.example.com", "token")
assert len(result) == 2
assert result[0]["id"] == 1
@patch("scripts.cleanup_stale_runners._api_request")
def test_returns_empty_on_none(self, mock_req: MagicMock) -> None:
mock_req.return_value = None
result = list_runners("https://git.example.com", "token")
assert result == []
@patch("scripts.cleanup_stale_runners._api_request")
def test_extracts_runners_from_dict(self, mock_req: MagicMock) -> None:
mock_req.return_value = {"runners": [{"id": 1}]}
result = list_runners("https://git.example.com", "token")
assert len(result) == 1
assert result[0]["id"] == 1
@patch("scripts.cleanup_stale_runners._api_request")
def test_returns_empty_on_non_list_non_dict(self, mock_req: MagicMock) -> None:
mock_req.return_value = "not a list"
result = list_runners("https://git.example.com", "token")
assert result == []
class TestDeleteRunner:
"""Tests for delete_runner()."""
@patch("scripts.cleanup_stale_runners._api_request")
def test_returns_true_on_success(self, mock_req: MagicMock) -> None:
mock_req.return_value = None
assert delete_runner("https://git.example.com", "token", 42) is True
@patch("scripts.cleanup_stale_runners._api_request")
def test_returns_false_on_error(self, mock_req: MagicMock) -> None:
mock_req.side_effect = RuntimeError("API error 404: not found")
assert delete_runner("https://git.example.com", "token", 42) is False
class TestApiRequest:
"""Tests for _api_request()."""
@patch("scripts.cleanup_stale_runners.urllib.request.urlopen")
def test_returns_json_on_success(self, mock_urlopen: MagicMock) -> None:
mock_resp = MagicMock()
mock_resp.status = 200
mock_resp.read.return_value = b'{"key": "value"}'
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = _api_request("https://git.example.com", "token", "GET", "/test")
assert result == {"key": "value"}
@patch("scripts.cleanup_stale_runners.urllib.request.urlopen")
def test_returns_none_on_204(self, mock_urlopen: MagicMock) -> None:
mock_resp = MagicMock()
mock_resp.status = 204
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = _api_request("https://git.example.com", "token", "DELETE", "/test/1")
assert result is None
@patch("scripts.cleanup_stale_runners.urllib.request.urlopen")
def test_returns_none_on_empty_body(self, mock_urlopen: MagicMock) -> None:
mock_resp = MagicMock()
mock_resp.status = 200
mock_resp.read.return_value = b""
mock_urlopen.return_value.__enter__.return_value = mock_resp
result = _api_request("https://git.example.com", "token", "GET", "/test")
assert result is None
@patch("scripts.cleanup_stale_runners.urllib.request.urlopen")
def test_raises_on_http_error(self, mock_urlopen: MagicMock) -> None:
import urllib.error
mock_error = urllib.error.HTTPError(
"url",
404,
"Not Found",
{},
None,
)
mock_error.read = MagicMock(return_value=b'{"message": "not found"}')
mock_urlopen.side_effect = mock_error
import pytest
with pytest.raises(RuntimeError, match="404"):
_api_request("https://git.example.com", "token", "GET", "/test")
class TestMain:
"""Tests for main()."""
@patch("scripts.cleanup_stale_runners.list_runners")
def test_no_runners(self, mock_list: MagicMock) -> None:
mock_list.return_value = []
rc = main(["--gitea-url", "https://git.example.com", "--token", "t"])
assert rc == 0
@patch("scripts.cleanup_stale_runners.list_runners")
def test_no_stale_runners(self, mock_list: MagicMock) -> None:
now = int(time.time())
mock_list.return_value = [
{"id": 1, "name": "runner-1", "last_online": now - 60},
]
rc = main(["--gitea-url", "https://git.example.com", "--token", "t"])
assert rc == 0
@patch("scripts.cleanup_stale_runners.list_runners")
def test_dry_run_does_not_delete(self, mock_list: MagicMock) -> None:
now = int(time.time())
mock_list.return_value = [
{"id": 1, "name": "runner-1", "last_online": now - 7200},
]
with patch("scripts.cleanup_stale_runners.delete_runner") as mock_del:
rc = main(
[
"--gitea-url",
"https://git.example.com",
"--token",
"t",
"--dry-run",
]
)
assert rc == 0
mock_del.assert_not_called()
@patch("scripts.cleanup_stale_runners.list_runners")
@patch("scripts.cleanup_stale_runners.delete_runner")
def test_deletes_stale_runners(self, mock_del: MagicMock, mock_list: MagicMock) -> None:
now = int(time.time())
mock_list.return_value = [
{"id": 1, "name": "runner-1", "last_online": now - 60},
{"id": 2, "name": "runner-2", "last_online": now - 7200},
{"id": 3, "name": "runner-3", "last_online": now - 9999},
]
mock_del.return_value = True
rc = main(
[
"--gitea-url",
"https://git.example.com",
"--token",
"t",
"--stale-threshold",
"3600",
]
)
assert rc == 0
assert mock_del.call_count == 2
@patch("scripts.cleanup_stale_runners.list_runners")
@patch("scripts.cleanup_stale_runners.delete_runner")
def test_returns_1_on_partial_failure(self, mock_del: MagicMock, mock_list: MagicMock) -> None:
now = int(time.time())
mock_list.return_value = [
{"id": 1, "name": "runner-1", "last_online": now - 7200},
{"id": 2, "name": "runner-2", "last_online": now - 7200},
]
mock_del.side_effect = [True, False]
rc = main(["--gitea-url", "https://git.example.com", "--token", "t"])
assert rc == 1
@patch("scripts.cleanup_stale_runners.list_runners")
def test_runner_with_zero_last_online(self, mock_list: MagicMock) -> None:
"""Runners with last_online=0 should be considered stale."""
mock_list.return_value = [
{"id": 1, "name": "runner-1", "last_online": 0},
]
with patch("scripts.cleanup_stale_runners.delete_runner") as mock_del:
mock_del.return_value = True
rc = main(["--gitea-url", "https://git.example.com", "--token", "t"])
assert rc == 0
mock_del.assert_called_once()
@patch("scripts.cleanup_stale_runners.list_runners")
def test_runner_with_missing_last_online(self, mock_list: MagicMock) -> None:
"""Runners with missing last_online should be considered stale."""
mock_list.return_value = [
{"id": 1, "name": "runner-1"},
]
with patch("scripts.cleanup_stale_runners.delete_runner") as mock_del:
mock_del.return_value = True
rc = main(["--gitea-url", "https://git.example.com", "--token", "t"])
assert rc == 0
mock_del.assert_called_once()
@patch("scripts.cleanup_stale_runners.list_runners")
@patch("scripts.cleanup_stale_runners.delete_runner")
def test_skips_runner_with_none_id(self, mock_del: MagicMock, mock_list: MagicMock) -> None:
"""Runners with id=None should be skipped during deletion."""
now = int(time.time())
mock_list.return_value = [
{"id": None, "name": "bad-runner", "last_online": now - 7200},
{"id": 2, "name": "runner-2", "last_online": now - 7200},
]
mock_del.return_value = True
rc = main(["--gitea-url", "https://git.example.com", "--token", "t"])
# 1/2 deleted (None id skipped), so rc=1 (partial)
assert rc == 1
mock_del.assert_called_once_with("https://git.example.com", "t", 2)