96de2d2573
Manage Telegram instances through Kubernetes with legacy adoption for forust and anna. Build and deploy the panel image alongside the runtime.
180 lines
6.1 KiB
Python
180 lines
6.1 KiB
Python
from datetime import UTC, datetime
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from app.config import Settings
|
|
from app.errors import PanelError
|
|
from app.kubernetes_service import KubernetesService
|
|
from app.models import AccountBase
|
|
from kubernetes.client.exceptions import ApiException
|
|
|
|
|
|
def service() -> KubernetesService:
|
|
core = Mock()
|
|
apps = Mock()
|
|
custom = Mock()
|
|
custom.get_namespaced_custom_object.side_effect = ApiException(status=404)
|
|
return KubernetesService(Settings(), core=core, apps=apps, custom=custom)
|
|
|
|
|
|
def account() -> AccountBase:
|
|
return AccountBase(
|
|
instance_id="test-account",
|
|
display_name="Test Account",
|
|
api_id=12345,
|
|
api_hash="0123456789abcdef0123456789abcdef",
|
|
)
|
|
|
|
|
|
def test_renders_managed_resources_without_leaking_credentials() -> None:
|
|
kube = service()
|
|
names = kube._resource_names("test-account")
|
|
secret = kube._secret(account(), "SESSION", names)
|
|
pvc = kube._pvc(account(), names)
|
|
deployment = kube._deployment(account(), names)
|
|
|
|
assert secret.string_data == {
|
|
"API_ID": "12345",
|
|
"API_HASH": "0123456789abcdef0123456789abcdef",
|
|
"STRINGSESSION": "SESSION",
|
|
}
|
|
assert pvc.spec.storage_class_name == "local-path-retain"
|
|
assert pvc.spec.resources.requests["storage"] == "1Gi"
|
|
assert deployment.spec.strategy.type == "Recreate"
|
|
assert deployment.spec.replicas == 1
|
|
assert deployment.spec.template.spec.automount_service_account_token is False
|
|
assert deployment.spec.template.spec.service_account_name == "userbot-runtime"
|
|
assert deployment.spec.template.spec.volumes[1].host_path.path.endswith("/Downloads")
|
|
assert deployment.spec.template.spec.containers[0].resources.requests == {
|
|
"cpu": "80m",
|
|
"memory": "512Mi",
|
|
}
|
|
|
|
|
|
def test_collision_is_reported_before_auth() -> None:
|
|
kube = service()
|
|
kube.apps.read_namespaced_deployment.return_value = object()
|
|
|
|
with pytest.raises(PanelError) as error:
|
|
kube.assert_available("test-account")
|
|
|
|
assert error.value.status_code == 409
|
|
assert "Deployment" in error.value.detail
|
|
|
|
|
|
def test_partial_provision_rolls_back_only_created_resources() -> None:
|
|
kube = service()
|
|
kube.ensure_prerequisites = Mock()
|
|
kube.assert_available = Mock()
|
|
kube.core.create_namespaced_persistent_volume_claim.side_effect = ApiException(status=500)
|
|
|
|
with pytest.raises(PanelError):
|
|
kube.provision(account(), "SESSION")
|
|
|
|
kube.core.delete_namespaced_secret.assert_called_once_with(
|
|
"userbot-test-account-credentials",
|
|
"userbot",
|
|
)
|
|
kube.core.delete_namespaced_persistent_volume_claim.assert_not_called()
|
|
kube.apps.delete_namespaced_deployment.assert_not_called()
|
|
|
|
|
|
def test_delete_retains_pvc_unless_explicitly_requested() -> None:
|
|
kube = service()
|
|
deployment = SimpleNamespace(
|
|
metadata=SimpleNamespace(
|
|
name="userbot-test-account",
|
|
annotations={
|
|
"userbot.forust.xyz/credentials-secret": "credentials",
|
|
"userbot.forust.xyz/pvc": "data",
|
|
},
|
|
)
|
|
)
|
|
kube._find_deployment = Mock(return_value=("userbot", deployment))
|
|
|
|
kube.delete("test-account", delete_data=False)
|
|
|
|
kube.apps.delete_namespaced_deployment.assert_called_once()
|
|
kube.core.delete_namespaced_secret.assert_called_once_with("credentials", "userbot")
|
|
kube.core.delete_namespaced_persistent_volume_claim.assert_not_called()
|
|
|
|
kube.delete("test-account", delete_data=True)
|
|
kube.core.delete_namespaced_persistent_volume_claim.assert_called_once_with(
|
|
"data",
|
|
"userbot",
|
|
)
|
|
|
|
|
|
def test_legacy_delete_is_blocked() -> None:
|
|
kube = service()
|
|
deployment = SimpleNamespace(
|
|
metadata=SimpleNamespace(
|
|
name="forust-userbot-deployment",
|
|
annotations={"userbot.forust.xyz/legacy": "true"},
|
|
)
|
|
)
|
|
kube._find_deployment = Mock(return_value=("default", deployment))
|
|
|
|
with pytest.raises(PanelError) as error:
|
|
kube.delete("forust", delete_data=False)
|
|
|
|
assert error.value.status_code == 409
|
|
|
|
|
|
def pod(*, ready: bool, phase: str = "Running", reason: str | None = None):
|
|
waiting = SimpleNamespace(reason=reason) if reason else None
|
|
state = SimpleNamespace(waiting=waiting, terminated=None)
|
|
status = SimpleNamespace(
|
|
container_statuses=[
|
|
SimpleNamespace(ready=ready, restart_count=2, state=state),
|
|
],
|
|
phase=phase,
|
|
start_time=datetime.now(UTC),
|
|
)
|
|
return SimpleNamespace(
|
|
metadata=SimpleNamespace(name="pod-1", creation_timestamp=datetime.now(UTC)),
|
|
status=status,
|
|
)
|
|
|
|
|
|
def deployment(replicas: int = 1):
|
|
resources = SimpleNamespace(limits={"cpu": "300m", "memory": "1536Mi"})
|
|
container = SimpleNamespace(image="userbot:latest", resources=resources)
|
|
template_spec = SimpleNamespace(containers=[container], volumes=[])
|
|
return SimpleNamespace(
|
|
metadata=SimpleNamespace(
|
|
name="userbot-test-account",
|
|
labels={"app.kubernetes.io/instance": "test-account"},
|
|
annotations={},
|
|
creation_timestamp=datetime.now(UTC),
|
|
),
|
|
spec=SimpleNamespace(
|
|
replicas=replicas,
|
|
selector=SimpleNamespace(match_labels={"app": "test"}),
|
|
template=SimpleNamespace(spec=template_spec),
|
|
),
|
|
status=SimpleNamespace(available_replicas=1 if replicas else 0),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("replicas", "pod_value", "expected"),
|
|
[
|
|
(0, None, "stopped"),
|
|
(1, pod(ready=True), "running"),
|
|
(1, pod(ready=False, phase="Pending"), "pending"),
|
|
(1, pod(ready=False, reason="CrashLoopBackOff"), "error"),
|
|
(1, pod(ready=False, phase="Failed"), "error"),
|
|
],
|
|
)
|
|
def test_status_classification(replicas, pod_value, expected) -> None:
|
|
kube = service()
|
|
kube._pods_for_deployment = Mock(return_value=[pod_value] if pod_value else [])
|
|
kube._pvc_storage = Mock(return_value=None)
|
|
kube._pod_metrics = Mock(return_value=(None, None))
|
|
|
|
result = kube._summarize("userbot", deployment(replicas))
|
|
|
|
assert result.status == expected
|