Some checks failed
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 33s
Deploy Trade-In / test (push) Failing after 1m32s
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / deploy (push) Has been skipped
199 lines
6.8 KiB
Python
199 lines
6.8 KiB
Python
"""Offline-тесты admin proxy-pool ручек (#2161).
|
|
|
|
Покрытие (db мокается, NO live network/DB):
|
|
- POST /api/v1/admin/proxies/bulk — UPSERT-счётчики, валидация affinity/kind
|
|
- GET /api/v1/admin/proxies — маскировка пароля, фильтры
|
|
- PATCH /api/v1/admin/proxies/{id} — enable/disable, 404
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture
|
|
def db() -> MagicMock:
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def client(db: MagicMock) -> TestClient:
|
|
from app.api.v1 import admin as admin_module
|
|
from app.core.db import get_db
|
|
|
|
app = FastAPI()
|
|
app.include_router(admin_module.router, prefix="/api/v1/admin")
|
|
|
|
def fake_db() -> Any:
|
|
yield db
|
|
|
|
app.dependency_overrides[get_db] = fake_db
|
|
return TestClient(app)
|
|
|
|
|
|
def _scalar_result(value: object) -> MagicMock:
|
|
res = MagicMock()
|
|
res.scalar.return_value = value
|
|
return res
|
|
|
|
|
|
# ── _mask_proxy_url unit ─────────────────────────────────────────────────────
|
|
|
|
|
|
def test_mask_proxy_url_hides_password() -> None:
|
|
from app.api.v1.admin import _mask_proxy_url
|
|
|
|
assert _mask_proxy_url("socks5://user:secret@host:1080") == "socks5://user:***@host:1080"
|
|
assert _mask_proxy_url("http://u:p@1.2.3.4:8080") == "http://u:***@1.2.3.4:8080"
|
|
|
|
|
|
def test_mask_proxy_url_no_password_untouched() -> None:
|
|
from app.api.v1.admin import _mask_proxy_url
|
|
|
|
assert _mask_proxy_url("http://host:8080") == "http://host:8080"
|
|
assert _mask_proxy_url(None) is None
|
|
assert _mask_proxy_url("") == ""
|
|
|
|
|
|
# ── POST /proxies/bulk ───────────────────────────────────────────────────────
|
|
|
|
|
|
def test_bulk_upsert_counts_inserted_and_updated(client: TestClient, db: MagicMock) -> None:
|
|
# первая строка вставлена (xmax=0 → True), вторая обновлена (False)
|
|
db.execute.side_effect = [_scalar_result(True), _scalar_result(False)]
|
|
r = client.post(
|
|
"/api/v1/admin/proxies/bulk",
|
|
json={
|
|
"proxies": [
|
|
{"url": "http://u:p@host1:8080", "provider_affinity": "avito"},
|
|
{"url": "http://u:p@host2:8080", "provider_affinity": "cian", "kind": "socks5"},
|
|
]
|
|
},
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
assert r.json() == {"inserted": 1, "updated": 1}
|
|
assert db.commit.called
|
|
|
|
|
|
def test_bulk_invalid_affinity_422(client: TestClient) -> None:
|
|
r = client.post(
|
|
"/api/v1/admin/proxies/bulk",
|
|
json={"proxies": [{"url": "http://host:8080", "provider_affinity": "garbage"}]},
|
|
)
|
|
assert r.status_code == 422
|
|
|
|
|
|
def test_bulk_invalid_kind_422(client: TestClient) -> None:
|
|
r = client.post(
|
|
"/api/v1/admin/proxies/bulk",
|
|
json={"proxies": [{"url": "http://host:8080", "kind": "ftp"}]},
|
|
)
|
|
assert r.status_code == 422
|
|
|
|
|
|
def test_bulk_empty_list_422(client: TestClient) -> None:
|
|
r = client.post("/api/v1/admin/proxies/bulk", json={"proxies": []})
|
|
assert r.status_code == 422
|
|
|
|
|
|
def test_bulk_defaults_affinity_any(client: TestClient, db: MagicMock) -> None:
|
|
db.execute.side_effect = [_scalar_result(True)]
|
|
r = client.post(
|
|
"/api/v1/admin/proxies/bulk",
|
|
json={"proxies": [{"url": "http://host:8080"}]},
|
|
)
|
|
assert r.status_code == 200
|
|
# affinity по умолчанию 'any' попал в параметры INSERT
|
|
_, kwargs = db.execute.call_args
|
|
assert kwargs is not None
|
|
params = db.execute.call_args.args[1]
|
|
assert params["aff"] == "any"
|
|
assert params["kind"] == "http"
|
|
|
|
|
|
# ── GET /proxies ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _proxy_db_row(**over: Any) -> dict[str, Any]:
|
|
base: dict[str, Any] = {
|
|
"id": 1,
|
|
"label": "test",
|
|
"url": "socks5://user:secret@host:1080",
|
|
"kind": "socks5",
|
|
"provider_affinity": "avito",
|
|
"rotate_url": "http://user:secret@rot:9000/changeip",
|
|
"enabled": True,
|
|
"consecutive_fails": 0,
|
|
"exit_ip": "1.2.3.4",
|
|
"latency_ms": 120,
|
|
"last_check_at": datetime(2026, 7, 1, tzinfo=UTC),
|
|
"last_ok_at": datetime(2026, 7, 1, tzinfo=UTC),
|
|
"leased_by": None,
|
|
"leased_at": None,
|
|
"geo": "RU",
|
|
"operator": "beeline",
|
|
"expires_at": None,
|
|
"created_at": datetime(2026, 6, 1, tzinfo=UTC),
|
|
"updated_at": datetime(2026, 7, 1, tzinfo=UTC),
|
|
}
|
|
base.update(over)
|
|
return base
|
|
|
|
|
|
def test_list_masks_password(client: TestClient, db: MagicMock) -> None:
|
|
result = MagicMock()
|
|
result.mappings.return_value.all.return_value = [_proxy_db_row()]
|
|
db.execute.return_value = result
|
|
|
|
r = client.get("/api/v1/admin/proxies")
|
|
assert r.status_code == 200, r.text
|
|
rows = r.json()
|
|
assert len(rows) == 1
|
|
assert rows[0]["url"] == "socks5://user:***@host:1080"
|
|
assert rows[0]["rotate_url"] == "http://user:***@rot:9000/changeip"
|
|
assert "secret" not in r.text
|
|
|
|
|
|
def test_list_passes_filters(client: TestClient, db: MagicMock) -> None:
|
|
result = MagicMock()
|
|
result.mappings.return_value.all.return_value = []
|
|
db.execute.return_value = result
|
|
|
|
r = client.get("/api/v1/admin/proxies", params={"provider": "cian", "enabled": "false"})
|
|
assert r.status_code == 200
|
|
params = db.execute.call_args.args[1]
|
|
assert params["provider"] == "cian"
|
|
assert params["enabled"] is False
|
|
|
|
|
|
# ── PATCH /proxies/{id} ──────────────────────────────────────────────────────
|
|
|
|
|
|
def test_patch_disable(client: TestClient, db: MagicMock) -> None:
|
|
result = MagicMock()
|
|
result.mappings.return_value.fetchone.return_value = _proxy_db_row(enabled=False)
|
|
db.execute.return_value = result
|
|
|
|
r = client.patch("/api/v1/admin/proxies/1", json={"enabled": False})
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["enabled"] is False
|
|
assert db.commit.called
|
|
|
|
|
|
def test_patch_not_found_404(client: TestClient, db: MagicMock) -> None:
|
|
result = MagicMock()
|
|
result.mappings.return_value.fetchone.return_value = None
|
|
db.execute.return_value = result
|
|
|
|
r = client.patch("/api/v1/admin/proxies/999", json={"enabled": True})
|
|
assert r.status_code == 404
|