新增并完善多模块接口测试用例,覆盖: 1. 权限校验:认证缺失、普通用户越权访问场景 2. 参数合法性校验:空值、格式错误、边界值、枚举约束 3. 响应字段增强:补充业务字段断言与分页回传校验 4. 异常场景:不存在资源、超限参数、无效格式等400/404/422场景
576 lines
19 KiB
Python
576 lines
19 KiB
Python
"""Integration tests for channels wizard_router endpoints."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import httpx
|
||
import pytest
|
||
|
||
from .conftest import BASE_URL, DEFAULT_CHANNEL_TYPE
|
||
|
||
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
||
|
||
|
||
def _make_validate_payload() -> dict:
|
||
"""Build a valid ValidateWizardRequest body (step_id 在路径参数中)."""
|
||
return {
|
||
"values": {"app_id": "pytest_app", "app_secret": "pytest_secret"},
|
||
}
|
||
|
||
|
||
def _make_finalize_payload() -> dict:
|
||
"""Build a valid FinalizeWizardRequest body."""
|
||
return {
|
||
"patches": [
|
||
{
|
||
"step_id": "account_info",
|
||
"values": {"app_id": "pytest_app", "app_secret": "pytest_secret"},
|
||
}
|
||
]
|
||
}
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth three-tier for GET /wizard/steps ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_list_wizard_steps_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.get(f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_list_wizard_steps_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps",
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_admin_can_list_wizard_steps_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert — adapter not registered → 404 or 501, or 200 with steps
|
||
assert response.status_code in (200, 404, 501), response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /wizard/steps/{step_id}/validate ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_validate_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
step_id = "account_info"
|
||
body = _make_validate_payload()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps/{step_id}/validate",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code in (200, 404, 501), response.text
|
||
|
||
|
||
async def test_wizard_validate_rejects_empty_step_id(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — step_id 路径参数为空字符串,FastAPI 路由匹配规则下
|
||
# 可能不匹配(404)或匹配到空 step_id 后由下游校验拒绝(400/422)
|
||
body = _make_validate_payload()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps//validate",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert — 空路径段不被接受,返回 4xx 错误
|
||
assert response.status_code in (400, 404, 422), response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /wizard/steps/{step_id}/apply ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_apply_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
step_id = "account_info"
|
||
body = {"values": {"app_id": "pytest_app", "app_secret": "pytest_secret"}}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps/{step_id}/apply",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code in (200, 404, 501), response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /wizard/finalize ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_finalize_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange
|
||
body = _make_finalize_payload()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/finalize",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code in (200, 404, 501), response.text
|
||
|
||
|
||
async def test_wizard_finalize_rejects_empty_patches(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — patches min_length=1
|
||
body = {"patches": []}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/finalize",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /wizard/oauth/initiate ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_oauth_initiate_rejects_invalid_redirect_uri_scheme(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — redirect_uri 必须为 http/https,ftp 等非法 scheme 拒绝
|
||
body = {"redirect_uri": "ftp://example.com/callback"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth/initiate",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_oauth_initiate_with_valid_params_returns_404_or_501_or_400(
|
||
test_client: httpx.AsyncClient, admin_headers
|
||
):
|
||
# Arrange — valid redirect_uri but channel/adapter 未注册
|
||
body = {"redirect_uri": "https://example.com/callback"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth/initiate",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert — non-existent channel / adapter → 404, 501, or 400
|
||
assert response.status_code in (400, 404, 501), response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /wizard/oauth-callback ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_oauth_callback_requires_code(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — code required (Pydantic min_length=1)
|
||
body = {"state": "pytest_state", "redirect_uri": "https://example.com/callback"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth-callback",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_oauth_callback_requires_state(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — state required (Pydantic min_length=1)
|
||
body = {"code": "pytest_code", "redirect_uri": "https://example.com/callback"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth-callback",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_oauth_callback_requires_redirect_uri(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — redirect_uri required
|
||
body = {"code": "pytest_code", "state": "pytest_state"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth-callback",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_oauth_callback_rejects_invalid_redirect_uri_scheme(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — redirect_uri 必须为 http/https,ftp 等非法 scheme 拒绝
|
||
body = {
|
||
"code": "pytest_code",
|
||
"state": "pytest_state",
|
||
"redirect_uri": "ftp://example.com/callback",
|
||
}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth-callback",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_oauth_callback_with_all_params_returns_404_or_501_or_400(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — all params present but channel not registered
|
||
body = {
|
||
"code": "pytest_code",
|
||
"state": "pytest_state",
|
||
"redirect_uri": "https://example.com/callback",
|
||
}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth-callback",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert — non-existent channel / adapter → 404, 501, or 400
|
||
assert response.status_code in (400, 404, 501), response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /wizard/qr/initiate (WIZ-07) ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_qr_initiate_requires_auth(test_client: httpx.AsyncClient):
|
||
# Act
|
||
response = await test_client.post(f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/qr/initiate")
|
||
# Assert
|
||
assert response.status_code == 401
|
||
|
||
|
||
async def test_wizard_qr_initiate_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/qr/initiate",
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403
|
||
|
||
|
||
async def test_wizard_qr_initiate_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers):
|
||
# Act — 账户尚未创建时复用 LoginAdapter 获取二维码,适配器未注册 → 404/501
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/qr/initiate",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code in (200, 404, 501), response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === POST /wizard/qr/wait (WIZ-08) ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_qr_wait_requires_session_id(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — session_id required (Pydantic min_length=1)
|
||
body = {"current_qr_data_url": "https://example.com/qr"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/qr/wait",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_wizard_qr_wait_rejects_empty_session_id(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — session_id min_length=1,空字符串拒绝
|
||
body = {"session_id": ""}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/qr/wait",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
async def test_wizard_qr_wait_returns_200_or_404_or_501(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — 合法 session_id 但扫码会话不存在 / 适配器未注册
|
||
body = {"session_id": "nonexistent_qr_session"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/qr/wait",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code in (200, 404, 501), response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Invalid channel_type validation ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_unregistered_channel_type(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — channel_type 为 str 子类无白名单,未注册插件返回 404
|
||
unregistered_channel = "invalid_channel_type_xyz"
|
||
# Act
|
||
response = await test_client.get(
|
||
f"{BASE_URL}/{unregistered_channel}/wizard/steps",
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 404, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth matrix for POST /wizard/steps/{step_id}/validate ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_validate_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = _make_validate_payload()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps/account_info/validate",
|
||
json=body,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_wizard_validate_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = _make_validate_payload()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps/account_info/validate",
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_wizard_validate_rejects_missing_values(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — values field required
|
||
body = {}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps/account_info/validate",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth matrix for POST /wizard/steps/{step_id}/apply ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_apply_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"values": {"app_id": "pytest_app", "app_secret": "pytest_secret"}}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps/account_info/apply",
|
||
json=body,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_wizard_apply_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {"values": {"app_id": "pytest_app", "app_secret": "pytest_secret"}}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps/account_info/apply",
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_wizard_apply_rejects_missing_values(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — values field required
|
||
body = {}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/steps/account_info/apply",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth matrix for POST /wizard/finalize ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_finalize_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = _make_finalize_payload()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/finalize",
|
||
json=body,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_wizard_finalize_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = _make_finalize_payload()
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/finalize",
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth matrix for POST /wizard/oauth/initiate ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_oauth_initiate_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"redirect_uri": "https://example.com/callback"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth/initiate",
|
||
json=body,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_oauth_initiate_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {"redirect_uri": "https://example.com/callback"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth/initiate",
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
async def test_oauth_initiate_rejects_missing_redirect_uri(test_client: httpx.AsyncClient, admin_headers):
|
||
# Arrange — redirect_uri required
|
||
body = {}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth/initiate",
|
||
json=body,
|
||
headers=admin_headers,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 422, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth matrix for POST /wizard/oauth-callback ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_oauth_callback_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {
|
||
"code": "pytest_code",
|
||
"state": "pytest_state",
|
||
"redirect_uri": "https://example.com/callback",
|
||
}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth-callback",
|
||
json=body,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_oauth_callback_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {
|
||
"code": "pytest_code",
|
||
"state": "pytest_state",
|
||
"redirect_uri": "https://example.com/callback",
|
||
}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/oauth-callback",
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|
||
|
||
|
||
# =============================================================================
|
||
# === Auth matrix for POST /wizard/qr/wait ===
|
||
# =============================================================================
|
||
|
||
|
||
async def test_wizard_qr_wait_requires_auth(test_client: httpx.AsyncClient):
|
||
# Arrange
|
||
body = {"session_id": "nonexistent_qr_session"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/qr/wait",
|
||
json=body,
|
||
)
|
||
# Assert
|
||
assert response.status_code == 401, response.text
|
||
|
||
|
||
async def test_wizard_qr_wait_requires_admin(test_client: httpx.AsyncClient, standard_user):
|
||
# Arrange
|
||
body = {"session_id": "nonexistent_qr_session"}
|
||
# Act
|
||
response = await test_client.post(
|
||
f"{BASE_URL}/{DEFAULT_CHANNEL_TYPE}/wizard/qr/wait",
|
||
json=body,
|
||
headers=standard_user["headers"],
|
||
)
|
||
# Assert
|
||
assert response.status_code == 403, response.text
|