2026-07-02 03:25:27 +08:00
|
|
|
"""Integration tests for channels health_router endpoints."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from .conftest import BASE_URL, DEFAULT_CHANNEL_TYPE, NON_EXISTENT_ACCOUNT_ID
|
|
|
|
|
|
|
|
|
|
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
|
|
|
|
|
|
|
|
|
HEALTH_URL = f"{BASE_URL}/health"
|
2026-07-07 16:27:06 +08:00
|
|
|
SINGLE_HEALTH_URL = f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/{NON_EXISTENT_ACCOUNT_ID}/health"
|
2026-07-02 03:25:27 +08:00
|
|
|
PROBE_URL = f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/{NON_EXISTENT_ACCOUNT_ID}/probe"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
2026-07-04 00:18:04 +08:00
|
|
|
# === GET /health (NO AUTH — wrapped in success/data) ===
|
2026-07-02 03:25:27 +08:00
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_health_does_not_require_auth(test_client: httpx.AsyncClient):
|
|
|
|
|
# Act — no Authorization header
|
|
|
|
|
response = await test_client.get(HEALTH_URL)
|
2026-07-04 00:18:04 +08:00
|
|
|
# Assert — 200 with success/data wrapper; data holds status/version/...
|
2026-07-02 03:25:27 +08:00
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
payload = response.json()
|
2026-07-04 00:18:04 +08:00
|
|
|
assert payload["success"] is True
|
|
|
|
|
data = payload["data"]
|
|
|
|
|
assert "status" in data
|
|
|
|
|
assert "version" in data
|
|
|
|
|
assert "degraded_components" in data
|
|
|
|
|
assert "channels" in data
|
2026-07-02 03:25:27 +08:00
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_get_health_works_with_standard_user(test_client: httpx.AsyncClient, standard_user):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act — standard user headers also accepted (no auth required)
|
|
|
|
|
response = await test_client.get(HEALTH_URL, headers=standard_user["headers"])
|
|
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
payload = response.json()
|
2026-07-04 00:18:04 +08:00
|
|
|
assert payload["success"] is True
|
|
|
|
|
data = payload["data"]
|
|
|
|
|
assert "status" in data
|
|
|
|
|
assert "version" in data
|
|
|
|
|
assert "degraded_components" in data
|
|
|
|
|
assert "channels" in data
|
2026-07-02 03:25:27 +08:00
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_get_health_not_captured_by_channel_type_path(test_client: httpx.AsyncClient):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act — /health must hit the static health endpoint, not be captured as
|
|
|
|
|
# channel_type="health" by the dynamic /{channel_type}/{account_id}/... path
|
|
|
|
|
response = await test_client.get(HEALTH_URL)
|
2026-07-04 00:18:04 +08:00
|
|
|
# Assert — success/data wrapper confirms correct route match
|
2026-07-02 03:25:27 +08:00
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
payload = response.json()
|
2026-07-04 00:18:04 +08:00
|
|
|
assert payload["success"] is True
|
|
|
|
|
data = payload["data"]
|
|
|
|
|
assert "status" in data
|
|
|
|
|
assert "version" in data
|
|
|
|
|
assert "channels" in data
|
2026-07-02 03:25:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# === GET /health/detail (admin auth matrix) ===
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_health_detail_requires_auth(test_client: httpx.AsyncClient):
|
|
|
|
|
# Act
|
|
|
|
|
response = await test_client.get(f"{HEALTH_URL}/detail")
|
|
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 401, response.text
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_get_health_detail_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act
|
2026-07-07 16:27:06 +08:00
|
|
|
response = await test_client.get(f"{HEALTH_URL}/detail", headers=standard_user["headers"])
|
2026-07-02 03:25:27 +08:00
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 403, response.text
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_admin_can_get_health_detail(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act
|
|
|
|
|
response = await test_client.get(f"{HEALTH_URL}/detail", headers=admin_headers)
|
|
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
payload = response.json()
|
|
|
|
|
assert payload["success"] is True
|
|
|
|
|
assert "data" in payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# === POST /health/diagnostics/export (admin auth matrix) ===
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_export_diagnostics_requires_auth(test_client: httpx.AsyncClient):
|
|
|
|
|
# Act
|
2026-07-07 16:27:06 +08:00
|
|
|
response = await test_client.post(f"{HEALTH_URL}/diagnostics/export", json={})
|
2026-07-02 03:25:27 +08:00
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 401, response.text
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_export_diagnostics_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
f"{HEALTH_URL}/diagnostics/export",
|
|
|
|
|
json={},
|
|
|
|
|
headers=standard_user["headers"],
|
|
|
|
|
)
|
|
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 403, response.text
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_admin_can_export_diagnostics_with_empty_body(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Arrange — all fields optional, empty dict is valid
|
|
|
|
|
body: dict = {}
|
|
|
|
|
# Act
|
2026-07-07 16:27:06 +08:00
|
|
|
response = await test_client.post(f"{HEALTH_URL}/diagnostics/export", json=body, headers=admin_headers)
|
2026-07-02 03:25:27 +08:00
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
payload = response.json()
|
|
|
|
|
assert payload["success"] is True
|
|
|
|
|
assert "data" in payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# === POST /health/diagnostics/export audit_log_count validation ===
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_export_diagnostics_rejects_audit_log_count_zero(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act — audit_log_count=0 violates ge=1
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
f"{HEALTH_URL}/diagnostics/export",
|
|
|
|
|
json={"audit_log_count": 0},
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
)
|
|
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_export_diagnostics_rejects_audit_log_count_over_max(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act — audit_log_count=501 violates le=500
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
f"{HEALTH_URL}/diagnostics/export",
|
|
|
|
|
json={"audit_log_count": 501},
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
)
|
|
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# === Single-channel health (non-existent account -> 404 or 501) ===
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_get_single_health_nonexistent_account(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act
|
|
|
|
|
response = await test_client.get(SINGLE_HEALTH_URL, headers=admin_headers)
|
|
|
|
|
# Assert — non-existent account: 404 (not found) or 501 (adapter not registered)
|
|
|
|
|
assert response.status_code in (404, 501), response.text
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_probe_channel_nonexistent_account(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Arrange — valid probe body with defaults
|
|
|
|
|
body = {"probe_type": "connectivity", "timeout_ms": 5000}
|
|
|
|
|
# Act
|
2026-07-07 16:27:06 +08:00
|
|
|
response = await test_client.post(PROBE_URL, json=body, headers=admin_headers)
|
2026-07-02 03:25:27 +08:00
|
|
|
# Assert — non-existent account: 404 or 501
|
|
|
|
|
assert response.status_code in (404, 501), response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# === POST /probe timeout_ms validation ===
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_probe_rejects_timeout_below_min(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act — timeout_ms=999 violates ge=1000
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
PROBE_URL,
|
|
|
|
|
json={"timeout_ms": 999},
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
)
|
|
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 422, response.text
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 16:27:06 +08:00
|
|
|
async def test_probe_rejects_timeout_above_max(test_client: httpx.AsyncClient, admin_headers):
|
2026-07-02 03:25:27 +08:00
|
|
|
# Act — timeout_ms=30001 violates le=30000
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
PROBE_URL,
|
|
|
|
|
json={"timeout_ms": 30001},
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
)
|
|
|
|
|
# Assert
|
|
|
|
|
assert response.status_code == 422, response.text
|