218 lines
8.5 KiB
Python
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)
|