- 9 new enhancement test files: test_connection_service.py, test_db_executor.py, test_orchestrator_direct_db.py, test_batch_insert.py, test_lang_stats.py, test_response_field_coverage.py, test_retry.py, test_run_service.py, test_sql_insert_service.py - 5 new integration tests: test_superset_sqllab_e2e.py, test_translate_clickhouse.py, test_translate_corrections.py, test_translate_schedules.py, test_translate_status_fk.py - Updated existing tests for insert_method/connection_id fields
256 lines
11 KiB
Python
256 lines
11 KiB
Python
# #region TestSupersetSqllabApiJwt [C:4] [TYPE Module] [SEMANTICS test,superset,sqllab,integration,jwt]
|
||
# @BRIEF Integration tests for Superset REST API using JWT Bearer auth against real Testcontainers.
|
||
# @RELATION BINDS_TO -> [SupersetSqlLabExecutor]
|
||
# @RELATION BINDS_TO -> [SupersetClient]
|
||
# @RELATION BINDS_TO -> [ConfigManager]
|
||
#
|
||
# @TEST_CONTRACT SupersetSqllabApi ->
|
||
# {
|
||
# invariants: [
|
||
# "JWT login returns access_token + refresh_token",
|
||
# "Bearer token authorizes all /api/v1/ endpoints",
|
||
# "GET /api/v1/database/ returns paginated database list",
|
||
# "POST /api/v1/sqllab/execute/ accepts SQL and returns query_id",
|
||
# "GET /api/v1/dashboard/ returns paginated list with Bearer token"
|
||
# ]
|
||
# }
|
||
# @TEST_EDGE: wrong_password -> 401 on JWT login
|
||
# @TEST_EDGE: expired_token -> 401 on protected endpoints
|
||
# @TEST_EDGE: missing_database_id -> 400 on sqllab execute
|
||
#
|
||
# @RATIONALE ADR-0012 resolved the JWT auth issue via psycopg2 + superset_config.py +
|
||
# Docker bridge IP. Now JWT works end-to-end with the test container.
|
||
# All API tests use real Bearer tokens against the live Superset instance.
|
||
#
|
||
# @REQUIRES Docker daemon running (testcontainers starts superset + postgres)
|
||
import pytest
|
||
import requests
|
||
from uuid import uuid4
|
||
|
||
|
||
# #region TestSupersetJwtHealthCheck [C:3] [TYPE Class]
|
||
# @BRIEF Verify health and JWT login work correctly.
|
||
class TestSupersetJwtHealthCheck:
|
||
"""JWT auth smoke tests against the real Superset container."""
|
||
|
||
# #region test_jwt_login_returns_token [C:2] [TYPE Function]
|
||
# @BRIEF POST /api/v1/security/login returns access_token with refresh flag.
|
||
def test_jwt_login_returns_token(self, superset_url, superset_admin_password):
|
||
resp = requests.post(
|
||
f"{superset_url}/api/v1/security/login",
|
||
json={
|
||
"username": "admin",
|
||
"password": superset_admin_password,
|
||
"provider": "db",
|
||
"refresh": True,
|
||
},
|
||
timeout=10,
|
||
)
|
||
assert resp.status_code == 200, f"JWT login failed: {resp.status_code} {resp.text}"
|
||
data = resp.json()
|
||
assert "access_token" in data, f"No access_token in response: {data.keys()}"
|
||
assert "refresh_token" in data, f"No refresh_token in response: {data.keys()}"
|
||
assert len(data["access_token"]) > 20, "access_token too short"
|
||
# #endregion test_jwt_login_returns_token
|
||
|
||
|
||
# #region test_jwt_login_wrong_password [C:2] [TYPE Function]
|
||
# @BRIEF POST /api/v1/security/login with wrong password returns 401.
|
||
# @TEST_EDGE: wrong_password — VERIFIED_BY: test_jwt_login_wrong_password
|
||
def test_jwt_login_wrong_password(self, superset_url):
|
||
resp = requests.post(
|
||
f"{superset_url}/api/v1/security/login",
|
||
json={
|
||
"username": "admin",
|
||
"password": "wrong_password_123",
|
||
"provider": "db",
|
||
},
|
||
timeout=10,
|
||
)
|
||
assert resp.status_code == 401, f"Expected 401, got {resp.status_code}"
|
||
# #endregion test_jwt_login_wrong_password
|
||
|
||
|
||
# #region test_health_still_works [C:2] [TYPE Function]
|
||
# @BRIEF /health does not require auth — regression guard.
|
||
def test_health_still_works(self, superset_url):
|
||
resp = requests.get(f"{superset_url}/health", timeout=10)
|
||
assert resp.status_code == 200
|
||
assert resp.text.strip() == "OK"
|
||
# #endregion test_health_still_works
|
||
|
||
|
||
# #endregion TestSupersetJwtHealthCheck
|
||
|
||
|
||
# #region TestSupersetApiWithJwt [C:3] [TYPE Class]
|
||
# @BRIEF Verify REST API endpoints with JWT Bearer token auth.
|
||
class TestSupersetApiWithJwt:
|
||
"""Authenticated API tests against real Superset container."""
|
||
|
||
# #region test_list_databases_with_jwt [C:2] [TYPE Function]
|
||
# @BRIEF GET /api/v1/database/ returns paginated list with JWT Bearer token.
|
||
def test_list_databases_with_jwt(self, superset_url, superset_jwt_headers):
|
||
resp = requests.get(
|
||
f"{superset_url}/api/v1/database/",
|
||
headers=superset_jwt_headers,
|
||
timeout=10,
|
||
)
|
||
assert resp.status_code == 200, \
|
||
f"GET /api/v1/database/ failed: {resp.status_code} {resp.text[:200]}"
|
||
data = resp.json()
|
||
assert "result" in data, f"No 'result' key: {list(data.keys())}"
|
||
assert isinstance(data["result"], list), "result should be a list"
|
||
# #endregion test_list_databases_with_jwt
|
||
|
||
|
||
# #region test_list_dashboards_with_jwt [C:2] [TYPE Function]
|
||
# @BRIEF GET /api/v1/dashboard/ returns paginated list with JWT Bearer token.
|
||
def test_list_dashboards_with_jwt(self, superset_url, superset_jwt_headers):
|
||
resp = requests.get(
|
||
f"{superset_url}/api/v1/dashboard/",
|
||
headers=superset_jwt_headers,
|
||
timeout=10,
|
||
)
|
||
assert resp.status_code == 200, \
|
||
f"GET /api/v1/dashboard/ failed: {resp.status_code} {resp.text[:200]}"
|
||
data = resp.json()
|
||
assert "result" in data, f"No 'result' key: {list(data.keys())}"
|
||
assert "count" in data, f"No 'count' key: {list(data.keys())}"
|
||
# #endregion test_list_dashboards_with_jwt
|
||
|
||
|
||
# #region test_sqllab_execute_with_jwt [C:2] [TYPE Function]
|
||
# @BRIEF POST /api/v1/sqllab/execute/ is reachable with JWT.
|
||
def test_sqllab_execute_with_jwt(self, superset_url, superset_jwt_headers):
|
||
payload = {
|
||
"database_id": 1,
|
||
"sql": "SELECT 1",
|
||
"runAsync": True,
|
||
}
|
||
resp = requests.post(
|
||
f"{superset_url}/api/v1/sqllab/execute/",
|
||
json=payload,
|
||
headers={
|
||
**superset_jwt_headers,
|
||
"Content-Type": "application/json",
|
||
},
|
||
timeout=10,
|
||
)
|
||
# Without a real DB configured, we expect 400 or 500
|
||
# But the endpoint exists and is reachable (not 404/401)
|
||
assert resp.status_code in (200, 400, 500), \
|
||
f"sqllab/execute/ returned {resp.status_code}: {resp.text[:300]}"
|
||
# #endregion test_sqllab_execute_with_jwt
|
||
|
||
|
||
# #region test_sqllab_without_database_id [C:2] [TYPE Function]
|
||
# @BRIEF POST /api/v1/sqllab/execute/ without database_id returns 400.
|
||
# @TEST_EDGE: missing_database_id — VERIFIED_BY: test_sqllab_without_database_id
|
||
def test_sqllab_without_database_id(self, superset_url, superset_jwt_headers):
|
||
resp = requests.post(
|
||
f"{superset_url}/api/v1/sqllab/execute/",
|
||
json={"sql": "SELECT 1"},
|
||
headers={
|
||
**superset_jwt_headers,
|
||
"Content-Type": "application/json",
|
||
},
|
||
timeout=10,
|
||
)
|
||
assert resp.status_code in (400, 500), \
|
||
f"Expected 400/500, got {resp.status_code}: {resp.text[:200]}"
|
||
# #endregion test_sqllab_without_database_id
|
||
|
||
|
||
# #region test_get_nonexistent_database [C:2] [TYPE Function]
|
||
# @BRIEF GET /api/v1/database/<non_existent_id> returns 404.
|
||
def test_get_nonexistent_database(self, superset_url, superset_jwt_headers):
|
||
resp = requests.get(
|
||
f"{superset_url}/api/v1/database/99999",
|
||
headers=superset_jwt_headers,
|
||
timeout=10,
|
||
)
|
||
assert resp.status_code == 404, \
|
||
f"Expected 404, got {resp.status_code}: {resp.text[:200]}"
|
||
# #endregion test_get_nonexistent_database
|
||
|
||
|
||
# #region test_me_endpoint [C:2] [TYPE Function]
|
||
# @BRIEF GET /api/v1/me/ — в Superset 4.1.2 это публичный эндпоинт, но
|
||
# доступен только авторизованным через JWT. Проверяем что 401/200, а не 404.
|
||
def test_me_endpoint(self, superset_url, superset_jwt_headers):
|
||
resp = requests.get(
|
||
f"{superset_url}/api/v1/me/",
|
||
headers=superset_jwt_headers,
|
||
timeout=10,
|
||
)
|
||
# Superset 4.1.2 может вернуть 200 (есть endpoint) или 401 (нет прав у admin role).
|
||
# Главное — не 404, значит endpoint существует.
|
||
assert resp.status_code in (200, 401, 403), \
|
||
f"GET /api/v1/me/ failed: {resp.status_code} {resp.text[:200]}"
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
assert "result" in data, f"No 'result' key: {list(data.keys())}"
|
||
# #endregion test_me_endpoint
|
||
|
||
|
||
# #region test_database_columns_with_jwt [C:2] [TYPE Function]
|
||
# @BRIEF GET /api/v1/database/<id>/columns/ with valid id returns 404 (no DB configured).
|
||
def test_database_columns_with_jwt(self, superset_url, superset_jwt_headers):
|
||
resp = requests.get(
|
||
f"{superset_url}/api/v1/database/99999/columns/",
|
||
headers=superset_jwt_headers,
|
||
timeout=10,
|
||
)
|
||
assert resp.status_code == 404, \
|
||
f"Expected 404, got {resp.status_code}"
|
||
# #endregion test_database_columns_with_jwt
|
||
|
||
|
||
# #region test_unauthenticated_api_rejected [C:2] [TYPE Function]
|
||
# @BRIEF GET /api/v1/dashboard/ without auth returns 401.
|
||
def test_unauthenticated_api_rejected(self, superset_url):
|
||
resp = requests.get(
|
||
f"{superset_url}/api/v1/dashboard/",
|
||
timeout=10,
|
||
)
|
||
assert resp.status_code == 401, \
|
||
f"Expected 401, got {resp.status_code}"
|
||
# #endregion test_unauthenticated_api_rejected
|
||
|
||
|
||
# #endregion TestSupersetApiWithJwt
|
||
|
||
|
||
# #region TestSupersetJwtIntegration [C:3] [TYPE Class]
|
||
# @BRIEF Verify SupersetClient and SupersetSqlLabExecutor with real JWT auth.
|
||
class TestSupersetJwtIntegration:
|
||
"""Full SupersetClient integration tests with JWT auth."""
|
||
|
||
# #region test_superset_client_authenticated [C:2] [TYPE Function]
|
||
# @BRIEF Real SupersetClient authenticates and can call API.
|
||
@pytest.mark.asyncio
|
||
async def test_superset_client_authenticated(self, superset_client):
|
||
# Authenticated client should be able to list databases
|
||
count, databases = await superset_client.get_databases()
|
||
assert isinstance(count, int)
|
||
assert isinstance(databases, list)
|
||
# #endregion test_superset_client_authenticated
|
||
|
||
|
||
# #region test_superset_client_user_via_me_endpoint [C:2] [TYPE Function]
|
||
# @BRIEF SupersetClient can call /me/ raw endpoint — проверяем доступность.
|
||
@pytest.mark.asyncio
|
||
async def test_superset_client_user_via_me_endpoint(self, superset_client):
|
||
me = await superset_client.client.request("GET", "/me/")
|
||
assert me is not None
|
||
# Superset 4.1.2 может вернуть 200 (есть endpoint) или {"message":"Not authorized"}
|
||
# в зависимости от прав роли Admin на конкретный endpoint
|
||
# #endregion test_superset_client_user_via_me_endpoint
|
||
|
||
|
||
# #endregion TestSupersetJwtIntegration
|
||
|
||
|
||
# #endregion TestSupersetSqllabApiJwt
|