Files
gpu-rent/tests/test_create_errors.py
Leonid PershinandCursor 0bad693b12 Enhance Selectel balance handling and error messaging in GPU rental system
- Updated Selectel documentation to clarify error responses related to balance issues.
- Introduced `peek_balance_rub` function for best-effort balance checks, handling API failures gracefully.
- Improved error messages for insufficient funds and quota issues, specifying Selectel's 403 policy response.
- Added balance checks in the doctor command to ensure users are informed about their balance status before attempting GPU creation.
- Refactored exception handling in cloud operations to provide clearer feedback on balance-related errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-23 17:54:11 +03:00

112 lines
3.6 KiB
Python

from types import SimpleNamespace
import pytest
from gpu_rent.cloud import (
_exc_brief,
_http_status,
_is_create_policy_deny,
_wrap,
create_gpu_server,
)
from gpu_rent.errors import CloudError
POLICY_403 = (
"ForbiddenException: 403: Client Error for url: "
"https://ru-6.cloud.api.selcloud.ru/compute/v2.1/servers, "
"Policy doesn't allow os_compute_api:servers:create to be performed."
)
class Forbidden(Exception):
def __init__(self, msg: str, http_status: int = 403):
super().__init__(msg)
self.http_status = http_status
def test_http_status_from_attr_and_text():
assert _http_status(Forbidden(POLICY_403)) == 403
assert _http_status(RuntimeError("no status")) is None
def test_is_create_policy_deny():
assert _is_create_policy_deny(Forbidden(POLICY_403))
assert not _is_create_policy_deny(RuntimeError("quota exceeded for cores"))
def test_exc_brief_strips_url_and_user_data():
dump = (
"BadRequestException: 400: Invalid input for field/attribute user_data. "
"Value: #cloud-config\nssh_authorized_keys:\n"
" - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFake gpu-rent\n"
)
brief = _exc_brief(RuntimeError(dump))
assert "ssh-ed25519" not in brief
assert "AAAAC3" not in brief
assert "http" not in brief.lower() or "https://" not in brief
def test_wrap_403_create_is_funds_not_gpu_quota():
err = _wrap(Forbidden(POLICY_403), "create server")
msg = str(err)
assert isinstance(err, CloudError)
assert "GPU=0" not in msg
assert "тикет" not in msg
assert "ssh-ed25519" not in msg
assert "https://" not in msg
assert "Пополни" in msg or "нет средств" in msg or "кончились деньги" in msg
def test_wrap_403_create_with_zero_balance(monkeypatch):
monkeypatch.setattr("gpu_rent.balance.peek_balance_rub", lambda *_a, **_k: 0.0)
msg = str(_wrap(Forbidden(POLICY_403), "create server", api_token="tok"))
assert "0 ₽" in msg
assert "нет средств" in msg
assert "Пополни" in msg
assert "GPU=0" not in msg
def test_wrap_403_create_with_healthy_balance_is_iam(monkeypatch):
monkeypatch.setattr("gpu_rent.balance.peek_balance_rub", lambda *_a, **_k: 1500.0)
msg = str(_wrap(Forbidden(POLICY_403), "create server", api_token="tok"))
assert "1500 ₽" in msg
assert "IAM" in msg
assert "GPU=0" not in msg
def test_wrap_quota_message_only_when_quota_in_error():
exc = RuntimeError("403 OverLimit: quota exceeded for instances")
msg = str(_wrap(exc, "create server"))
assert "квота" in msg.lower()
def test_create_gpu_server_stops_on_first_403():
calls: list[int] = []
class Compute:
def create_server(self, **kw):
calls.append(1)
raise Forbidden(POLICY_403)
logs: list[str] = []
with pytest.raises(CloudError) as caught:
create_gpu_server(
SimpleNamespace(compute=Compute()),
flavor_id="1",
net_id="n",
sg_name="g",
boot_volume_id="b",
data_volume_id="d",
az="ru-6a",
spot=True,
public_key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFake gpu-rent",
log=logs.append,
)
assert len(calls) == 1
assert logs == []
msg = str(caught.value)
assert "GPU=0" not in msg
assert "тикет" not in msg
assert "ssh-ed25519" not in msg
assert "Пополни" in msg or "нет средств" in msg or "кончились деньги" in msg