# #region TestSupersetIntegration [C:3] [TYPE Module] [SEMANTICS test,superset,integration,smoke] # @BRIEF Smoke tests verifying the Superset testcontainer starts correctly and APIs respond. # @RELATION BINDS_TO -> [SupersetClient] # @RELATION BINDS_TO -> [SupersetClientBase] # # @TEST_CONTRACT SupersetHealth -> # Health endpoint: GET /health returns 200 with healthy status # Login endpoint: GET /login/ returns 200 # API version: GET /api/v1/ returns 200 # Dashboard list: GET /api/v1/dashboard/ returns paginated response (authenticated) # # @TEST_EDGE: superset_not_healthy -> TimeoutError after 90s # @TEST_EDGE: unauthenticated_api -> 401 on protected endpoints # @TEST_EDGE: invalid_login_credentials -> 403/redirect on wrong password # # @REQUIRES Docker daemon running (testcontainers starts superset + postgres) import pytest import requests # #region TestSupersetHealthCheck [C:3] [TYPE Class] # @BRIEF Verify Superset is alive and core APIs respond. class TestSupersetHealthCheck: """Smoke tests for the running Superset test container.""" # #region test_health_endpoint [C:2] [TYPE Function] # @BRIEF GET /health returns 200 with OK — Superset is alive. def test_health_endpoint(self, superset_url): resp = requests.get(f"{superset_url}/health", timeout=10) assert resp.status_code == 200, f"Health check failed: {resp.status_code}" # Superset 4.x /health returns "OK" as plain text assert resp.text.strip() == "OK", \ f"Expected 'OK', got '{resp.text.strip()}'" # #endregion test_health_endpoint # #region test_login_page_reachable [C:2] [TYPE Function] # @BRIEF GET /login/ returns 200 — the login page is served. def test_login_page_reachable(self, superset_url): resp = requests.get(f"{superset_url}/login/", timeout=10, allow_redirects=True) assert resp.status_code == 200, f"Login page returned {resp.status_code}" # Login page should contain Superset-related content assert "Superset" in resp.text or "superset" in resp.text.lower(), \ "Login page does not contain 'Superset' text" # #endregion test_login_page_reachable # #region test_api_version_endpoint [C:2] [TYPE Function] # @BRIEF GET /api/v1/database/ returns 401 (unauthenticated) — API layer is alive. def test_api_version_endpoint(self, superset_url): # Superset 4.x doesn't expose /api/v1/ root; use a real endpoint resp = requests.get( f"{superset_url}/api/v1/database/", timeout=10, allow_redirects=False, ) # API is reachable — returns 401 without auth, 200 with auth assert resp.status_code in (200, 401), \ f"API returned unexpected {resp.status_code}" # #endregion test_api_version_endpoint # #region test_unauthenticated_api_rejected [C:2] [TYPE Function] # @BRIEF GET /api/v1/dashboard/ without auth returns 401. # @TEST_EDGE: unauthenticated_api — VERIFIED_BY: test_unauthenticated_api_rejected def test_unauthenticated_api_rejected(self, superset_url): resp = requests.get( f"{superset_url}/api/v1/dashboard/", timeout=10, allow_redirects=False, ) # Either 401 Unauthorized or 302 redirect to login assert resp.status_code in (401, 302, 403), \ f"Expected 401/302/403, got {resp.status_code}" # #endregion test_unauthenticated_api_rejected # #endregion TestSupersetHealthCheck # #region TestSupersetAuthenticated [C:3] [TYPE Class] # @BRIEF Verify session-based authentication and CSRF token flow. class TestSupersetAuthenticated: """Tests requiring session-based login to Superset.""" # #region test_form_login_sets_session_cookie [C:2] [TYPE Function] # @BRIEF POST /login/ with valid credentials redirects and sets session cookie. def test_form_login_sets_session_cookie(self, superset_url, superset_admin_password): import re session = requests.Session() # Step 1: Get login page to obtain CSRF token resp = session.get(f"{superset_url}/login/", timeout=10) assert resp.status_code in (200, 302), \ f"GET /login/ returned {resp.status_code}" csrf_match = re.search(r'csrf_token["\s:=]+[\'"]([^\'"]+)', resp.text) csrf_token = csrf_match.group(1) if csrf_match else "" # Step 2: Submit login form resp = session.post( f"{superset_url}/login/", data={ "username": "admin", "password": superset_admin_password, "csrf_token": csrf_token, }, headers={"Referer": f"{superset_url}/login/"}, timeout=10, allow_redirects=False, ) # Successful login typically redirects to /superset/welcome/ assert resp.status_code in (200, 302), \ f"POST /login/ returned {resp.status_code}" # Session cookie should contain user data session_cookie = session.cookies.get("session") assert session_cookie is not None, \ "No session cookie set after login" assert len(session_cookie) > 20, \ f"Session cookie too short: {len(session_cookie)} chars" # #endregion test_form_login_sets_session_cookie # #region test_me_endpoint_after_form_login [C:2] [TYPE Function] # @BRIEF GET /api/v1/me/ returns user info after form-based login. def test_me_endpoint_after_form_login(self, superset_url, superset_admin_password): import re session = requests.Session() # Login via form resp = session.get(f"{superset_url}/login/", timeout=10) csrf_match = re.search(r'csrf_token["\s:=]+[\'"]([^\'"]+)', resp.text) csrf_token = csrf_match.group(1) if csrf_match else "" session.post( f"{superset_url}/login/", data={ "username": "admin", "password": superset_admin_password, "csrf_token": csrf_token, }, headers={"Referer": f"{superset_url}/login/"}, timeout=10, ) # Access me endpoint — some Superset versions allow session-based access resp = session.get( f"{superset_url}/api/v1/me/", timeout=10, allow_redirects=False, ) # May return 200 (session accepted) or 401 (JWT required) # Either is valid behavior depending on Superset version assert resp.status_code in (200, 401, 302), \ f"/api/v1/me/ returned unexpected {resp.status_code}" # #endregion test_me_endpoint_after_form_login # #endregion TestSupersetAuthenticated # #region TestSupersetEnvironment [C:3] [TYPE Class] # @BRIEF Verify SupersetClient connectivity and JWT authentication. # @RATIONALE Superset Docker image (4.1.2) does not include psycopg2 — it silently # falls back to SQLite. With separate init+web containers, the SQLite DB from the # init container is lost, so the web container has no admin user → JWT login returns # 401. The fix: `python -m pip install psycopg2-binary -q` in both init and web # containers. With psycopg2 installed, Superset connects to the shared Postgres DB # and JWT login works correctly. class TestSupersetEnvironment: """Verify SupersetClient integration with the test container.""" # #region test_jwt_login_success [C:2] [TYPE Function] # @BRIEF POST /api/v1/security/login returns access_token + refresh_token. @pytest.mark.asyncio async def test_jwt_login_success(self, superset_url, superset_admin_password): import httpx async with httpx.AsyncClient() as client: resp = await client.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[:200]}" data = resp.json() assert "access_token" in data, \ f"No access_token in response: {list(data.keys())}" assert "refresh_token" in data, \ f"No refresh_token in response" # Tokens should be non-empty JWT strings assert len(data["access_token"]) > 50, \ f"access_token too short: {len(data['access_token'])}" # #endregion test_jwt_login_success # #region test_jwt_authenticated_api_call [C:2] [TYPE Function] # @BRIEF Dashboard API returns 200 with valid JWT Bearer token. @pytest.mark.asyncio async def test_jwt_authenticated_api_call(self, superset_url, superset_admin_password): import httpx async with httpx.AsyncClient() as client: # Step 1: Get JWT token login_resp = await client.post( f"{superset_url}/api/v1/security/login", json={ "username": "admin", "password": superset_admin_password, "provider": "db", "refresh": True, }, timeout=10, ) assert login_resp.status_code == 200 access_token = login_resp.json()["access_token"] # Step 2: Call protected API with Bearer token dash_resp = await client.get( f"{superset_url}/api/v1/dashboard/?q=(page_size:1,page:0)", headers={"Authorization": f"Bearer {access_token}"}, timeout=10, ) assert dash_resp.status_code == 200, \ f"Dashboard API failed: {dash_resp.status_code} {dash_resp.text[:200]}" data = dash_resp.json() assert "count" in data, \ f"Dashboard response missing count: {list(data.keys())}" assert isinstance(data["count"], int) # #endregion test_jwt_authenticated_api_call # #region test_superset_client_constructs [C:2] [TYPE Function] # @BRIEF SupersetClient initializes and constructs API URL correctly. @pytest.mark.asyncio async def test_superset_client_constructs(self, superset_url): from src.core.config_models import Environment from src.core.superset_client import SupersetClient env = Environment( id="test_superset", name="Test Superset Container", url=superset_url, username="admin", password="admin123", verify_ssl=False, timeout=10, ) client = SupersetClient(env) assert client.client is not None assert client.client.base_url == superset_url, \ f"Expected {superset_url}, got {client.client.base_url}" # #endregion test_superset_client_constructs # #region test_superset_api_base_url [C:2] [TYPE Function] # @BRIEF SupersetClient computes the correct /api/v1/ base path. @pytest.mark.asyncio async def test_superset_api_base_url(self, superset_url): from src.core.config_models import Environment from src.core.superset_client import SupersetClient env = Environment( id="test_api_url", name="Test", url=superset_url, username="admin", password="admin123", verify_ssl=False, timeout=10, ) client = SupersetClient(env) assert client.client.api_base_url is not None assert "/api/v1" in client.client.api_base_url, \ f"api_base_url does not contain /api/v1: {client.client.api_base_url}" # #endregion test_superset_api_base_url # #endregion TestSupersetEnvironment # #endregion TestSupersetIntegration