Systematic rename of all semantic anchors (#region, [DEF], @RELATION) across 1400+ files — backend Python, frontend Svelte/TS, specs, docs: - Flat anchors become Namespace.Module.Entity - @RELATION references updated to match new anchor paths - Zero business logic changes
256 lines
12 KiB
Python
256 lines
12 KiB
Python
# #region Test.SupersetSqllabApiIntegration.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 -> [Plugin.SupersetExecutor.SupersetSqlLabExecutor]
|
||
# @RELATION BINDS_TO -> [Core.Init.SupersetClient]
|
||
# @RELATION BINDS_TO -> [Core.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 Test.SupersetSqllabApiIntegration.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.SupersetSqllabApiIntegration.TestJwtLoginReturnsToken [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.SupersetSqllabApiIntegration.TestJwtLoginReturnsToken
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestJwtLoginWrongPassword [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.SupersetSqllabApiIntegration.TestJwtLoginWrongPassword
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestHealthStillWorks [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.SupersetSqllabApiIntegration.TestHealthStillWorks
|
||
|
||
|
||
# #endregion Test.SupersetSqllabApiIntegration.TestSupersetJwtHealthCheck
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.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.SupersetSqllabApiIntegration.TestListDatabasesWithJwt [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.SupersetSqllabApiIntegration.TestListDatabasesWithJwt
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestListDashboardsWithJwt [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.SupersetSqllabApiIntegration.TestListDashboardsWithJwt
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestSqllabExecuteWithJwt [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.SupersetSqllabApiIntegration.TestSqllabExecuteWithJwt
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestSqllabWithoutDatabaseId [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.SupersetSqllabApiIntegration.TestSqllabWithoutDatabaseId
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestGetNonexistentDatabase [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.SupersetSqllabApiIntegration.TestGetNonexistentDatabase
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestMeEndpoint [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.SupersetSqllabApiIntegration.TestMeEndpoint
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestDatabaseColumnsWithJwt [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.SupersetSqllabApiIntegration.TestDatabaseColumnsWithJwt
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestUnauthenticatedApiRejected [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.SupersetSqllabApiIntegration.TestUnauthenticatedApiRejected
|
||
|
||
|
||
# #endregion Test.SupersetSqllabApiIntegration.TestSupersetApiWithJwt
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.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.SupersetSqllabApiIntegration.TestSupersetClientAuthenticated [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.SupersetSqllabApiIntegration.TestSupersetClientAuthenticated
|
||
|
||
|
||
# #region Test.SupersetSqllabApiIntegration.TestSupersetClientUserViaMeEndpoint [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.SupersetSqllabApiIntegration.TestSupersetClientUserViaMeEndpoint
|
||
|
||
|
||
# #endregion Test.SupersetSqllabApiIntegration.TestSupersetJwtIntegration
|
||
|
||
|
||
# #endregion Test.SupersetSqllabApiIntegration.TestSupersetSqllabApiJwt
|