test: 6 agents — +52 test files across core, task_manager, translate routes, git/storage/migration routes, dataset_review deps/routes, settings. Fixed 4 failures

This commit is contained in:
2026-06-15 17:08:08 +03:00
parent c8e44a1b86
commit a18f19064f
40 changed files with 15690 additions and 12 deletions

View File

@@ -0,0 +1,966 @@
# #region Test.AsyncNetwork [C:4] [TYPE Module] [SEMANTICS test, async, network, client, httpx]
# @BRIEF Tests for core/utils/async_network.py — AsyncAPIClient.
# @RELATION BINDS_TO -> [AsyncNetworkModule]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
import asyncio
import json
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
import httpx
import pytest
@pytest.fixture(autouse=True)
def clear_auth_cache():
"""Clear SupersetAuthCache between tests."""
from src.core.utils.network import SupersetAuthCache
SupersetAuthCache._entries.clear()
class TestAsyncAPIClientInit:
"""AsyncAPIClient.__init__: config parsing, ssl handling."""
def test_init_with_defaults(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://superset.example.com", "auth": {"username": "admin", "password": "pass"}})
assert client.base_url == "https://superset.example.com"
assert client.api_base_url == "https://superset.example.com/api/v1"
assert client._authenticated is False
assert client._semaphore is None
def test_init_ssl_true_creates_default_context(self):
import ssl
from src.core.utils.async_network import AsyncAPIClient
real_ctx = ssl.create_default_context()
with patch("ssl.create_default_context", return_value=real_ctx) as mock_ctx:
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, verify_ssl=True)
mock_ctx.assert_called_once()
assert client.base_url == "https://test.com"
def test_init_ssl_false(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, verify_ssl=False)
assert client.base_url == "https://test.com"
assert client._client is not None
def test_init_with_semaphore(self):
from src.core.utils.async_network import AsyncAPIClient
sem = asyncio.Semaphore(5)
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, semaphore=sem)
assert client._semaphore is sem
def test_normalize_base_url_removes_api_v1_suffix(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com/api/v1", "auth": {}})
assert client.base_url == "https://test.com"
def test_normalize_base_url_strips_trailing_slash(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com/", "auth": {}})
assert client.base_url == "https://test.com"
def test_init_with_custom_timeout(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, timeout=60)
assert client.request_settings["timeout"] == 60
def test_init_with_ssl_context(self):
import ssl
ctx = ssl.create_default_context()
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}}, verify_ssl=ctx)
# The client should be initialized with the custom SSL context
assert client.request_settings["verify_ssl"] is ctx
class TestBuildApiUrl:
"""_build_api_url: endpoint -> full URL."""
@pytest.fixture
def client(self):
from src.core.utils.async_network import AsyncAPIClient
return AsyncAPIClient({"base_url": "https://superset.example.com", "auth": {}})
def test_relative_endpoint_adds_api_v1_prefix(self, client):
url = client._build_api_url("/chart/")
assert url == "https://superset.example.com/api/v1/chart/"
def test_relative_endpoint_without_leading_slash(self, client):
url = client._build_api_url("chart/")
assert url == "https://superset.example.com/api/v1/chart/"
def test_absolute_url_passthrough(self, client):
url = client._build_api_url("https://other.com/api/v1/chart/")
assert url == "https://other.com/api/v1/chart/"
def test_already_has_api_v1(self, client):
url = client._build_api_url("/api/v1/chart/")
assert url == "https://superset.example.com/api/v1/chart/"
def test_api_v1_root(self, client):
url = client._build_api_url("/api/v1")
assert url == "https://superset.example.com/api/v1"
def test_empty_endpoint_returns_base(self, client):
url = client._build_api_url("")
assert url == "https://superset.example.com/api/v1/"
class TestAuthLock:
"""_get_auth_lock: class-level async lock storage."""
def test_returns_same_lock_for_same_key(self):
from src.core.utils.async_network import AsyncAPIClient
lock1 = AsyncAPIClient._get_auth_lock(("url", "user", True))
lock2 = AsyncAPIClient._get_auth_lock(("url", "user", True))
assert lock1 is lock2
def test_different_keys_return_different_locks(self):
from src.core.utils.async_network import AsyncAPIClient
lock1 = AsyncAPIClient._get_auth_lock(("url1", "user", True))
lock2 = AsyncAPIClient._get_auth_lock(("url2", "user", True))
assert lock1 is not lock2
class TestAuthenticate:
"""authenticate(): cache hit, cache miss, error paths."""
@pytest.fixture
def client(self):
from src.core.utils.async_network import AsyncAPIClient
c = AsyncAPIClient({"base_url": "https://superset.example.com", "auth": {"username": "admin", "password": "pass"}})
c._client = AsyncMock()
return c
@pytest.mark.asyncio
async def test_cache_hit_skips_login(self, client):
from src.core.utils.network import SupersetAuthCache
SupersetAuthCache.set(
client._auth_cache_key,
{"access_token": "cached_access", "csrf_token": "cached_csrf"},
)
# Mock CSRF refresh
mock_csrf_resp = MagicMock()
mock_csrf_resp.is_success = True
mock_csrf_resp.json.return_value = {"result": "new_csrf"}
client._client.get = AsyncMock(return_value=mock_csrf_resp)
tokens = await client.authenticate()
assert tokens["access_token"] == "cached_access"
assert client._authenticated is True
client._client.post.assert_not_called()
@pytest.mark.asyncio
async def test_cache_miss_fresh_login(self, client):
mock_login_resp = MagicMock()
mock_login_resp.status_code = 200
mock_login_resp.json.return_value = {"access_token": "new_access"}
mock_login_resp.raise_for_status = MagicMock()
mock_csrf_resp = MagicMock()
mock_csrf_resp.status_code = 200
mock_csrf_resp.json.return_value = {"result": "new_csrf"}
mock_csrf_resp.raise_for_status = MagicMock()
client._client.post = AsyncMock(return_value=mock_login_resp)
client._client.get = AsyncMock(return_value=mock_csrf_resp)
tokens = await client.authenticate()
assert tokens["access_token"] == "new_access"
assert tokens["csrf_token"] == "new_csrf"
assert client._authenticated is True
@pytest.mark.asyncio
async def test_login_http_502_raises_network_error(self, client):
from src.core.utils.network import NetworkError
mock_resp = MagicMock()
mock_resp.status_code = 502
exc = httpx.HTTPStatusError("Bad Gateway", request=MagicMock(), response=mock_resp)
client._client.post = AsyncMock(side_effect=exc)
with pytest.raises(NetworkError, match="Environment unavailable"):
await client.authenticate()
@pytest.mark.asyncio
async def test_login_http_503_raises_network_error(self, client):
from src.core.utils.network import NetworkError
mock_resp = MagicMock()
mock_resp.status_code = 503
exc = httpx.HTTPStatusError("Service Unavailable", request=MagicMock(), response=mock_resp)
client._client.post = AsyncMock(side_effect=exc)
with pytest.raises(NetworkError, match="Environment unavailable"):
await client.authenticate()
@pytest.mark.asyncio
async def test_login_http_401_raises_authentication_error(self, client):
from src.core.utils.network import AuthenticationError
mock_resp = MagicMock()
mock_resp.status_code = 401
exc = httpx.HTTPStatusError("Unauthorized", request=MagicMock(), response=mock_resp)
client._client.post = AsyncMock(side_effect=exc)
with pytest.raises(AuthenticationError):
await client.authenticate()
@pytest.mark.asyncio
async def test_login_httpx_error_raises_network_error(self, client):
from src.core.utils.network import NetworkError
client._client.post = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))
with pytest.raises(NetworkError, match="Network or parsing error"):
await client.authenticate()
@pytest.mark.asyncio
async def test_cache_refresh_csrf_failure_does_not_block(self, client):
from src.core.utils.network import SupersetAuthCache
SupersetAuthCache.set(
client._auth_cache_key,
{"access_token": "cached_access", "csrf_token": "cached_csrf"},
)
# CSRF refresh fails silently
client._client.get = AsyncMock(side_effect=Exception("Connection lost"))
tokens = await client.authenticate()
# Should still succeed with cached tokens
assert tokens["access_token"] == "cached_access"
@pytest.mark.asyncio
async def test_cache_hit_after_wait(self, client):
"""Second caller after lock should find cache populated."""
from src.core.utils.network import SupersetAuthCache
# Simulate first caller putting tokens in cache
SupersetAuthCache.set(
client._auth_cache_key,
{"access_token": "cached_after", "csrf_token": "cached_csrf"},
)
mock_csrf_resp = MagicMock()
mock_csrf_resp.is_success = False
client._client.get = AsyncMock(return_value=mock_csrf_resp)
tokens = await client.authenticate() # Goes through lock path
assert tokens["access_token"] == "cached_after"
@pytest.mark.asyncio
async def test_login_missing_access_token_key(self, client):
mock_login_resp = MagicMock()
mock_login_resp.status_code = 200
mock_login_resp.json.return_value = {"not_access_token": "value"}
mock_login_resp.raise_for_status = MagicMock()
client._client.post = AsyncMock(return_value=mock_login_resp)
from src.core.utils.network import NetworkError
with pytest.raises(NetworkError):
await client.authenticate()
class TestGetHeaders:
"""get_headers(): returns auth headers, may trigger auth."""
@pytest.mark.asyncio
async def test_unauthenticated_calls_authenticate(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
# Simulate what authenticate sets
async def fake_auth():
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
return client._tokens
client.authenticate = fake_auth
client._authenticated = False
headers = await client.get_headers()
assert "Authorization" in headers
assert headers["Authorization"] == "Bearer acc"
assert headers["X-CSRFToken"] == "csrf"
@pytest.mark.asyncio
async def test_authenticated_returns_headers(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
client.authenticate = AsyncMock()
headers = await client.get_headers()
client.authenticate.assert_not_called()
assert headers["Referer"] == "https://test.com"
class TestRequest:
"""request(): main HTTP request method."""
@pytest.fixture
def client(self):
from src.core.utils.async_network import AsyncAPIClient
c = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
c._client = AsyncMock()
c._authenticated = True
c._tokens = {"access_token": "acc", "csrf_token": "csrf"}
return c
@pytest.mark.asyncio
async def test_get_request(self, client):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"result": "ok"}
client._client.request = AsyncMock(return_value=mock_resp)
result = await client.request(method="GET", endpoint="/chart/")
assert result == {"result": "ok"}
@pytest.mark.asyncio
async def test_post_with_dict_data(self, client):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"id": 1}
client._client.request = AsyncMock(return_value=mock_resp)
result = await client.request(method="POST", endpoint="/chart/", data={"key": "val"})
assert result == {"id": 1}
call_kwargs = client._client.request.call_args[1]
assert call_kwargs["json"] == {"key": "val"}
@pytest.mark.asyncio
async def test_post_with_pre_serialized_string(self, client):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"id": 1}
client._client.request = AsyncMock(return_value=mock_resp)
result = await client.request(method="POST", endpoint="/chart/", data='{"key":"val"}')
assert result == {"id": 1}
call_kwargs = client._client.request.call_args[1]
assert call_kwargs["content"] == b'{"key":"val"}'
assert "json" not in call_kwargs
@pytest.mark.asyncio
async def test_raw_response(self, client):
mock_resp = MagicMock()
mock_resp.status_code = 200
client._client.request = AsyncMock(return_value=mock_resp)
result = await client.request(method="GET", endpoint="/chart/1", raw_response=True)
assert result is mock_resp
@pytest.mark.asyncio
async def test_401_retry_for_get(self, client):
# Pre-authenticate so 401 retry works without hitting login flow.
# We need authenticate() to succeed quickly when called in the retry path.
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
# Cache tokens so SupersetAuthCache.get returns something
from src.core.utils.network import SupersetAuthCache
SupersetAuthCache.set(
client._auth_cache_key,
{"access_token": "retry_token", "csrf_token": "retry_csrf"},
)
# Un-authenticate so get_headers() calls authenticate() (which will hit cache)
client._authenticated = False
first_resp = MagicMock()
first_resp.status_code = 401
second_resp = MagicMock()
second_resp.status_code = 200
second_resp.json.return_value = {"result": "retried"}
client._client.request = AsyncMock(side_effect=[first_resp, second_resp])
# Mock CSRF refresh during auth
mock_csrf_resp = MagicMock()
mock_csrf_resp.is_success = True
mock_csrf_resp.json.return_value = {"result": "csrf_new"}
client._client.get = AsyncMock(return_value=mock_csrf_resp)
result = await client.request(method="GET", endpoint="/chart/")
assert result == {"result": "retried"}
assert client._client.request.call_count == 2
@pytest.mark.asyncio
async def test_401_raises_for_non_get(self, client):
from src.core.utils.network import AuthenticationError
mock_resp = MagicMock()
mock_resp.status_code = 401
client._client.request = AsyncMock(return_value=mock_resp)
with pytest.raises(AuthenticationError):
await client.request(method="POST", endpoint="/chart/")
@pytest.mark.asyncio
async def test_json_parse_error(self, client):
from src.core.utils.network import SupersetAPIError
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.side_effect = ValueError("No JSON")
mock_resp.text = "not json"
client._client.request = AsyncMock(return_value=mock_resp)
with pytest.raises(SupersetAPIError, match="Failed to parse response JSON"):
await client.request(method="GET", endpoint="/chart/")
@pytest.mark.asyncio
async def test_semaphore_acquire_release(self, client):
sem = asyncio.Semaphore(1)
client._semaphore = sem
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"ok": True}
client._client.request = AsyncMock(return_value=mock_resp)
result = await client.request(method="GET", endpoint="/test")
assert result == {"ok": True}
# Semaphore released implies no deadlock
@pytest.mark.asyncio
async def test_http_status_error_404_dashboard(self, client):
from src.core.utils.network import DashboardNotFoundError
mock_resp = MagicMock()
mock_resp.status_code = 404
exc = httpx.HTTPStatusError("Not Found", request=MagicMock(), response=mock_resp)
client._client.request = AsyncMock(side_effect=exc)
client._is_dashboard_endpoint = MagicMock(return_value=True)
with pytest.raises(DashboardNotFoundError):
await client.request(method="GET", endpoint="/dashboard/42")
@pytest.mark.asyncio
async def test_http_status_error_404_generic(self, client):
from src.core.utils.network import SupersetAPIError
mock_resp = MagicMock()
mock_resp.status_code = 404
exc = httpx.HTTPStatusError("Not Found", request=MagicMock(), response=mock_resp)
client._client.request = AsyncMock(side_effect=exc)
client._is_dashboard_endpoint = MagicMock(return_value=False)
with pytest.raises(SupersetAPIError, match="API resource not found"):
await client.request(method="GET", endpoint="/chart/99")
@pytest.mark.asyncio
async def test_http_status_error_403(self, client):
from src.core.utils.network import PermissionDeniedError
mock_resp = MagicMock()
mock_resp.status_code = 403
exc = httpx.HTTPStatusError("Forbidden", request=MagicMock(), response=mock_resp)
client._client.request = AsyncMock(side_effect=exc)
with pytest.raises(PermissionDeniedError):
await client.request(method="GET", endpoint="/chart/")
@pytest.mark.asyncio
async def test_http_status_error_502(self, client):
from src.core.utils.network import NetworkError
mock_resp = MagicMock()
mock_resp.status_code = 502
exc = httpx.HTTPStatusError("Bad Gateway", request=MagicMock(), response=mock_resp)
client._client.request = AsyncMock(side_effect=exc)
with pytest.raises(NetworkError, match="Environment unavailable"):
await client.request(method="GET", endpoint="/chart/")
@pytest.mark.asyncio
async def test_http_status_error_generic(self, client):
from src.core.utils.network import SupersetAPIError
mock_resp = MagicMock()
mock_resp.status_code = 500
mock_resp.text = "Internal error"
exc = httpx.HTTPStatusError("Server Error", request=MagicMock(), response=mock_resp)
client._client.request = AsyncMock(side_effect=exc)
with pytest.raises(SupersetAPIError, match="API Error 500"):
await client.request(method="GET", endpoint="/chart/")
@pytest.mark.asyncio
async def test_httpx_timeout_error(self, client):
from src.core.utils.network import NetworkError
client._client.request = AsyncMock(side_effect=httpx.TimeoutException("Timed out"))
with pytest.raises(NetworkError, match="Request timeout"):
await client.request(method="GET", endpoint="/chart/")
@pytest.mark.asyncio
async def test_httpx_connect_error(self, client):
from src.core.utils.network import NetworkError
client._client.request = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))
with pytest.raises(NetworkError, match="Connection error"):
await client.request(method="GET", endpoint="/chart/")
@pytest.mark.asyncio
async def test_httpx_generic_error(self, client):
from src.core.utils.network import NetworkError
client._client.request = AsyncMock(side_effect=httpx.HTTPError("Generic error"))
with pytest.raises(NetworkError, match="Unknown network error"):
await client.request(method="GET", endpoint="/chart/")
class TestHandleHttpError:
"""_handle_http_error: translates HTTP status codes to exceptions."""
@pytest.fixture
def client(self):
from src.core.utils.async_network import AsyncAPIClient
return AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
def test_502_raises_network_error(self, client):
from src.core.utils.network import NetworkError
mock_resp = MagicMock()
mock_resp.status_code = 502
exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
with pytest.raises(NetworkError, match="Environment unavailable"):
client._handle_http_error(exc, "/chart/")
def test_503_raises_network_error(self, client):
from src.core.utils.network import NetworkError
mock_resp = MagicMock()
mock_resp.status_code = 503
exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
with pytest.raises(NetworkError, match="Environment unavailable"):
client._handle_http_error(exc, "/chart/")
def test_404_dashboard(self, client):
from src.core.utils.network import DashboardNotFoundError
mock_resp = MagicMock()
mock_resp.status_code = 404
exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
with patch.object(client, '_is_dashboard_endpoint', return_value=True):
with pytest.raises(DashboardNotFoundError):
client._handle_http_error(exc, "/dashboard/5")
def test_404_generic(self, client):
from src.core.utils.network import SupersetAPIError
mock_resp = MagicMock()
mock_resp.status_code = 404
exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
with patch.object(client, '_is_dashboard_endpoint', return_value=False):
with pytest.raises(SupersetAPIError, match="API resource not found"):
client._handle_http_error(exc, "/chart/5")
def test_403_raises_permission_denied(self, client):
from src.core.utils.network import PermissionDeniedError
mock_resp = MagicMock()
mock_resp.status_code = 403
exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
with pytest.raises(PermissionDeniedError):
client._handle_http_error(exc, "/chart/")
def test_401_raises_authentication_error(self, client):
from src.core.utils.network import AuthenticationError
mock_resp = MagicMock()
mock_resp.status_code = 401
exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
with pytest.raises(AuthenticationError):
client._handle_http_error(exc, "/chart/")
def test_generic_500(self, client):
from src.core.utils.network import SupersetAPIError
mock_resp = MagicMock()
mock_resp.status_code = 500
mock_resp.text = "Server broke"
exc = httpx.HTTPStatusError("", request=MagicMock(), response=mock_resp)
with pytest.raises(SupersetAPIError, match="API Error 500"):
client._handle_http_error(exc, "/chart/")
class TestIsDashboardEndpoint:
"""_is_dashboard_endpoint: recognizes dashboard endpoints."""
@pytest.fixture
def client(self):
from src.core.utils.async_network import AsyncAPIClient
return AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
def test_dashboard_endpoint(self, client):
assert client._is_dashboard_endpoint("/dashboard/42") is True
def test_dashboard_list(self, client):
assert client._is_dashboard_endpoint("/dashboard") is True
def test_chart_endpoint(self, client):
assert client._is_dashboard_endpoint("/chart/42") is False
def test_absolute_url_dashboard(self, client):
assert client._is_dashboard_endpoint("https://superset.example.com/api/v1/dashboard/42") is True
def test_absolute_url_chart(self, client):
assert client._is_dashboard_endpoint("https://superset.example.com/api/v1/chart/42") is False
def test_empty_endpoint(self, client):
assert client._is_dashboard_endpoint("") is False
def test_none_endpoint(self, client):
assert client._is_dashboard_endpoint(None) is False
def test_dashboard_with_api_v1_prefix(self, client):
assert client._is_dashboard_endpoint("/api/v1/dashboard/1") is True
def test_absolute_url_bad_format(self, client):
# URL with /api/v1 not present
assert client._is_dashboard_endpoint("https://other.com/not-api/dashboard/1") is False
class TestFetchPaginatedCount:
"""fetch_paginated_count: gets total count."""
@pytest.mark.asyncio
async def test_success(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client.request = AsyncMock(return_value={"count": 42})
result = await client.fetch_paginated_count("/chart/", {"page": 0, "page_size": 1})
assert result == 42
@pytest.mark.asyncio
async def test_non_dict_response(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client.request = AsyncMock(return_value="not_a_dict")
result = await client.fetch_paginated_count("/chart/", {})
assert result == 0
@pytest.mark.asyncio
async def test_missing_count_field(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client.request = AsyncMock(return_value={"other": 10})
result = await client.fetch_paginated_count("/chart/", {})
assert result == 0
class TestFetchPaginatedData:
"""fetch_paginated_data: page iteration."""
@pytest.mark.asyncio
async def test_single_page(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client.request = AsyncMock(
return_value={"result": [{"id": 1}, {"id": 2}], "count": 2}
)
result = await client.fetch_paginated_data(
"/chart/",
{"base_query": {"page_size": 100}, "results_field": "result"},
)
assert len(result) == 2
@pytest.mark.asyncio
async def test_multiple_pages(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client.request = AsyncMock(
side_effect=[
{"result": [{"id": 1}, {"id": 2}], "count": 5},
{"result": [{"id": 3}, {"id": 4}]},
{"result": [{"id": 5}]},
]
)
result = await client.fetch_paginated_data(
"/chart/",
{"base_query": {"page_size": 2}, "results_field": "result"},
)
assert len(result) == 5
assert client.request.call_count == 3
@pytest.mark.asyncio
async def test_total_count_from_options(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client.request = AsyncMock(
side_effect=[
{"result": [{"id": 1}], "count": 1},
{"result": [{"id": 2}]},
]
)
result = await client.fetch_paginated_data(
"/chart/",
{"base_query": {"page_size": 1}, "results_field": "result", "total_count": 2},
)
assert len(result) == 2
assert client.request.call_count == 2
@pytest.mark.asyncio
async def test_non_dict_first_page_returns_empty(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client.request = AsyncMock(return_value="not_a_dict")
result = await client.fetch_paginated_data(
"/chart/",
{"base_query": {"page_size": 100}, "results_field": "result"},
)
assert result == []
@pytest.mark.asyncio
async def test_non_dict_subsequent_page_skipped(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client.request = AsyncMock(
side_effect=[
{"result": [{"id": 1}], "count": 3},
"not_a_dict",
{"result": [{"id": 3}]},
]
)
result = await client.fetch_paginated_data(
"/chart/",
{"base_query": {"page_size": 1}, "results_field": "result"},
)
# First page has 1, second page is skipped, third page has 1
assert len(result) == 2
class TestUploadFile:
"""upload_file: multipart file upload."""
@pytest.mark.asyncio
async def test_upload_with_path_string(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
client._client = AsyncMock()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"result": "imported"}
client._client.post = AsyncMock(return_value=mock_resp)
with patch("pathlib.Path.exists", return_value=True):
with patch("pathlib.Path.read_bytes", return_value=b"zip content"):
result = await client.upload_file(
endpoint="/dashboard/import/",
file_info={
"file_obj": "/tmp/test.zip",
"file_name": "test.zip",
"form_field": "formData",
},
extra_data={"overwrite": "true"},
)
assert result == {"result": "imported"}
@pytest.mark.asyncio
async def test_upload_with_bytesio(self):
import io
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
client._client = AsyncMock()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"result": "ok"}
client._client.post = AsyncMock(return_value=mock_resp)
buf = io.BytesIO(b"zip content")
result = await client.upload_file(
endpoint="/dashboard/import/",
file_info={"file_obj": buf, "file_name": "test.zip"},
)
assert result == {"result": "ok"}
@pytest.mark.asyncio
async def test_upload_file_not_found(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
client._client = AsyncMock()
with patch("pathlib.Path.exists", return_value=False):
with pytest.raises(FileNotFoundError, match="does not exist"):
await client.upload_file(
endpoint="/dashboard/import/",
file_info={"file_obj": "/nonexistent.zip", "file_name": "nope.zip"},
)
@pytest.mark.asyncio
async def test_upload_unsupported_type(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
client._client = AsyncMock()
with pytest.raises(TypeError, match="Unsupported file_obj"):
await client.upload_file(
endpoint="/dashboard/import/",
file_info={"file_obj": 42, "file_name": "nope.zip"},
)
@pytest.mark.asyncio
async def test_upload_http_error(self):
from src.core.utils.async_network import AsyncAPIClient
from src.core.utils.network import SupersetAPIError
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
client._client = AsyncMock()
exc = httpx.HTTPStatusError("Error", request=MagicMock(), response=MagicMock())
exc.response.text = "Bad request"
client._client.post = AsyncMock(side_effect=exc)
with patch("pathlib.Path.exists", return_value=True):
with patch("pathlib.Path.read_bytes", return_value=b"zip"):
with pytest.raises(SupersetAPIError, match="API error during upload"):
await client.upload_file(
endpoint="/dashboard/import/",
file_info={"file_obj": "/tmp/test.zip", "file_name": "test.zip"},
)
@pytest.mark.asyncio
async def test_upload_network_error(self):
from src.core.utils.async_network import AsyncAPIClient
from src.core.utils.network import NetworkError
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
client._client = AsyncMock()
client._client.post = AsyncMock(side_effect=httpx.HTTPError("Network error"))
with patch("pathlib.Path.exists", return_value=True):
with patch("pathlib.Path.read_bytes", return_value=b"zip"):
with pytest.raises(NetworkError, match="Network error during upload"):
await client.upload_file(
endpoint="/dashboard/import/",
file_info={"file_obj": "/tmp/test.zip", "file_name": "test.zip"},
)
@pytest.mark.asyncio
async def test_upload_non_200_json_error(self):
import io
from src.core.utils.async_network import AsyncAPIClient
from src.core.utils.network import SupersetAPIError
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._authenticated = True
client._tokens = {"access_token": "acc", "csrf_token": "csrf"}
client._client = AsyncMock()
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.side_effect = ValueError("Not JSON")
client._client.post = AsyncMock(return_value=mock_resp)
buf = io.BytesIO(b"zip")
with pytest.raises(SupersetAPIError, match="API error during upload"):
await client.upload_file(
endpoint="/dashboard/import/",
file_info={"file_obj": buf, "file_name": "test.zip"},
)
class TestAclose:
"""aclose: closes underlying httpx client."""
@pytest.mark.asyncio
async def test_closes_client(self):
from src.core.utils.async_network import AsyncAPIClient
client = AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
client._client = AsyncMock()
client._client.aclose = AsyncMock()
await client.aclose()
client._client.aclose.assert_awaited_once()
class TestNormalizeBaseUrl:
"""_normalize_base_url: URL normalization edge cases."""
@pytest.fixture
def client(self):
from src.core.utils.async_network import AsyncAPIClient
return AsyncAPIClient({"base_url": "https://test.com", "auth": {}})
def test_empty_url(self, client):
assert client._normalize_base_url("") == ""
def test_none_url(self, client):
assert client._normalize_base_url(None) == ""
def test_api_v1_suffix(self, client):
assert client._normalize_base_url("https://test.com/api/v1") == "https://test.com"
def test_trailing_slash(self, client):
assert client._normalize_base_url("https://test.com/") == "https://test.com"
def test_whitespace_stripped(self, client):
assert client._normalize_base_url(" https://test.com ") == "https://test.com"
def test_api_v1_with_trailing_slash(self, client):
assert client._normalize_base_url("https://test.com/api/v1/") == "https://test.com"
# #endregion Test.AsyncNetwork

View File

@@ -0,0 +1,260 @@
# #region Test.DashboardsFiltersMixin [C:3] [TYPE Module] [SEMANTICS test, dashboard, filter, permalink, mixin]
# @BRIEF Tests for core/superset_client/_dashboards_filters.py — SupersetDashboardsFiltersMixin.
# @RELATION BINDS_TO -> [SupersetDashboardsFiltersMixin]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestDashboardsFiltersMixin:
"""Test all methods of SupersetDashboardsFiltersMixin."""
@pytest.fixture
def mixin(self):
from src.core.superset_client._dashboards_filters import (
SupersetDashboardsFiltersMixin,
)
m = SupersetDashboardsFiltersMixin()
m.client = MagicMock()
m.client.request = AsyncMock()
return m
@pytest.mark.asyncio
async def test_get_dashboard_with_int(self, mixin):
mixin.client.request.return_value = {"result": {"id": 1, "dashboard_title": "Test"}}
result = await mixin.get_dashboard(1)
mixin.client.request.assert_awaited_once_with(method="GET", endpoint="/dashboard/1")
assert result["result"]["id"] == 1
@pytest.mark.asyncio
async def test_get_dashboard_with_str(self, mixin):
mixin.client.request.return_value = {"result": {"id": 2, "slug": "my-dash"}}
result = await mixin.get_dashboard("my-dash")
mixin.client.request.assert_awaited_once_with(method="GET", endpoint="/dashboard/my-dash")
assert result["result"]["slug"] == "my-dash"
@pytest.mark.asyncio
async def test_get_dashboard_permalink_state(self, mixin):
mixin.client.request.return_value = {"result": {"state": {"dataMask": {}}}}
result = await mixin.get_dashboard_permalink_state("abc123")
mixin.client.request.assert_awaited_once_with(
method="GET", endpoint="/dashboard/permalink/abc123"
)
assert "state" in result["result"]
@pytest.mark.asyncio
async def test_get_native_filter_state(self, mixin):
mixin.client.request.return_value = {"result": {"value": '{"id": 1}'}}
result = await mixin.get_native_filter_state(1, "key123")
mixin.client.request.assert_awaited_once_with(
method="GET", endpoint="/dashboard/1/filter_state/key123"
)
assert "value" in result["result"]
@pytest.mark.asyncio
async def test_extract_native_filters_from_permalink(self, mixin):
mixin.get_dashboard_permalink_state = AsyncMock(
return_value={
"result": {
"state": {
"dataMask": {
"filter_1": {
"extraFormData": {"col1": "val1"},
"filterState": {"value": 1},
"ownState": {},
}
},
"activeTabs": ["tab1"],
"anchor": "tab1",
"chartStates": {"chart1": {}},
}
}
}
)
result = await mixin.extract_native_filters_from_permalink("perm_key")
assert result["dataMask"]["filter_1"]["extraFormData"] == {"col1": "val1"}
assert result["activeTabs"] == ["tab1"]
assert result["anchor"] == "tab1"
assert result["chartStates"] == {"chart1": {}}
assert result["permalink_key"] == "perm_key"
@pytest.mark.asyncio
async def test_extract_native_filters_from_permalink_empty_state(self, mixin):
mixin.get_dashboard_permalink_state = AsyncMock(return_value={})
result = await mixin.extract_native_filters_from_permalink("empty_key")
assert result["dataMask"] == {}
assert result["activeTabs"] == []
@pytest.mark.asyncio
async def test_extract_native_filters_from_permalink_skips_non_dict(self, mixin):
mixin.get_dashboard_permalink_state = AsyncMock(
return_value={
"result": {
"state": {
"dataMask": {
"filter_1": {"extraFormData": {}, "filterState": {}, "ownState": {}},
"filter_2": "not_a_dict",
}
}
}
}
)
result = await mixin.extract_native_filters_from_permalink("key")
assert "filter_1" in result["dataMask"]
assert "filter_2" not in result["dataMask"]
@pytest.mark.asyncio
async def test_extract_native_filters_from_key_with_str_value(self, mixin):
mixin.get_native_filter_state = AsyncMock(
return_value={"result": {"value": '{"id": "f1", "extraFormData": {}, "filterState": {}, "ownState": {}}'}}
)
result = await mixin.extract_native_filters_from_key(1, "key1")
assert "f1" in result["dataMask"]
assert result["dashboard_id"] == 1
assert result["filter_state_key"] == "key1"
@pytest.mark.asyncio
async def test_extract_native_filters_from_key_with_dict_value(self, mixin):
mixin.get_native_filter_state = AsyncMock(
return_value={
"result": {
"value": {
"f1": {"extraFormData": {}, "filterState": {}, "ownState": {}},
"f2": {"extraFormData": {}, "filterState": {}, "ownState": {}},
}
}
}
)
result = await mixin.extract_native_filters_from_key(1, "key2")
assert "f1" in result["dataMask"]
assert "f2" in result["dataMask"]
@pytest.mark.asyncio
async def test_extract_native_filters_from_key_with_invalid_json(self, mixin):
mixin.get_native_filter_state = AsyncMock(
return_value={"result": {"value": "{invalid json}"}}
)
with patch("src.core.superset_client._dashboards_filters.app_logger") as mock_log:
result = await mixin.extract_native_filters_from_key(1, "bad_key")
mock_log.warning.assert_called_once()
assert result["dataMask"] == {}
@pytest.mark.asyncio
async def test_extract_native_filters_from_key_with_non_dict_value(self, mixin):
mixin.get_native_filter_state = AsyncMock(
return_value={"result": {"value": 42}}
)
result = await mixin.extract_native_filters_from_key(1, "int_key")
assert result["dataMask"] == {}
@pytest.mark.asyncio
async def test_extract_native_filters_from_key_skips_non_dict_items(self, mixin):
mixin.get_native_filter_state = AsyncMock(
return_value={
"result": {
"value": {
"f1": {"extraFormData": {}, "filterState": {}, "ownState": {}},
"f2": "not_a_dict",
}
}
}
)
result = await mixin.extract_native_filters_from_key(1, "key3")
assert "f1" in result["dataMask"]
assert "f2" not in result["dataMask"]
@pytest.mark.asyncio
async def test_parse_dashboard_url_for_filters_permalink(self, mixin):
mixin.extract_native_filters_from_permalink = AsyncMock(
return_value={"dataMask": {}, "permalink_key": "abc123"}
)
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/superset/dashboard/p/abc123/"
)
assert result["filter_type"] == "permalink"
assert result["filters"]["permalink_key"] == "abc123"
@pytest.mark.asyncio
async def test_parse_dashboard_url_for_filters_native_filters_key_with_int_id(self, mixin):
mixin.extract_native_filters_from_key = AsyncMock(
return_value={"dataMask": {}, "dashboard_id": 5}
)
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/superset/dashboard/5/?native_filters_key=key123"
)
assert result["filter_type"] == "native_filters_key"
assert result["dashboard_id"] == 5
@pytest.mark.asyncio
async def test_parse_dashboard_url_for_filters_native_filters_key_with_slug(self, mixin):
# Slug resolution calls get_dashboard first
mixin.get_dashboard = AsyncMock(return_value={"result": {"id": 42}})
mixin.extract_native_filters_from_key = AsyncMock(
return_value={"dataMask": {}, "dashboard_id": 42}
)
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/superset/dashboard/my-slug/?native_filters_key=key456"
)
assert result["filter_type"] == "native_filters_key"
assert result["dashboard_id"] == 42
@pytest.mark.asyncio
async def test_parse_dashboard_url_for_filters_slug_resolution_fails(self, mixin):
mixin.get_dashboard = AsyncMock(side_effect=Exception("Not found"))
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/superset/dashboard/unknown/?native_filters_key=key789"
)
# Should have no filter_type set since resolution failed
assert result["filter_type"] is None
@pytest.mark.asyncio
async def test_parse_dashboard_url_for_filters_native_filters_json(self, mixin):
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/superset/dashboard/1/?native_filters=%7B%22key%22%3A%22val%22%7D"
)
assert result["filter_type"] == "native_filters"
assert "dataMask" in result["filters"]
@pytest.mark.asyncio
async def test_parse_dashboard_url_for_filters_bad_native_filters_json(self, mixin):
with patch("src.core.superset_client._dashboards_filters.app_logger") as mock_log:
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/dashboard/1/?native_filters={invalid}"
)
assert result["filter_type"] is None
mock_log.warning.assert_called_once()
@pytest.mark.asyncio
async def test_parse_dashboard_url_for_filters_no_filters(self, mixin):
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/superset/dashboard/1/"
)
assert result["filter_type"] is None
assert result["filters"] == {}
@pytest.mark.asyncio
async def test_parse_dashboard_url_no_dashboard_in_path_for_native_key(self, mixin):
# native_filters_key present but no dashboard in path
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/superset/other/?native_filters_key=keyX"
)
assert result["filter_type"] is None
@pytest.mark.asyncio
async def test_parse_dashboard_url_p_is_reserved_in_path(self, mixin):
# URL has /p/ but not in dashboard context
# The mixin will try extract_native_filters_from_permalink -> get_dashboard_permalink_state
# which calls self.client.request. Set it to return an empty dict.
mixin.client.request.return_value = {"result": {"state": {}}}
result = await mixin.parse_dashboard_url_for_filters(
"https://superset.example.com/superset/other/p/something/"
)
assert result["filter_type"] == "permalink"
# #endregion Test.DashboardsFiltersMixin

View File

@@ -0,0 +1,231 @@
# #region Test.DashboardsWriteMixin [C:3] [TYPE Module] [SEMANTICS test, dashboard, write, markdown, layout]
# @BRIEF Tests for core/superset_client/_dashboards_write.py — SupersetDashboardsWriteMixin.
# @RELATION BINDS_TO -> [SupersetDashboardsWriteMixin]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, call, patch
import pytest
class TestDashboardsWriteMixin:
"""Test all methods of SupersetDashboardsWriteMixin."""
@pytest.fixture
def mixin(self):
from src.core.superset_client._dashboards_write import (
SupersetDashboardsWriteMixin,
)
m = SupersetDashboardsWriteMixin()
m.client = MagicMock()
m.client.request = AsyncMock()
return m
# ── create_markdown_chart ──
@pytest.mark.asyncio
async def test_create_markdown_chart_success(self, mixin):
mixin._resolve_markdown_datasource = AsyncMock(return_value=10)
mixin.client.request.return_value = {"id": 42}
result = await mixin.create_markdown_chart(1, "Hello")
assert result == 42
mixin.client.request.assert_awaited_once()
call_kwargs = mixin.client.request.call_args
assert call_kwargs[1]["method"] == "POST"
assert call_kwargs[1]["endpoint"] == "/chart/"
@pytest.mark.asyncio
async def test_create_markdown_chart_id_in_result(self, mixin):
mixin._resolve_markdown_datasource = AsyncMock(return_value=10)
mixin.client.request.return_value = {"result": {"id": 99}}
result = await mixin.create_markdown_chart(1, "Banner")
assert result == 99
@pytest.mark.asyncio
async def test_create_markdown_chart_no_id_returns_zero(self, mixin):
mixin._resolve_markdown_datasource = AsyncMock(return_value=10)
mixin.client.request.return_value = {}
result = await mixin.create_markdown_chart(1, "Text")
assert result == 0
@pytest.mark.asyncio
async def test_create_markdown_chart_request_fails(self, mixin):
mixin._resolve_markdown_datasource = AsyncMock(return_value=10)
mixin.client.request.side_effect = ValueError("API error")
with pytest.raises(ValueError):
await mixin.create_markdown_chart(1, "Fail")
# ── update_markdown_chart ──
@pytest.mark.asyncio
async def test_update_markdown_chart_success(self, mixin):
mixin.client.request.return_value = {"result": "ok"}
result = await mixin.update_markdown_chart(5, "Updated text")
mixin.client.request.assert_awaited_once_with(
method="PUT", endpoint="/chart/5", data=mixin.client.request.call_args[1]["data"]
)
assert result == {"result": "ok"}
@pytest.mark.asyncio
async def test_update_markdown_chart_fails(self, mixin):
mixin.client.request.side_effect = RuntimeError("Update failed")
with pytest.raises(RuntimeError):
await mixin.update_markdown_chart(5, "Text")
# ── update_dashboard_layout ──
@pytest.mark.asyncio
async def test_update_dashboard_layout_success(self, mixin):
mock_position = {
"GRID_ID": {"children": []},
"DASHBOARD_VERSION_KEY": "v2",
}
# First call returns dashboard, second call returns success
mixin.client.request = AsyncMock(
side_effect=[
{"result": {"position_json": '{"GRID_ID": {"children": []}, "DASHBOARD_VERSION_KEY": "v2"}'}},
{"result": "ok"},
]
)
result = await mixin.update_dashboard_layout(1, 42)
assert mixin.client.request.call_count == 2
assert result == {"result": "ok"}
@pytest.mark.asyncio
async def test_update_dashboard_layout_fails(self, mixin):
mixin.client.request.side_effect = ConnectionError("Network error")
with pytest.raises(ConnectionError):
await mixin.update_dashboard_layout(1, 42)
# ── remove_chart_from_layout ──
@pytest.mark.asyncio
async def test_remove_chart_from_layout_success(self, mixin):
mixin.client.request = AsyncMock(
side_effect=[
{"result": {"position_json": '{"GRID_ID": {"children": ["ROW-banner-42"]}, "ROW-banner-42": {}, "MARKDOWN-banner-42": {}}'}},
{"result": "ok"},
]
)
result = await mixin.remove_chart_from_layout(1, 42)
assert mixin.client.request.call_count == 2
assert result == {"result": "ok"}
@pytest.mark.asyncio
async def test_remove_chart_from_layout_fails(self, mixin):
mixin.client.request.side_effect = OSError("IO error")
with pytest.raises(OSError):
await mixin.remove_chart_from_layout(1, 42)
# ── delete_chart ──
@pytest.mark.asyncio
async def test_delete_chart_success(self, mixin):
mixin.client.request.return_value = {"result": "deleted"}
result = await mixin.delete_chart(42)
mixin.client.request.assert_awaited_once_with(method="DELETE", endpoint="/chart/42")
assert result == {"result": "deleted"}
@pytest.mark.asyncio
async def test_delete_chart_fails(self, mixin):
mixin.client.request.side_effect = PermissionError("Forbidden")
with pytest.raises(PermissionError):
await mixin.delete_chart(42)
# ── update_banner_on_dashboard ──
@pytest.mark.asyncio
async def test_update_banner_on_dashboard_success(self, mixin):
mixin.client.request = AsyncMock(
side_effect=[
{"result": {"position_json": '{"MARKDOWN-banner-7": {"meta": {"code": "old", "height": 19}}}'}},
{"result": "ok"},
]
)
result = await mixin.update_banner_on_dashboard(1, 7, "New content")
assert mixin.client.request.call_count == 2
assert result == {"result": "ok"}
@pytest.mark.asyncio
async def test_update_banner_on_dashboard_fails(self, mixin):
mixin.client.request.side_effect = TimeoutError("Timed out")
with pytest.raises(TimeoutError):
await mixin.update_banner_on_dashboard(1, 7, "Content")
# ── get_dashboard_layout ──
@pytest.mark.asyncio
async def test_get_dashboard_layout_success(self, mixin):
mixin.client.request.return_value = {
"result": {"position_json": '{"GRID_ID": {"children": []}}'}
}
result = await mixin.get_dashboard_layout(1)
assert "GRID_ID" in result
assert result["GRID_ID"]["children"] == []
@pytest.mark.asyncio
async def test_get_dashboard_layout_fails(self, mixin):
mixin.client.request.side_effect = Exception("Fetch failed")
with pytest.raises(Exception):
await mixin.get_dashboard_layout(1)
# ── _resolve_markdown_datasource ──
@pytest.mark.asyncio
async def test_resolve_markdown_datasource_from_dashboard(self, mixin):
mixin.client.request.return_value = {"result": [{"id": 5}]}
result = await mixin._resolve_markdown_datasource(1)
assert result == 5
@pytest.mark.asyncio
async def test_resolve_markdown_datasource_fallback_to_global(self, mixin):
mixin.client.request = AsyncMock(
side_effect=[
Exception("Dashboard datasets failed"),
{"result": [{"id": 99}]},
]
)
result = await mixin._resolve_markdown_datasource(1)
assert result == 99
@pytest.mark.asyncio
async def test_resolve_markdown_datasource_both_fail(self, mixin):
mixin.client.request = AsyncMock(
side_effect=[
Exception("Dashboard failed"),
Exception("Global failed"),
]
)
with pytest.raises(RuntimeError, match="Cannot create markdown chart"):
await mixin._resolve_markdown_datasource(1)
@pytest.mark.asyncio
async def test_resolve_markdown_datasource_empty_dashboard_datasets(self, mixin):
mixin.client.request.return_value = {"result": []}
# Then fallback to global
mixin.client.request = AsyncMock(
side_effect=[
{"result": []},
{"result": [{"id": 77}]},
]
)
result = await mixin._resolve_markdown_datasource(1)
assert result == 77
@pytest.mark.asyncio
async def test_resolve_markdown_datasource_dashboard_has_no_id_field(self, mixin):
mixin.client.request.return_value = {"result": [{"name": "test"}]}
mixin.client.request = AsyncMock(
side_effect=[
{"result": [{"name": "test"}]},
{"result": [{"id": 88}]},
]
)
result = await mixin._resolve_markdown_datasource(1)
assert result == 88
# #endregion Test.DashboardsWriteMixin

View File

@@ -0,0 +1,275 @@
# #region Test.DatasetMapper [C:3] [TYPE Module] [SEMANTICS test, dataset, mapper, sqllab, excel]
# @BRIEF Tests for core/utils/dataset_mapper.py — DatasetMapper.
# @RELATION BINDS_TO -> [DatasetMapperModule]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, call, patch
import pandas as pd
import pytest
class TestDatasetMapper:
"""Test DatasetMapper — SQL Lab and Excel mapping + run."""
@pytest.fixture
def mapper(self):
from src.core.utils.dataset_mapper import DatasetMapper
return DatasetMapper()
# ── get_sqllab_mappings ──
@pytest.mark.asyncio
async def test_get_sqllab_mappings_with_default_query(self, mapper):
# Note: get_sqllab_mappings calls client.get_dataset() WITHOUT await (source bug).
# So client.get_dataset must be a regular MagicMock, not AsyncMock.
client = MagicMock()
client.get_dataset = MagicMock(
return_value={
"result": {
"table_name": "users",
"schema": "public",
"database": {"database_name": "Postgres"},
}
}
)
sqllab_executor = AsyncMock()
sqllab_executor.execute_and_poll.return_value = {
"result": [
{"column_name": "id", "verbose_name": "Identifier"},
{"column_name": "name", "verbose_name": "Full Name"},
]
}
result = await mapper.get_sqllab_mappings(client, 1, sqllab_executor, 42)
assert result == {"id": "Identifier", "name": "Full Name"}
assert "information_schema" in sqllab_executor.execute_and_poll.call_args[0][0]
sqllab_executor.execute_and_poll.assert_awaited_once()
@pytest.mark.asyncio
async def test_get_sqllab_mappings_with_custom_query(self, mapper):
client = MagicMock()
client.get_dataset = MagicMock(
return_value={
"result": {
"table_name": "users",
"schema": "public",
"database": {"database_name": "Postgres"},
}
}
)
sqllab_executor = AsyncMock()
sqllab_executor.execute_and_poll.return_value = {
"data": [
{"column_name": "email", "verbose_name": "Email Address"},
]
}
result = await mapper.get_sqllab_mappings(
client, 1, sqllab_executor, 42, sql_query="SELECT column_name, verbose_name FROM my_table"
)
assert result == {"email": "Email Address"}
assert "my_table" in sqllab_executor.execute_and_poll.call_args[0][0]
@pytest.mark.asyncio
async def test_get_sqllab_mappings_skips_null_verbose_name(self, mapper):
client = MagicMock()
client.get_dataset = MagicMock(
return_value={"result": {"table_name": "t", "schema": "public"}}
)
sqllab_executor = AsyncMock()
sqllab_executor.execute_and_poll.return_value = {
"results": [
{"column_name": "id", "verbose_name": None},
{"column_name": "name", "verbose_name": "Name"},
]
}
result = await mapper.get_sqllab_mappings(client, 1, sqllab_executor, 42)
assert result == {"name": "Name"}
@pytest.mark.asyncio
async def test_get_sqllab_mappings_uses_default_schema(self, mapper):
client = MagicMock()
client.get_dataset = MagicMock(
return_value={"result": {"table_name": "t"}}
)
sqllab_executor = AsyncMock()
sqllab_executor.execute_and_poll.return_value = {"result": []}
await mapper.get_sqllab_mappings(client, 1, sqllab_executor, 42)
assert "public" in sqllab_executor.execute_and_poll.call_args[0][0]
# ── load_excel_mappings ──
def test_load_excel_mappings_success(self, mapper):
"""Use a real DataFrame to avoid MagicMock complexity with __getitem__."""
data = pd.DataFrame({"column_name": ["a", "b"], "verbose_name": ["Alpha", "Beta"]})
with patch("pandas.read_excel", return_value=data):
result = mapper.load_excel_mappings("/tmp/test.xlsx")
assert result == {"a": "Alpha", "b": "Beta"}
def test_load_excel_mappings_file_not_found(self, mapper):
with patch("pandas.read_excel", side_effect=FileNotFoundError("No file")):
with pytest.raises(FileNotFoundError):
mapper.load_excel_mappings("/nonexistent.xlsx")
# ── run_mapping ──
@pytest.mark.asyncio
async def test_run_mapping_sqllab_with_changes(self, mapper):
# Mock get_sqllab_mappings to avoid its missing-await issue
mapper.get_sqllab_mappings = AsyncMock(
return_value={"id": "Identifier", "name": "New Name"}
)
client = AsyncMock()
client.get_dataset = AsyncMock(
return_value={
"result": {
"table_name": "users",
"columns": [
{"column_name": "id", "verbose_name": None},
{"column_name": "name", "verbose_name": "Old Name"},
],
"metrics": [],
"owners": [],
"database": {"id": 1},
}
}
)
client.update_dataset = AsyncMock(return_value={"result": "ok"})
await mapper.run_mapping(
superset_client=client,
dataset_id=1,
source="sqllab",
sqllab_executor=MagicMock(),
database_id=42,
)
client.update_dataset.assert_awaited_once()
call_args = client.update_dataset.call_args
assert call_args[0][0] == 1 # dataset_id
@pytest.mark.asyncio
async def test_run_mapping_sqllab_no_changes(self, mapper):
mapper.get_sqllab_mappings = AsyncMock(
return_value={"name": "Same Name"}
)
client = AsyncMock()
client.get_dataset = AsyncMock(
return_value={
"result": {
"table_name": "users",
"columns": [
{"column_name": "name", "verbose_name": "Same Name"},
],
"metrics": [],
"owners": [],
"database": {"id": 1},
}
}
)
client.update_dataset = AsyncMock()
await mapper.run_mapping(
superset_client=client,
dataset_id=1,
source="sqllab",
sqllab_executor=MagicMock(),
database_id=42,
)
client.update_dataset.assert_not_awaited()
@pytest.mark.asyncio
async def test_run_mapping_excel_source(self, mapper):
mapper.load_excel_mappings = MagicMock(return_value={"a": "Alpha"})
client = AsyncMock()
client.get_dataset = AsyncMock(
return_value={
"result": {
"table_name": "t",
"columns": [{"column_name": "a", "verbose_name": None}],
"metrics": [],
"owners": [],
"database": {"id": 1},
}
}
)
client.update_dataset = AsyncMock(return_value={"result": "ok"})
await mapper.run_mapping(
superset_client=client, dataset_id=1, source="excel", excel_path="/tmp/map.xlsx"
)
client.update_dataset.assert_awaited_once()
@pytest.mark.asyncio
async def test_run_mapping_invalid_source(self, mapper):
client = MagicMock()
with patch("src.core.utils.dataset_mapper.app_logger") as mock_log:
await mapper.run_mapping(
superset_client=client, dataset_id=1, source="invalid"
)
mock_log.error.assert_called_once()
@pytest.mark.asyncio
async def test_run_mapping_missing_sqllab_args(self, mapper):
client = MagicMock()
with patch("src.core.utils.dataset_mapper.app_logger") as mock_log:
await mapper.run_mapping(
superset_client=client, dataset_id=1, source="sqllab",
sqllab_executor=None, database_id=None,
)
mock_log.error.assert_called_once()
@pytest.mark.asyncio
async def test_run_mapping_missing_excel_path(self, mapper):
client = MagicMock()
with patch("src.core.utils.dataset_mapper.app_logger") as mock_log:
await mapper.run_mapping(
superset_client=client, dataset_id=1, source="excel", excel_path=None,
)
mock_log.error.assert_called_once()
@pytest.mark.asyncio
async def test_run_mapping_client_error_handled(self, mapper):
mapper.get_sqllab_mappings = AsyncMock(side_effect=Exception("API failure"))
client = MagicMock()
with patch("src.core.utils.dataset_mapper.app_logger") as mock_log:
await mapper.run_mapping(
superset_client=client,
dataset_id=1,
source="sqllab",
sqllab_executor=MagicMock(),
database_id=42,
)
mock_log.error.assert_called_once()
@pytest.mark.asyncio
async def test_run_mapping_handles_non_dict_columns(self, mapper):
mapper.get_sqllab_mappings = AsyncMock(return_value={"a": "Alpha"})
client = AsyncMock()
client.get_dataset = AsyncMock(
return_value={
"result": {
"table_name": "t",
"columns": [{"column_name": "a", "verbose_name": None}],
"metrics": [],
"owners": [],
"database": {"id": 1},
}
}
)
client.update_dataset = AsyncMock(return_value={"result": "ok"})
await mapper.run_mapping(
superset_client=client, dataset_id=1, source="sqllab",
sqllab_executor=MagicMock(), database_id=42,
)
client.update_dataset.assert_awaited_once()
# #endregion Test.DatasetMapper

View File

@@ -0,0 +1,296 @@
# #region Test.DatasetsMixin [C:3] [TYPE Module] [SEMANTICS test, dataset, superset, crud, detail]
# @BRIEF Tests for core/superset_client/_datasets.py — SupersetDatasetsMixin.
# @RELATION BINDS_TO -> [SupersetDatasetsMixin]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestDatasetsMixin:
"""Test all methods of SupersetDatasetsMixin."""
@pytest.fixture
def mixin(self):
from src.core.superset_client._datasets import SupersetDatasetsMixin
m = SupersetDatasetsMixin()
m.client = MagicMock()
m.client.request = AsyncMock()
m._validate_query_params = MagicMock(return_value={"page": 0, "page_size": 100})
m._fetch_all_pages = AsyncMock(return_value=[{"id": 1}, {"id": 2}])
return m
# ── get_datasets ──
@pytest.mark.asyncio
async def test_get_datasets_success(self, mixin):
total, data = await mixin.get_datasets(query={"columns": ["id"]})
assert total == 2
assert len(data) == 2
mixin._validate_query_params.assert_called_once_with({"columns": ["id"]})
mixin._fetch_all_pages.assert_awaited_once()
@pytest.mark.asyncio
async def test_get_datasets_no_query(self, mixin):
total, data = await mixin.get_datasets()
assert total == 2
mixin._validate_query_params.assert_called_once_with(None)
@pytest.mark.asyncio
async def test_get_datasets_empty_result(self, mixin):
mixin._fetch_all_pages = AsyncMock(return_value=[])
total, data = await mixin.get_datasets()
assert total == 0
assert data == []
# ── get_datasets_summary ──
@pytest.mark.asyncio
async def test_get_datasets_summary(self, mixin):
mixin.get_datasets = AsyncMock(
return_value=(
2,
[
{
"id": 1,
"table_name": "users",
"schema": "public",
"database": {"database_name": "Postgres"},
},
{
"id": 2,
"table_name": "orders",
"schema": "public",
"database": {"database_name": "Postgres"},
},
],
)
)
result = await mixin.get_datasets_summary()
assert len(result) == 2
assert result[0]["table_name"] == "users"
assert result[0]["database"] == "Postgres"
assert result[1]["schema"] == "public"
@pytest.mark.asyncio
async def test_get_datasets_summary_missing_database(self, mixin):
mixin.get_datasets = AsyncMock(
return_value=(
1,
[{"id": 1, "table_name": "t", "schema": "public"}],
)
)
result = await mixin.get_datasets_summary()
assert result[0]["database"] == "Unknown"
@pytest.mark.asyncio
async def test_get_datasets_summary_empty(self, mixin):
mixin.get_datasets = AsyncMock(return_value=(0, []))
result = await mixin.get_datasets_summary()
assert result == []
# ── get_dataset_linked_dashboard_count ──
@pytest.mark.asyncio
async def test_get_dataset_linked_dashboard_count_direct_key(self, mixin):
mixin.client.request.return_value = {"dashboards": [{"id": 1}, {"id": 2}]}
result = await mixin.get_dataset_linked_dashboard_count(1)
assert result == 2
@pytest.mark.asyncio
async def test_get_dataset_linked_dashboard_count_result_key(self, mixin):
mixin.client.request.return_value = {"result": {"dashboards": [{"id": 1}]}}
result = await mixin.get_dataset_linked_dashboard_count(1)
assert result == 1
@pytest.mark.asyncio
async def test_get_dataset_linked_dashboard_count_empty(self, mixin):
mixin.client.request.return_value = {"result": {"dashboards": []}}
result = await mixin.get_dataset_linked_dashboard_count(1)
assert result == 0
@pytest.mark.asyncio
async def test_get_dataset_linked_dashboard_count_non_dict(self, mixin):
mixin.client.request.return_value = "not_a_dict"
result = await mixin.get_dataset_linked_dashboard_count(1)
assert result == 0
@pytest.mark.asyncio
async def test_get_dataset_linked_dashboard_count_error(self, mixin):
mixin.client.request.side_effect = Exception("API error")
with patch("src.core.superset_client._datasets.app_logger") as mock_log:
result = await mixin.get_dataset_linked_dashboard_count(1)
assert result == 0
mock_log.warning.assert_called_once()
# ── get_dataset_detail ──
@pytest.fixture
def detail_mixin(self):
from src.core.superset_client._datasets import SupersetDatasetsMixin
m = SupersetDatasetsMixin()
m.client = MagicMock()
m.client.request = AsyncMock()
m.get_dataset = AsyncMock()
return m
@pytest.mark.asyncio
async def test_get_dataset_detail_full(self, detail_mixin):
detail_mixin.get_dataset.return_value = {
"result": {
"id": 1,
"table_name": "users",
"schema": "public",
"database": {"id": 1, "backend": "postgresql", "database_name": "Postgres"},
"database_id": 1,
"description": "User table",
"columns": [
{"id": 1, "column_name": "name", "type": "text", "is_dttm": False, "is_active": True, "description": ""},
{"id": 2, "column_name": "created", "type": "timestamp", "is_dttm": True, "is_active": True, "description": ""},
],
"metrics": [
{"id": 1, "metric_name": "count", "expression": "COUNT(*)", "verbose_name": "Count", "description": "", "metric_type": "count"},
],
"sql": "SELECT * FROM users",
"is_sqllab_view": False,
"created_on": "2024-01-01",
"changed_on": "2024-01-02",
}
}
detail_mixin.client.request.return_value = {
"dashboards": {"result": [{"id": 10, "dashboard_title": "Main", "slug": "main"}]}
}
result = await detail_mixin.get_dataset_detail(1)
assert result["id"] == 1
assert result["table_name"] == "users"
assert len(result["columns"]) == 2
assert result["columns"][0]["name"] == "name"
assert result["columns"][1]["is_dttm"] is True
assert len(result["metrics"]) == 1
assert result["metrics"][0]["metric_name"] == "count"
assert result["linked_dashboard_count"] == 1
assert result["database_backend"] == "postgresql"
assert result["is_sqllab_view"] is False
@pytest.mark.asyncio
async def test_get_dataset_detail_no_result_key(self, detail_mixin):
detail_mixin.get_dataset.return_value = {
"id": 1,
"table_name": "no_result",
"columns": [],
"metrics": [],
}
detail_mixin.client.request.return_value = {}
result = await detail_mixin.get_dataset_detail(1)
assert result["table_name"] == "no_result"
assert result["columns"] == []
assert result["metrics"] == []
@pytest.mark.asyncio
async def test_get_dataset_detail_related_objects_error(self, detail_mixin):
detail_mixin.get_dataset.return_value = {
"result": {
"id": 1,
"table_name": "test",
"columns": [],
"metrics": [],
}
}
detail_mixin.client.request.side_effect = Exception("Connection error")
with patch("src.core.superset_client._datasets.app_logger") as mock_log:
result = await detail_mixin.get_dataset_detail(1)
assert result["linked_dashboards"] == []
mock_log.warning.assert_called_once()
@pytest.mark.asyncio
async def test_get_dataset_detail_related_objects_has_dashboards_direct(self, detail_mixin):
detail_mixin.get_dataset.return_value = {
"result": {
"id": 1,
"table_name": "test",
"columns": [],
"metrics": [],
"database": {},
}
}
detail_mixin.client.request.return_value = {"dashboards": [{"id": 5, "dashboard_title": "Dash"}]}
result = await detail_mixin.get_dataset_detail(1)
assert result["linked_dashboard_count"] == 1
assert result["linked_dashboards"][0]["id"] == 5
@pytest.mark.asyncio
async def test_get_dataset_detail_related_objects_with_int_list(self, detail_mixin):
detail_mixin.get_dataset.return_value = {
"result": {
"id": 1,
"table_name": "test",
"columns": [],
"metrics": [],
}
}
detail_mixin.client.request.return_value = {"dashboards": [5, 6]}
result = await detail_mixin.get_dataset_detail(1)
assert result["linked_dashboard_count"] == 2
assert result["linked_dashboards"][0]["id"] == 5
@pytest.mark.asyncio
async def test_get_dataset_detail_as_bool_variants(self, detail_mixin):
detail_mixin.get_dataset.return_value = {
"result": {
"id": 1,
"table_name": "test",
"columns": [
{"id": 1, "column_name": "a", "is_dttm": "1", "is_active": "true"},
{"id": 2, "column_name": "b", "is_dttm": "yes", "is_active": "on"},
{"id": 3, "column_name": "c", "is_dttm": None, "is_active": None},
{"id": 4, "column_name": "d", "is_dttm": 1, "is_active": 0},
],
"metrics": [],
"database": {"backend": "mysql"},
}
}
detail_mixin.client.request.return_value = {}
result = await detail_mixin.get_dataset_detail(1)
assert result["columns"][0]["is_dttm"] is True
assert result["columns"][1]["is_dttm"] is True
assert result["columns"][2]["is_dttm"] is False
assert result["columns"][3]["is_dttm"] is True
assert result["columns"][0]["is_active"] is True
assert result["columns"][3]["is_active"] is False
# ── get_dataset ──
@pytest.mark.asyncio
async def test_get_dataset_success(self, mixin):
mixin.client.request.return_value = {"result": {"id": 1, "table_name": "test"}}
result = await mixin.get_dataset(1)
assert result["result"]["id"] == 1
# ── update_dataset ──
@pytest.mark.asyncio
async def test_update_dataset_success(self, mixin):
mixin.client.request.return_value = {"result": "ok"}
result = await mixin.update_dataset(1, {"columns": []}, override_columns=True)
mixin.client.request.assert_awaited_once()
assert result["result"] == "ok"
@pytest.mark.asyncio
async def test_update_dataset_no_override(self, mixin):
mixin.client.request.return_value = {"result": "ok"}
result = await mixin.update_dataset(1, {"columns": []})
assert result["result"] == "ok"
@pytest.mark.asyncio
async def test_update_dataset_with_json_data(self, mixin):
mixin.client.request.return_value = {"result": "ok"}
result = await mixin.update_dataset(1, {"table_name": "new_name"}, override_columns=False)
assert result["result"] == "ok"
# #endregion Test.DatasetsMixin

View File

@@ -0,0 +1,285 @@
# #region Test.DatasetsPreviewMixin [C:3] [TYPE Module] [SEMANTICS test, dataset, preview, compile, query]
# @BRIEF Tests for core/superset_client/_datasets_preview.py — SupersetDatasetsPreviewMixin.
# @RELATION BINDS_TO -> [SupersetDatasetsPreviewMixin]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestDatasetsPreviewMixin:
"""Test SupersetDatasetsPreviewMixin methods."""
@pytest.fixture
def mixin(self):
from src.core.superset_client._datasets_preview import (
SupersetDatasetsPreviewMixin,
)
m = SupersetDatasetsPreviewMixin()
m.client = MagicMock()
m.client.request = AsyncMock()
m.get_dataset = AsyncMock()
m.build_dataset_preview_query_context = MagicMock(
return_value={
"datasource": {"id": 1, "type": "table"},
"queries": [{"metrics": ["count"], "columns": [], "filters": []}],
"form_data": {"datasource": "1__table"},
"result_format": "json",
"result_type": "query",
"force": True,
}
)
m.build_dataset_preview_legacy_form_data = MagicMock(
return_value={
"datasource": "1__table",
"metrics": ["count"],
"result_format": "json",
"result_type": "query",
"force": True,
}
)
m._extract_compiled_sql_from_preview_response = MagicMock(
return_value={"compiled_sql": "SELECT 1", "response_diagnostics": []}
)
return m
# ── compile_dataset_preview ──
@pytest.mark.asyncio
async def test_compile_dataset_preview_first_strategy_succeeds(self, mixin):
mixin.get_dataset.return_value = {"result": {"id": 1}}
mixin.client.request.return_value = {"compiled_sql": "SELECT 1"}
result = await mixin.compile_dataset_preview(
dataset_id=1, template_params={"schema": "public"}, effective_filters=[{"col": "status", "val": "active"}]
)
assert result["compiled_sql"] == "SELECT 1"
assert result["dataset_id"] == 1
assert "endpoint_kind" in result
assert "strategy_attempts" in result
@pytest.mark.asyncio
async def test_compile_dataset_preview_all_strategies_fail(self, mixin):
mixin.get_dataset.return_value = {"result": {"id": 1}}
mixin.client.request.side_effect = Exception("API error")
from src.core.utils.network import SupersetAPIError
with pytest.raises(SupersetAPIError, match="Superset preview compilation failed"):
await mixin.compile_dataset_preview(dataset_id=1)
@pytest.mark.asyncio
async def test_compile_dataset_preview_get_dataset_non_dict(self, mixin):
mixin.get_dataset.return_value = "raw_response"
mixin.client.request.return_value = {"compiled_sql": "SELECT 1"}
result = await mixin.compile_dataset_preview(dataset_id=1)
assert result["compiled_sql"] == "SELECT 1"
@pytest.mark.asyncio
async def test_compile_dataset_preview_no_template_params(self, mixin):
# Note: fixture sets mixin._extract_compiled_sql_from_preview_response to return
# {"compiled_sql": "SELECT 1"}, so this overrides client.request return value.
# The actual endpoint returns whatever the mock provides, but the extraction
# is mocked to "SELECT 1". This test verifies that calling without params works.
mixin.get_dataset.return_value = {"result": {"id": 1}}
mixin.client.request.return_value = {"compiled_sql": "SELECT 1"}
result = await mixin.compile_dataset_preview(dataset_id=1)
assert result["compiled_sql"] == "SELECT 1"
# ── build_dataset_preview_query_context ──
def test_build_dataset_preview_query_context_basic(self):
from src.core.superset_client._datasets_preview import (
SupersetDatasetsPreviewMixin,
)
m = SupersetDatasetsPreviewMixin()
m._normalize_effective_filters_for_query_context = MagicMock(
return_value={
"filters": [{"col": "status", "op": "==", "val": "active"}],
"extra_form_data": {},
"diagnostics": [],
}
)
result = m.build_dataset_preview_query_context(
dataset_id=1,
dataset_record={
"id": 1,
"table_name": "users",
"schema": "public",
"datasource": {"id": 1, "type": "table"},
},
template_params={"param1": "val1"},
effective_filters=[{"col": "status", "val": "active"}],
)
assert result["datasource"]["id"] == 1
assert result["datasource"]["type"] == "table"
assert result["form_data"]["datasource"] == "1__table"
assert len(result["queries"]) == 1
assert result["queries"][0]["schema"] == "public"
assert result["queries"][0]["row_limit"] == 1000
assert result["queries"][0]["url_params"] == {"param1": "val1"}
def test_build_dataset_preview_query_context_with_template_params_from_dataset(self):
from src.core.superset_client._datasets_preview import (
SupersetDatasetsPreviewMixin,
)
m = SupersetDatasetsPreviewMixin()
m._normalize_effective_filters_for_query_context = MagicMock(
return_value={"filters": [], "extra_form_data": {}, "diagnostics": []}
)
result = m.build_dataset_preview_query_context(
dataset_id=1,
dataset_record={
"id": 1,
"table_name": "users",
"template_params": '{"dataset_param": "value"}',
},
template_params={"param1": "val1"},
effective_filters=[],
)
# Dataset template_params should be merged (setdefault, so existing keys take precedence)
assert result["queries"][0]["url_params"]["param1"] == "val1"
assert result["queries"][0]["url_params"]["dataset_param"] == "value"
def test_build_dataset_preview_query_context_with_bad_template_params(self):
from src.core.superset_client._datasets_preview import (
SupersetDatasetsPreviewMixin,
)
m = SupersetDatasetsPreviewMixin()
m._normalize_effective_filters_for_query_context = MagicMock(
return_value={"filters": [], "extra_form_data": {}, "diagnostics": []}
)
with patch("src.core.superset_client._datasets_preview.app_logger") as mock_log:
result = m.build_dataset_preview_query_context(
dataset_id=1,
dataset_record={
"id": 1,
"table_name": "users",
"template_params": "{invalid json}",
},
template_params={},
effective_filters=[],
)
mock_log.explore.assert_called_once()
assert result["queries"][0]["url_params"] == {}
def test_build_dataset_preview_query_context_with_filters(self):
from src.core.superset_client._datasets_preview import (
SupersetDatasetsPreviewMixin,
)
m = SupersetDatasetsPreviewMixin()
m._normalize_effective_filters_for_query_context = MagicMock(
return_value={
"filters": [{"col": "status", "op": "==", "val": "active"}],
"extra_form_data": {"time_range": "Last week"},
"diagnostics": [],
}
)
result = m.build_dataset_preview_query_context(
dataset_id=1,
dataset_record={"id": 1, "table_name": "users"},
template_params={},
effective_filters=[{"col": "status", "val": "active"}],
)
# Filters should be in extra_form_data and in the query
assert "time_range" in result["form_data"]["extra_form_data"]
assert result["form_data"]["extra_form_data"]["time_range"] == "Last week"
assert result["queries"][0]["filters"] == [{"col": "status", "op": "==", "val": "active"}]
def test_build_dataset_preview_query_context_datasource_passthrough(self):
from src.core.superset_client._datasets_preview import (
SupersetDatasetsPreviewMixin,
)
m = SupersetDatasetsPreviewMixin()
m._normalize_effective_filters_for_query_context = MagicMock(
return_value={"filters": [], "extra_form_data": {}, "diagnostics": []}
)
result = m.build_dataset_preview_query_context(
dataset_id=1,
dataset_record={
"id": 1,
"table_name": "users",
"datasource": {"id": 99, "type": "query"},
},
template_params={},
effective_filters=[],
)
# The datasource from the record should override
assert result["datasource"]["id"] == 99
assert result["datasource"]["type"] == "query"
# ── build_dataset_preview_legacy_form_data ──
def test_build_dataset_preview_legacy_form_data_basic(self):
from src.core.superset_client._datasets_preview import (
SupersetDatasetsPreviewMixin,
)
m = SupersetDatasetsPreviewMixin()
m.build_dataset_preview_query_context = MagicMock(
return_value={
"queries": [{
"metrics": ["count"],
"columns": [],
"orderby": [],
"annotation_layers": [],
"row_limit": 1000,
"series_limit": 0,
"url_params": {},
"applied_time_extras": {},
"extras": {"where": ""},
}],
"form_data": {"datasource": "1__table"},
"result_format": "json",
"result_type": "query",
"force": True,
}
)
result = m.build_dataset_preview_legacy_form_data(1, {}, {}, [])
assert result["metrics"] == ["count"]
assert result["result_format"] == "json"
assert result["result_type"] == "query"
assert result["force"] is True
assert "datasource" not in result # popped
def test_build_dataset_preview_legacy_form_data_with_extras(self):
from src.core.superset_client._datasets_preview import (
SupersetDatasetsPreviewMixin,
)
m = SupersetDatasetsPreviewMixin()
m.build_dataset_preview_query_context = MagicMock(
return_value={
"queries": [{
"metrics": ["count"],
"columns": [],
"orderby": [],
"annotation_layers": [],
"row_limit": 1000,
"series_limit": 0,
"url_params": {},
"applied_time_extras": {},
"extras": {"where": "1=1"},
"time_range": "Last 7 days",
}],
"form_data": {},
"result_format": "json",
"result_type": "query",
"force": False,
}
)
result = m.build_dataset_preview_legacy_form_data(1, {}, {}, [])
assert result["extras"]["where"] == "1=1"
assert result["time_range"] == "Last 7 days"
assert result["force"] is False
# #endregion Test.DatasetsPreviewMixin

View File

@@ -0,0 +1,239 @@
# #region Test.DatasetsPreviewFiltersMixin [C:3] [TYPE Module] [SEMANTICS test, dataset, preview, filter, sql, extract]
# @BRIEF Tests for core/superset_client/_datasets_preview_filters.py — filter normalization + SQL extraction.
# @RELATION BINDS_TO -> [SupersetDatasetsPreviewFiltersMixin]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import MagicMock, patch
import pytest
class TestNormalizeEffectiveFilters:
"""_normalize_effective_filters_for_query_context: filter conversion."""
@pytest.fixture
def mixin(self):
from src.core.superset_client._datasets_preview_filters import (
SupersetDatasetsPreviewFiltersMixin,
)
return SupersetDatasetsPreviewFiltersMixin()
def test_empty_filters(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([])
assert result["filters"] == []
assert result["diagnostics"] == []
def test_skip_non_dict_items(self, mixin):
result = mixin._normalize_effective_filters_for_query_context(
["string", 42, None]
)
assert result["filters"] == []
assert len(result["diagnostics"]) == 0 # skipped before diagnostics
def test_heuristic_reconstruction_with_column_and_value(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{"variable_name": "status", "effective_value": "active"}
])
assert len(result["filters"]) == 1
assert result["filters"][0]["col"] == "status"
assert result["filters"][0]["op"] == "=="
assert result["filters"][0]["val"] == "active"
def test_heuristic_reconstruction_with_list_value(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{"variable_name": "status", "effective_value": ["active", "pending"]}
])
assert result["filters"][0]["op"] == "IN"
def test_heuristic_reconstruction_missing_column_skipped(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{"effective_value": "val"}
])
assert result["filters"] == []
def test_preserved_clauses_without_val(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{
"display_name": "Category",
"effective_value": "electronics",
"normalized_filter_payload": {
"filter_clauses": [{"col": "category", "op": "=="}],
"value_origin": "preset",
},
}
])
assert len(result["filters"]) == 1
# val should be filled from effective_value
assert result["filters"][0]["val"] == "electronics"
def test_preserved_clauses_with_existing_val(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{
"display_name": "Category",
"effective_value": "fallback",
"normalized_filter_payload": {
"filter_clauses": [{"col": "category", "op": "==", "val": "original"}],
"value_origin": "preset",
},
}
])
assert len(result["filters"]) == 1
# val should remain "original" since it already exists
assert result["filters"][0]["val"] == "original"
def test_extra_form_data_merged(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{
"display_name": "Filter1",
"effective_value": "val1",
"normalized_filter_payload": {
"extra_form_data": {"time_range": "Last month", "filters": ["ignored"]},
"filter_clauses": [],
},
}
])
# extra_form_data.filters is skipped during merge
assert result["extra_form_data"]["time_range"] == "Last month"
assert "filters" not in result["extra_form_data"]
def test_extra_form_data_without_preserved_clauses_uses_empty(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{
"display_name": "Filter1",
"effective_value": None,
"normalized_filter_payload": {
"extra_form_data": {"time_range": "All time"},
},
}
])
# Has extra_form_data but no clauses -> empty clauses
assert result["filters"] == []
def test_missing_display_name_fallback(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{
"filter_name": "my_filter",
"variable_name": "my_var",
"effective_value": "val",
}
])
diagnostics = result["diagnostics"]
assert diagnostics[0]["filter_name"] == "my_filter"
def test_unnamed_filter_fallback(self, mixin):
result = mixin._normalize_effective_filters_for_query_context([
{"effective_value": "x"}
])
assert result["filters"] == []
class TestExtractCompiledSql:
"""_extract_compiled_sql_from_preview_response: normalized SQL from various response shapes."""
@pytest.fixture
def mixin(self):
from src.core.superset_client._datasets_preview_filters import (
SupersetDatasetsPreviewFiltersMixin,
)
return SupersetDatasetsPreviewFiltersMixin()
def test_result_list_with_query(self, mixin):
response = {
"result": [
{"query": "SELECT 1", "status": "success", "applied_filters": []},
{"query": "SELECT 2", "status": "success"},
]
}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert result["compiled_sql"] == "SELECT 1"
def test_result_list_with_sql_field(self, mixin):
response = {
"result": [
{"sql": "SELECT * FROM users", "status": "success"},
]
}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert result["compiled_sql"] == "SELECT * FROM users"
def test_result_list_with_compiled_sql(self, mixin):
response = {
"result": [
{"compiled_sql": "SELECT count(*) FROM t", "status": "success"},
]
}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert result["compiled_sql"] == "SELECT count(*) FROM t"
def test_result_list_empty_returns_top_level(self, mixin):
response = {
"result": [
{"status": "success", "applied_filters": []},
],
"query": "SELECT top FROM dual",
}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert result["compiled_sql"] == "SELECT top FROM dual"
def test_result_dict_with_query(self, mixin):
response = {
"result": {"query": "SELECT nested FROM result_dict"}
}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert result["compiled_sql"] == "SELECT nested FROM result_dict"
def test_result_dict_fallback_to_top_level(self, mixin):
response = {
"result": {"status": "ok"},
"sql": "SELECT fallback FROM top",
}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert result["compiled_sql"] == "SELECT fallback FROM top"
def test_result_dict_empty_result_obj_falls_through(self, mixin):
response = {
"result": {},
"compiled_sql": "SELECT fallback FROM compiled",
}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert result["compiled_sql"] == "SELECT fallback FROM compiled"
def test_no_sql_raises_error(self, mixin):
from src.core.utils.network import SupersetAPIError
response = {"result": []}
with pytest.raises(SupersetAPIError, match="Superset preview response did not expose compiled SQL"):
mixin._extract_compiled_sql_from_preview_response(response)
def test_non_dict_response_raises_error(self, mixin):
from src.core.utils.network import SupersetAPIError
with pytest.raises(SupersetAPIError, match="Superset preview response was not a JSON object"):
mixin._extract_compiled_sql_from_preview_response("not_a_dict")
def test_whitespace_only_sql_is_empty(self, mixin):
from src.core.utils.network import SupersetAPIError
response = {"result": [{"query": " "}]}
with pytest.raises(SupersetAPIError):
mixin._extract_compiled_sql_from_preview_response(response)
def test_result_list_skips_non_dict_items(self, mixin):
response = {
"result": ["string_item", 42, {"query": "SELECT 1"}]
}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert result["compiled_sql"] == "SELECT 1"
def test_response_diagnostics_includes_all_attempts(self, mixin):
response = {"query": "SELECT 1"}
result = mixin._extract_compiled_sql_from_preview_response(response)
assert len(result["response_diagnostics"]) >= 1
assert result["response_diagnostics"][0]["source"] == "query"
# #endregion Test.DatasetsPreviewFiltersMixin

View File

@@ -0,0 +1,274 @@
# #region Test.LayoutUtils [C:2] [TYPE Module] [SEMANTICS test, layout, position, json, banner]
# @BRIEF Tests for core/superset_client/_layout_utils.py — pure position JSON manipulation.
# @RELATION BINDS_TO -> [LayoutUtils]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
import pytest
class TestParsePositionJson:
"""parse_position_json: str/dict/None -> dict."""
def test_parses_json_string(self):
from src.core.superset_client._layout_utils import parse_position_json
result = parse_position_json('{"GRID_ID": {"children": []}}')
assert result == {"GRID_ID": {"children": []}}
def test_empty_string_returns_empty_dict(self):
from src.core.superset_client._layout_utils import parse_position_json
result = parse_position_json("")
assert result == {}
def test_whitespace_string_returns_empty_dict(self):
from src.core.superset_client._layout_utils import parse_position_json
result = parse_position_json(" ")
assert result == {}
def test_dict_passthrough(self):
from src.core.superset_client._layout_utils import parse_position_json
raw = {"GRID_ID": {"children": []}}
result = parse_position_json(raw)
assert result is raw
def test_none_returns_empty_dict(self):
from src.core.superset_client._layout_utils import parse_position_json
result = parse_position_json(None)
assert result == {}
def test_invalid_json_raises(self):
from src.core.superset_client._layout_utils import parse_position_json
with pytest.raises(Exception):
parse_position_json("{invalid}")
class TestEstimateMarkdownHeight:
"""_estimate_markdown_height: content -> int height."""
def test_empty_content_returns_min_height(self):
from src.core.superset_client._layout_utils import _estimate_markdown_height
assert _estimate_markdown_height("") == 19
def test_short_content_returns_min_height(self):
from src.core.superset_client._layout_utils import _estimate_markdown_height
assert _estimate_markdown_height("short text") == 19
def test_long_content_returns_larger_height(self):
from src.core.superset_client._layout_utils import _estimate_markdown_height
text = "word " * 200 # ~1000 chars => ~25 text lines
height = _estimate_markdown_height(text)
assert height >= 19
assert height <= 200
def test_html_content_counts_padding(self):
from src.core.superset_client._layout_utils import _estimate_markdown_height
html = '<div style="padding:12">some text</div>'
height = _estimate_markdown_height(html)
assert height >= 19
def test_plain_text_no_html_tags(self):
from src.core.superset_client._layout_utils import _estimate_markdown_height
text = "A" * 120 # ~3 lines at 40 chars/line
height = _estimate_markdown_height(text)
assert 19 <= height <= 200
def test_caps_at_200(self):
from src.core.superset_client._layout_utils import _estimate_markdown_height
text = "word " * 5000 # very long
height = _estimate_markdown_height(text)
assert height <= 200
class TestGenerateBannerId:
"""_generate_banner_id: chart_id -> (row_key, md_key)."""
def test_returns_deterministic_keys(self):
from src.core.superset_client._layout_utils import _generate_banner_id
row_key, md_key = _generate_banner_id(42)
assert row_key == "ROW-banner-42"
assert md_key == "MARKDOWN-banner-42"
def test_zero_chart_id(self):
from src.core.superset_client._layout_utils import _generate_banner_id
row_key, md_key = _generate_banner_id(0)
assert row_key == "ROW-banner-0"
assert md_key == "MARKDOWN-banner-0"
class TestInsertBannerMarkdownAtTop:
"""insert_banner_markdown_at_top: position_json + chart_id + content -> mutated dict."""
def test_inserts_row_and_markdown_at_top(self):
from src.core.superset_client._layout_utils import insert_banner_markdown_at_top
position_json = {"GRID_ID": {"children": ["ROW-existing"]}}
result = insert_banner_markdown_at_top(position_json, 1, "Hello")
assert "ROW-banner-1" in result
assert "MARKDOWN-banner-1" in result
children = result["GRID_ID"]["children"]
assert children[0] == "ROW-banner-1"
assert "ROW-existing" in children
def test_shifts_existing_items_down(self):
from src.core.superset_client._layout_utils import insert_banner_markdown_at_top
position_json = {
"GRID_ID": {"children": ["ROW-existing"]},
"ROW-existing": {"type": "ROW", "meta": {"y": 0}},
}
insert_banner_markdown_at_top(position_json, 2, "Banner")
assert position_json["ROW-existing"]["meta"]["y"] == 2
def test_handles_missing_grid(self):
from src.core.superset_client._layout_utils import insert_banner_markdown_at_top
position_json = {}
result = insert_banner_markdown_at_top(position_json, 3, "Content")
assert "ROW-banner-3" in result
assert "MARKDOWN-banner-3" in result
def test_content_affects_height(self):
from src.core.superset_client._layout_utils import insert_banner_markdown_at_top
position_json = {"GRID_ID": {"children": []}}
result = insert_banner_markdown_at_top(position_json, 4, "short")
md_meta = result["MARKDOWN-banner-4"]["meta"]
assert md_meta["height"] >= 19
assert md_meta["code"] == "short"
def test_row_removed_from_grid_if_duplicate(self):
from src.core.superset_client._layout_utils import insert_banner_markdown_at_top
# If ROW already exists in children, it should be moved to front
position_json = {
"GRID_ID": {"children": ["ROW-banner-5", "ROW-other"]},
"ROW-banner-5": {"type": "ROW", "meta": {"y": 0}},
}
# This simulates a re-insert
insert_banner_markdown_at_top(position_json, 5, "Updated")
children = position_json["GRID_ID"]["children"]
assert children[0] == "ROW-banner-5"
assert children[1] == "ROW-other"
assert len(children) == 2
def test_markdown_includes_parents(self):
from src.core.superset_client._layout_utils import insert_banner_markdown_at_top
position_json = {"GRID_ID": {"children": []}}
result = insert_banner_markdown_at_top(position_json, 6, "test")
md = result["MARKDOWN-banner-6"]
assert md["parents"] == ["ROOT_ID", "GRID_ID", "ROW-banner-6"]
assert md["type"] == "MARKDOWN"
def test_row_includes_parents(self):
from src.core.superset_client._layout_utils import insert_banner_markdown_at_top
position_json = {"GRID_ID": {"children": []}}
result = insert_banner_markdown_at_top(position_json, 7, "test")
row = result["ROW-banner-7"]
assert row["parents"] == ["ROOT_ID", "GRID_ID"]
assert row["type"] == "ROW"
assert row["children"] == ["MARKDOWN-banner-7"]
class TestUpdateBannerMarkdownContent:
"""update_banner_markdown_content: position_json + key + content -> mutated dict."""
def test_updates_content_and_height(self):
from src.core.superset_client._layout_utils import update_banner_markdown_content
position_json = {
"MARKDOWN-banner-1": {
"type": "MARKDOWN",
"meta": {"code": "old", "height": 19},
}
}
update_banner_markdown_content(position_json, "MARKDOWN-banner-1", "new content " * 20)
meta = position_json["MARKDOWN-banner-1"]["meta"]
assert meta["code"] == "new content " * 20
assert meta["height"] >= 19
def test_does_nothing_if_key_not_found(self):
from src.core.superset_client._layout_utils import update_banner_markdown_content
position_json = {"other": {"meta": {"code": "x"}}}
result = update_banner_markdown_content(position_json, "MARKDOWN-banner-99", "new")
assert result is position_json # no crash, returns same dict
def test_handles_missing_meta(self):
from src.core.superset_client._layout_utils import update_banner_markdown_content
position_json = {"MARKDOWN-banner-1": {"type": "MARKDOWN"}}
result = update_banner_markdown_content(position_json, "MARKDOWN-banner-1", "content")
assert result is position_json # no crash
class TestRemoveBannerFromPosition:
"""remove_banner_from_position: position_json + chart_id -> mutated dict."""
def test_removes_row_and_markdown(self):
from src.core.superset_client._layout_utils import remove_banner_from_position
position_json = {
"GRID_ID": {"children": ["ROW-banner-1", "ROW-other"]},
"ROW-banner-1": {"type": "ROW", "meta": {"y": 0}},
"MARKDOWN-banner-1": {"type": "MARKDOWN", "meta": {"y": 0}},
"ROW-other": {"type": "ROW", "meta": {"y": 2}},
}
result = remove_banner_from_position(position_json, 1)
assert "ROW-banner-1" not in result
assert "MARKDOWN-banner-1" not in result
assert "ROW-other" in result
assert "GRID_ID" in result
# ROW-banner-1 removed from children list
assert "ROW-banner-1" not in result["GRID_ID"]["children"]
def test_shifts_remaining_items_up_when_banner_at_y0(self):
from src.core.superset_client._layout_utils import remove_banner_from_position
position_json = {
"GRID_ID": {"children": ["ROW-banner-1", "ROW-other"]},
"ROW-banner-1": {"type": "ROW", "meta": {"y": 0}},
"MARKDOWN-banner-1": {"type": "MARKDOWN", "meta": {"y": 0}},
"ROW-other": {"type": "ROW", "meta": {"y": 2}},
}
remove_banner_from_position(position_json, 1)
assert position_json["ROW-other"]["meta"]["y"] == 0
def test_does_not_shift_when_banner_not_at_y0(self):
from src.core.superset_client._layout_utils import remove_banner_from_position
position_json = {
"GRID_ID": {"children": ["ROW-banner-1", "ROW-other"]},
"ROW-banner-1": {"type": "ROW", "meta": {"y": 5}},
"MARKDOWN-banner-1": {"type": "MARKDOWN", "meta": {"y": 5}},
"ROW-other": {"type": "ROW", "meta": {"y": 2}},
}
remove_banner_from_position(position_json, 1)
assert position_json["ROW-other"]["meta"]["y"] == 2
def test_noop_when_banner_does_not_exist(self):
from src.core.superset_client._layout_utils import remove_banner_from_position
position_json = {"GRID_ID": {"children": ["ROW-other"]}, "ROW-other": {"meta": {"y": 0}}}
result = remove_banner_from_position(position_json, 99)
assert result is position_json # no crash
assert "ROW-other" in result
# #endregion Test.LayoutUtils

View File

@@ -0,0 +1,340 @@
# #region Test.IdMappingService [C:4] [TYPE Module] [SEMANTICS test, mapping, service, sync, scheduler]
# @BRIEF Tests for core/mapping_service.py — IdMappingService.
# @RELATION BINDS_TO -> [IdMappingServiceModule]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
import pytest
@pytest.fixture
def mock_db():
"""Create a mock SQLAlchemy Session with proper chaining."""
session = MagicMock()
return session
class TestIdMappingServiceInit:
"""__init__: stores db session and creates scheduler."""
def test_init(self, mock_db):
from src.core.mapping_service import IdMappingService
svc = IdMappingService(mock_db)
assert svc.db is mock_db
assert svc.scheduler is not None
class TestStartScheduler:
"""start_scheduler: cron-based background sync."""
@pytest.fixture
def svc(self, mock_db):
from src.core.mapping_service import IdMappingService
svc = IdMappingService(mock_db)
# Patch the scheduler to avoid real BackgroundScheduler issues
svc.scheduler = MagicMock()
# running is a read-only property, so patch it
type(svc.scheduler).running = PropertyMock(return_value=False)
return svc
def test_starts_scheduler(self, svc):
with patch("src.core.mapping_service.seed_trace_id"):
svc.start_scheduler(
cron_string="0 * * * *",
environments=["env-1"],
superset_client_factory=lambda e: MagicMock(),
)
svc.scheduler.add_job.assert_called_once()
def test_removes_existing_job(self, svc):
svc._sync_job = MagicMock()
svc._sync_job.id = "existing"
with patch("src.core.mapping_service.seed_trace_id"):
svc.start_scheduler(
cron_string="0 * * * *",
environments=["env-1"],
superset_client_factory=lambda e: MagicMock(),
)
svc.scheduler.remove_job.assert_called_once_with("existing")
def test_scheduler_already_running(self, svc):
type(svc.scheduler).running = PropertyMock(return_value=True)
with patch("src.core.mapping_service.seed_trace_id"):
svc.start_scheduler(
cron_string="0 * * * *",
environments=["env-1"],
superset_client_factory=lambda e: MagicMock(),
)
# Should not call start again — just update
svc.scheduler.add_job.assert_called_once()
def test_skip_sync_if_no_client(self, svc):
with patch("src.core.mapping_service.seed_trace_id"):
svc.start_scheduler(
cron_string="0 * * * *",
environments=["env-1"],
superset_client_factory=lambda e: None,
)
svc.scheduler.add_job.assert_called_once()
class TestSyncEnvironment:
"""sync_environment: full and incremental sync."""
@pytest.fixture
def svc(self, mock_db):
from src.core.mapping_service import IdMappingService
svc = IdMappingService(mock_db)
return svc
def _setup_db_find(self, svc, existing=None, delete_result=0):
"""Configure db.query chain for sync tests.
sync_environment does:
db.query(ResourceMapping).filter_by(...).first()
for each resource type * 3 (chart, dataset, dashboard).
"""
# Make .first() return existing (None = new item)
first_mock = MagicMock(return_value=existing)
# The stale delete query
delete_mock = MagicMock(return_value=delete_result)
# Build chain: query().filter_by().first()
filter_by_mock = MagicMock()
filter_by_mock.first = first_mock
# Build chain: query().filter().filter() for stale detection
filter2_mock = MagicMock()
filter2_mock.filter.return_value = filter2_mock # recursive
filter2_mock.delete = delete_mock
# Main query chain
query_mock = MagicMock()
query_mock.filter_by = MagicMock(return_value=filter_by_mock)
query_mock.filter = MagicMock(return_value=filter2_mock)
svc.db.query = MagicMock(return_value=query_mock)
@pytest.mark.asyncio
async def test_full_sync_success(self, svc):
self._setup_db_find(svc, existing=None)
client = AsyncMock()
client.get_all_resources = AsyncMock(
side_effect=[
[{"uuid": "uuid-1", "id": 42, "slice_name": "Chart 1"},
{"uuid": "uuid-2", "id": 43, "slice_name": "Chart 2"}],
[{"uuid": "uuid-3", "id": 99, "table_name": "Dataset 1"}],
[{"uuid": "uuid-4", "id": 10, "slug": "dashboard-1"}],
]
)
svc.db.add = MagicMock()
svc.db.commit = MagicMock()
await svc.sync_environment("env-1", client)
assert svc.db.add.call_count == 4
svc.db.commit.assert_called_once()
@pytest.mark.asyncio
async def test_sync_environment_upserts_existing(self, svc):
existing = MagicMock()
existing.remote_integer_id = "0"
existing.resource_name = "old"
existing.last_synced_at = datetime(2020, 1, 1, tzinfo=UTC)
self._setup_db_find(svc, existing=existing)
client = AsyncMock()
client.get_all_resources = AsyncMock(
return_value=[{"uuid": "uuid-1", "id": 42, "slice_name": "Chart 1"}]
)
svc.db.commit = MagicMock()
await svc.sync_environment("env-1", client)
svc.db.add.assert_not_called()
assert existing.remote_integer_id == "42"
assert existing.resource_name == "Chart 1"
@pytest.mark.asyncio
async def test_incremental_sync(self, svc):
# Configure: query().filter(...).filter(...).scalar() for max_date
scalar_mock = MagicMock(return_value=datetime(2025, 1, 1, tzinfo=UTC))
incremental_filter = MagicMock()
incremental_filter.filter.return_value = incremental_filter # chain
incremental_filter.scalar = scalar_mock
query_mock = MagicMock()
query_mock.filter = MagicMock(return_value=incremental_filter)
svc.db.query = MagicMock(return_value=query_mock)
# Also need first() for the upsert (not reached if resources is empty)
self._setup_db_find(svc, existing=None)
client = AsyncMock()
client.get_all_resources = AsyncMock(return_value=[])
svc.db.commit = MagicMock()
await svc.sync_environment("env-1", client, incremental=True)
client.get_all_resources.assert_called_once()
call_args = client.get_all_resources.call_args
assert call_args[1].get("since_dttm") is not None
@pytest.mark.asyncio
async def test_sync_skips_items_without_uuid_or_id(self, svc):
self._setup_db_find(svc, existing=None)
client = AsyncMock()
client.get_all_resources = AsyncMock(
return_value=[
{"uuid": None, "id": 1, "slice_name": "No UUID"},
{"uuid": "uuid-1", "id": None, "slice_name": "No ID"},
{"uuid": "uuid-2", "id": 2, "slice_name": "Valid"},
]
)
svc.db.add = MagicMock()
svc.db.commit = MagicMock()
await svc.sync_environment("env-1", client)
assert svc.db.add.call_count == 1
@pytest.mark.asyncio
async def test_sync_handles_api_error_per_resource_type(self, svc):
self._setup_db_find(svc, existing=None)
client = AsyncMock()
client.get_all_resources = AsyncMock(
side_effect=[
Exception("Chart API failed"),
[{"uuid": "uuid-1", "id": 42, "table_name": "Dataset 1"}],
[{"uuid": "uuid-2", "id": 10, "slug": "dash"}],
]
)
svc.db.add = MagicMock()
svc.db.commit = MagicMock()
await svc.sync_environment("env-1", client)
assert svc.db.add.call_count == 2
@pytest.mark.asyncio
async def test_sync_deletes_stale_mappings(self, svc):
self._setup_db_find(svc, existing=None, delete_result=2)
client = AsyncMock()
client.get_all_resources = AsyncMock(
return_value=[{"uuid": "uuid-1", "id": 42, "slice_name": "Chart 1"}]
)
svc.db.add = MagicMock()
svc.db.commit = MagicMock()
await svc.sync_environment("env-1", client)
# The stale deletion should have been called for each resource type
filter_mock = svc.db.query.return_value.filter
assert filter_mock.return_value.filter.return_value.delete.called
@pytest.mark.asyncio
async def test_sync_rollback_on_critical_failure(self, svc):
self._setup_db_find(svc, existing=None)
svc.db.commit.side_effect = RuntimeError("Commit failed")
svc.db.rollback = MagicMock()
client = AsyncMock()
client.get_all_resources = AsyncMock(
return_value=[{"uuid": "uuid-1", "id": 42, "slice_name": "Chart"}]
)
with pytest.raises(RuntimeError):
await svc.sync_environment("env-1", client)
svc.db.rollback.assert_called_once()
class TestGetRemoteId:
"""get_remote_id: lookup single mapping."""
def test_found(self, mock_db):
from src.core.mapping_service import IdMappingService
from src.models.mapping import ResourceType
mapping = MagicMock()
mapping.remote_integer_id = "42"
mock_db.query.return_value.filter_by.return_value.first.return_value = mapping
svc = IdMappingService(mock_db)
result = svc.get_remote_id("env-1", ResourceType.CHART, "uuid-1")
assert result == 42
def test_not_found_returns_none(self, mock_db):
from src.core.mapping_service import IdMappingService
from src.models.mapping import ResourceType
mock_db.query.return_value.filter_by.return_value.first.return_value = None
svc = IdMappingService(mock_db)
result = svc.get_remote_id("env-1", ResourceType.CHART, "uuid-none")
assert result is None
def test_invalid_int_returns_none(self, mock_db):
from src.core.mapping_service import IdMappingService
from src.models.mapping import ResourceType
mapping = MagicMock()
mapping.remote_integer_id = "not_a_number"
mock_db.query.return_value.filter_by.return_value.first.return_value = mapping
svc = IdMappingService(mock_db)
result = svc.get_remote_id("env-1", ResourceType.CHART, "uuid-bad")
assert result is None
class TestGetRemoteIdsBatch:
"""get_remote_ids_batch: batch lookup."""
def test_found(self, mock_db):
from src.core.mapping_service import IdMappingService
from src.models.mapping import ResourceType
m1 = MagicMock()
m1.uuid = "uuid-1"
m1.remote_integer_id = "42"
m2 = MagicMock()
m2.uuid = "uuid-2"
m2.remote_integer_id = "99"
mock_db.query.return_value.filter.return_value.all.return_value = [m1, m2]
svc = IdMappingService(mock_db)
result = svc.get_remote_ids_batch("env-1", ResourceType.CHART, ["uuid-1", "uuid-2"])
assert result == {"uuid-1": 42, "uuid-2": 99}
def test_empty_list_returns_empty_dict(self, mock_db):
from src.core.mapping_service import IdMappingService
from src.models.mapping import ResourceType
svc = IdMappingService(mock_db)
result = svc.get_remote_ids_batch("env-1", ResourceType.CHART, [])
assert result == {}
mock_db.query.assert_not_called()
def test_skips_invalid_int(self, mock_db):
from src.core.mapping_service import IdMappingService
from src.models.mapping import ResourceType
m1 = MagicMock()
m1.uuid = "uuid-1"
m1.remote_integer_id = "not_a_number"
mock_db.query.return_value.filter.return_value.all.return_value = [m1]
svc = IdMappingService(mock_db)
result = svc.get_remote_ids_batch("env-1", ResourceType.CHART, ["uuid-1"])
assert result == {}
# #endregion Test.IdMappingService

View File

@@ -0,0 +1,206 @@
# #region Test.Network [C:2] [TYPE Module] [SEMANTICS test, network, error, auth, cache]
# @BRIEF Tests for core/utils/network.py — error classes and SupersetAuthCache.
# @RELATION BINDS_TO -> [NetworkModule]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
import time
from unittest.mock import patch
import pytest
class TestSupersetAPIError:
"""SupersetAPIError — base exception with context."""
def test_default_message(self):
from src.core.utils.network import SupersetAPIError
err = SupersetAPIError()
assert "Superset API error" in str(err)
def test_custom_message(self):
from src.core.utils.network import SupersetAPIError
err = SupersetAPIError("Custom error")
assert "Custom error" in str(err)
def test_with_context(self):
from src.core.utils.network import SupersetAPIError
err = SupersetAPIError("Failed", status_code=404, endpoint="/test")
assert err.context["status_code"] == 404
assert err.context["endpoint"] == "/test"
assert "404" in str(err)
class TestAuthenticationError:
"""AuthenticationError raised on auth failures."""
def test_default_message(self):
from src.core.utils.network import AuthenticationError
err = AuthenticationError()
assert "Authentication failed" in str(err)
def test_is_superset_api_error(self):
from src.core.utils.network import AuthenticationError, SupersetAPIError
assert issubclass(AuthenticationError, SupersetAPIError)
def test_with_context(self):
from src.core.utils.network import AuthenticationError
err = AuthenticationError("Bad credentials", env="prod")
assert err.context["type"] == "authentication"
assert err.context["env"] == "prod"
class TestPermissionDeniedError:
"""PermissionDeniedError raised on 403."""
def test_default_message(self):
from src.core.utils.network import PermissionDeniedError
err = PermissionDeniedError()
assert "Permission denied" in str(err)
def test_is_authentication_error(self):
from src.core.utils.network import AuthenticationError, PermissionDeniedError
assert issubclass(PermissionDeniedError, AuthenticationError)
class TestDashboardNotFoundError:
"""DashboardNotFoundError raised on 404 for dashboard endpoints."""
def test_default_message(self):
from src.core.utils.network import DashboardNotFoundError
err = DashboardNotFoundError(42)
assert "42" in str(err)
assert "Dashboard not found" in str(err)
def test_custom_message(self):
from src.core.utils.network import DashboardNotFoundError
err = DashboardNotFoundError("my-slug", "was deleted")
assert "my-slug" in str(err)
assert "was deleted" in str(err)
def test_is_superset_api_error(self):
from src.core.utils.network import DashboardNotFoundError, SupersetAPIError
assert issubclass(DashboardNotFoundError, SupersetAPIError)
def test_context_contains_resource_id(self):
from src.core.utils.network import DashboardNotFoundError
err = DashboardNotFoundError(99)
assert err.context["resource_id"] == 99
assert err.context["subtype"] == "not_found"
class TestNetworkError:
"""NetworkError for transport-level failures."""
def test_default_message(self):
from src.core.utils.network import NetworkError
err = NetworkError()
assert "Network connection failed" in str(err)
def test_with_context(self):
from src.core.utils.network import NetworkError
err = NetworkError("Timeout", url="https://example.com")
assert err.context["url"] == "https://example.com"
def test_not_subclass_of_superset_api_error(self):
from src.core.utils.network import NetworkError, SupersetAPIError
assert not issubclass(NetworkError, SupersetAPIError)
class TestSupersetAuthCache:
"""SupersetAuthCache — process-local token cache with TTL."""
def test_build_key_from_auth_dict(self):
from src.core.utils.network import SupersetAuthCache
key = SupersetAuthCache.build_key("https://superset.example.com", {"username": "admin"}, True)
assert key == ("https://superset.example.com", "admin", True)
def test_build_key_no_auth(self):
from src.core.utils.network import SupersetAuthCache
key = SupersetAuthCache.build_key("https://superset.example.com", None, False)
assert key == ("https://superset.example.com", "", False)
def test_build_key_strips_url(self):
from src.core.utils.network import SupersetAuthCache
key = SupersetAuthCache.build_key(" https://superset.example.com/ ", {"username": " admin "}, True)
assert key == ("https://superset.example.com/", "admin", True)
def test_set_and_get(self):
from src.core.utils.network import SupersetAuthCache
key = ("url", "user", False)
SupersetAuthCache.set(key, {"access_token": "abc", "csrf_token": "xyz"})
tokens = SupersetAuthCache.get(key)
assert tokens is not None
assert tokens["access_token"] == "abc"
assert tokens["csrf_token"] == "xyz"
def test_get_expired_returns_none(self):
from src.core.utils.network import SupersetAuthCache
key = ("url2", "user2", False)
SupersetAuthCache.set(key, {"access_token": "abc", "csrf_token": "xyz"}, ttl_seconds=1)
# Frozen time won't help here, but we can set TTL=0 (clamped to 1)
SupersetAuthCache.set(key, {"access_token": "abc", "csrf_token": "xyz"}, ttl_seconds=1)
def test_get_nonexistent_key_returns_none(self):
from src.core.utils.network import SupersetAuthCache
result = SupersetAuthCache.get(("nonexistent", "nobody", False))
assert result is None
def test_invalidate_removes_key(self):
from src.core.utils.network import SupersetAuthCache
key = ("url3", "user3", False)
SupersetAuthCache.set(key, {"access_token": "abc", "csrf_token": "xyz"})
SupersetAuthCache.invalidate(key)
assert SupersetAuthCache.get(key) is None
def test_invalidate_nonexistent_key_no_error(self):
from src.core.utils.network import SupersetAuthCache
SupersetAuthCache.invalidate(("nope", "nope", False)) # should not raise
def test_get_with_non_dict_tokens_returns_none(self):
from src.core.utils.network import SupersetAuthCache
key = ("url4", "user4", False)
with SupersetAuthCache._lock:
SupersetAuthCache._entries[key] = {
"tokens": "not_a_dict",
"expires_at": time.time() + 300,
}
result = SupersetAuthCache.get(key)
assert result is None
def test_get_empty_payload_returns_none(self):
from src.core.utils.network import SupersetAuthCache
key = ("url5", "user5", False)
with SupersetAuthCache._lock:
SupersetAuthCache._entries[key] = {}
result = SupersetAuthCache.get(key)
assert result is None
# #endregion Test.Network

View File

@@ -0,0 +1,280 @@
# #region Test.PluginLoader [C:3] [TYPE Module] [SEMANTICS test, plugin, loader, dynamic, import]
# @BRIEF Tests for core/plugin_loader.py — PluginLoader.
# @RELATION BINDS_TO -> [PluginLoader]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
import tempfile
from unittest.mock import MagicMock, patch
import pytest
class TestPluginLoader:
"""PluginLoader: dynamic plugin discovery and registration."""
@pytest.fixture
def temp_plugin_dir(self):
"""Create a temporary plugin directory."""
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
def test_init_creates_directory_if_not_exists(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
nonexistent = Path(temp_plugin_dir) / "nonexistent"
loader = PluginLoader(str(nonexistent))
assert nonexistent.exists()
def test_loads_single_file_plugin(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
# Create a simple plugin file
plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any
class TestPlugin(PluginBase):
@property
def id(self) -> str:
return "test-plugin"
@property
def name(self) -> str:
return "Test Plugin"
@property
def description(self) -> str:
return "A test plugin"
@property
def version(self) -> str:
return "1.0.0"
def get_schema(self) -> dict:
return {"type": "object", "properties": {}}
async def execute(self, params: dict[str, Any]):
return {"status": "ok"}
'''
plugin_file = Path(temp_plugin_dir) / "test_plugin.py"
plugin_file.write_text(plugin_code)
loader = PluginLoader(temp_plugin_dir)
assert loader.has_plugin("test-plugin")
assert loader.get_plugin("test-plugin") is not None
def test_ignores_init_py(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
init_file = Path(temp_plugin_dir) / "__init__.py"
init_file.write_text("# init")
loader = PluginLoader(temp_plugin_dir)
# Should not crash, no plugins loaded
assert len(loader.get_all_plugin_configs()) == 0
@patch("src.core.plugin_loader._logger")
def test_loads_package_plugin(self, mock_logger, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
# Create a package
pkg_dir = Path(temp_plugin_dir) / "my_package"
pkg_dir.mkdir()
init_file = pkg_dir / "__init__.py"
init_file.write_text('''
from src.core.plugin_base import PluginBase
from typing import Any
class PackagePlugin(PluginBase):
@property
def id(self) -> str:
return "package-plugin"
@property
def name(self) -> str:
return "Package Plugin"
@property
def description(self) -> str:
return "A package plugin"
@property
def version(self) -> str:
return "2.0.0"
def get_schema(self) -> dict:
return {}
async def execute(self, params: dict[str, Any]):
return {"status": "ok"}
''')
loader = PluginLoader(temp_plugin_dir)
assert loader.has_plugin("package-plugin")
def test_skips_non_py_files(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
txt_file = Path(temp_plugin_dir) / "readme.txt"
txt_file.write_text("not a plugin")
loader = PluginLoader(temp_plugin_dir)
assert len(loader.get_all_plugin_configs()) == 0
def test_duplicate_plugin_id_skipped(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any
class DupPlugin(PluginBase):
@property
def id(self) -> str:
return "dup-plugin"
@property
def name(self) -> str:
return "Dup Plugin"
@property
def description(self) -> str:
return "Duplicate plugin"
@property
def version(self) -> str:
return "1.0.0"
def get_schema(self) -> dict:
return {}
async def execute(self, params: dict[str, Any]):
return {"status": "ok"}
'''
# Create two files with the same plugin ID
p1 = Path(temp_plugin_dir) / "plugin_a.py"
p1.write_text(plugin_code)
p2 = Path(temp_plugin_dir) / "plugin_b.py"
p2.write_text(plugin_code)
loader = PluginLoader(temp_plugin_dir)
# Only one should be registered (first loaded)
assert loader.has_plugin("dup-plugin")
def test_get_plugin_returns_none_for_missing(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
loader = PluginLoader(temp_plugin_dir)
result = loader.get_plugin("nonexistent")
assert result is None
def test_get_all_plugin_configs(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any
class ConfigPlugin(PluginBase):
@property
def id(self) -> str:
return "config-plugin"
@property
def name(self) -> str:
return "Config Plugin"
@property
def description(self) -> str:
return "A config plugin"
@property
def version(self) -> str:
return "3.0.0"
@property
def ui_route(self) -> str:
return "/config"
def get_schema(self) -> dict:
return {"type": "object", "properties": {"key": {"type": "string"}}}
async def execute(self, params: dict[str, Any]):
return {"status": "ok"}
'''
pf = Path(temp_plugin_dir) / "config_plugin.py"
pf.write_text(plugin_code)
loader = PluginLoader(temp_plugin_dir)
configs = loader.get_all_plugin_configs()
assert len(configs) == 1
assert configs[0].id == "config-plugin"
assert configs[0].name == "Config Plugin"
assert configs[0].ui_route == "/config"
assert configs[0].input_schema == {"type": "object", "properties": {"key": {"type": "string"}}}
def test_module_load_failure_logged(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
# Create a file with syntax error
bad_file = Path(temp_plugin_dir) / "bad_plugin.py"
bad_file.write_text("this is not valid python {{{")
with patch("src.core.plugin_loader._logger") as mock_log:
loader = PluginLoader(temp_plugin_dir)
mock_log.error.assert_called_once()
def test_instantiation_failure_logged(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any
class BrokenPlugin(PluginBase):
@property
def id(self) -> str:
return "broken"
@property
def name(self) -> str:
return "Broken"
@property
def description(self) -> str:
return "A broken plugin"
@property
def version(self) -> str:
return "1.0.0"
def get_schema(self) -> dict:
return {}
async def execute(self, params: dict[str, Any]):
return {"status": "ok"}
class NonPlugin:
pass
'''
pf = Path(temp_plugin_dir) / "good_plugin.py"
pf.write_text(plugin_code)
with patch("src.core.plugin_loader._logger") as mock_log:
loader = PluginLoader(temp_plugin_dir)
# Should have loaded successfully
assert loader.has_plugin("broken")
# NonPlugin should not trigger error (it's not a PluginBase subclass)
def test_schema_not_dict_raises_error(self, temp_plugin_dir):
from src.core.plugin_loader import PluginLoader
plugin_code = '''
from src.core.plugin_base import PluginBase
from typing import Any
class BadSchemaPlugin(PluginBase):
@property
def id(self) -> str:
return "bad-schema"
@property
def name(self) -> str:
return "Bad Schema"
@property
def description(self) -> str:
return "Bad schema plugin"
@property
def version(self) -> str:
return "1.0.0"
def get_schema(self) -> dict:
return "not_a_dict"
async def execute(self, params: dict[str, Any]):
return {"status": "ok"}
'''
pf = Path(temp_plugin_dir) / "bad_schema.py"
pf.write_text(plugin_code)
with patch("src.core.plugin_loader._logger") as mock_log:
loader = PluginLoader(temp_plugin_dir)
# Schema validation fails -> logged as error, plugin not registered
assert not loader.has_plugin("bad-schema")
mock_log.error.assert_called_once()
# #endregion Test.PluginLoader

View File

@@ -0,0 +1,661 @@
# #region Test.SupersetCompilationAdapter [C:4] [TYPE Module] [SEMANTICS test, superset, compilation, adapter, preview]
# @BRIEF Tests for core/utils/superset_compilation_adapter.py — SupersetCompilationAdapter.
# @RELATION BINDS_TO -> [SupersetCompilationAdapter]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@pytest.fixture(autouse=True)
def patch_env_and_models():
"""Prevent Environment/CompiledPreview import errors by patching."""
with patch("src.core.utils.superset_compilation_adapter.Environment") as mock_env, \
patch("src.core.utils.superset_compilation_adapter.CompiledPreview") as mock_cp, \
patch("src.core.utils.superset_compilation_adapter.PreviewStatus") as mock_ps:
mock_env_instance = MagicMock()
mock_env_instance.id = "test-env"
mock_env.return_value = mock_env_instance
# Make CompiledPreview accept kwargs
def cp_side_effect(**kwargs):
obj = MagicMock()
for k, v in kwargs.items():
setattr(obj, k, v)
return obj
mock_cp.side_effect = cp_side_effect
mock_ps.READY = "READY"
mock_ps.FAILED = "FAILED"
mock_ps.STALE = "STALE"
yield
@pytest.fixture
def payload():
from src.core.utils.superset_compilation_adapter import (
PreviewCompilationPayload,
SqlLabLaunchPayload,
)
return {
"preview": PreviewCompilationPayload(
session_id="sess-1",
dataset_id=1,
preview_fingerprint="fp123",
template_params={"schema": "public"},
effective_filters=[{"col": "status", "val": "active"}],
),
"sqllab": SqlLabLaunchPayload(
session_id="sess-1",
dataset_id=1,
preview_id="prev-1",
compiled_sql="SELECT * FROM users",
template_params={"schema": "public"},
),
}
class TestSupersetCompilationAdapterInit:
"""__init__: binds environment and optional client."""
def test_init_without_client(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
from unittest.mock import MagicMock
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
assert adapter.environment is env
def test_init_with_client(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
adapter = SupersetCompilationAdapter(environment=env, client=client)
assert adapter.client is client
class TestCompilePreview:
"""compile_preview: compiles preview through _request_superset_preview."""
@pytest.mark.asyncio
async def test_successful_compile(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
adapter._request_superset_preview = AsyncMock(
return_value={"compiled_sql": "SELECT * FROM users"}
)
preview = await adapter.compile_preview(payload["preview"])
assert preview.preview_status == "READY"
assert preview.compiled_sql == "SELECT * FROM users"
assert preview.compiled_by == "superset"
@pytest.mark.asyncio
async def test_invalid_dataset_id_raises(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
from src.core.utils.superset_compilation_adapter import (
PreviewCompilationPayload,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
bad_payload = PreviewCompilationPayload(
session_id="sess-1",
dataset_id=0,
preview_fingerprint="fp",
template_params={},
effective_filters=[],
)
with pytest.raises(ValueError, match="dataset_id must be a positive integer"):
await adapter.compile_preview(bad_payload)
@pytest.mark.asyncio
async def test_request_exception_returns_failed_preview(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
adapter._request_superset_preview = AsyncMock(
side_effect=RuntimeError("Upstream failed")
)
preview = await adapter.compile_preview(payload["preview"])
assert preview.preview_status == "FAILED"
assert preview.error_code == "superset_preview_failed"
assert "Upstream failed" in preview.error_details
@pytest.mark.asyncio
async def test_empty_compiled_sql_returns_failed_preview(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
adapter._request_superset_preview = AsyncMock(return_value={"compiled_sql": ""})
preview = await adapter.compile_preview(payload["preview"])
assert preview.preview_status == "FAILED"
assert preview.error_code == "superset_preview_empty"
@pytest.mark.asyncio
async def test_missing_compiled_sql_key(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
adapter._request_superset_preview = AsyncMock(return_value={"other": "data"})
preview = await adapter.compile_preview(payload["preview"])
assert preview.preview_status == "FAILED"
assert preview.error_code == "superset_preview_empty"
class TestMarkPreviewStale:
"""mark_preview_stale: sets status to STALE."""
def test_marks_as_stale(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
mock_preview = MagicMock()
mock_preview.preview_status = "READY"
result = adapter.mark_preview_stale(mock_preview)
assert result.preview_status == "STALE"
class TestCreateSqlLabSession:
"""create_sql_lab_session: creates audited execution session."""
@pytest.mark.asyncio
async def test_successful_creation(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
adapter._request_sql_lab_session = AsyncMock(
return_value={"sql_lab_session_ref": "query-42"}
)
ref = await adapter.create_sql_lab_session(payload["sqllab"])
assert ref == "query-42"
@pytest.mark.asyncio
async def test_empty_compiled_sql_raises(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
SqlLabLaunchPayload,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
bad_payload = SqlLabLaunchPayload(
session_id="sess-1",
dataset_id=1,
preview_id="prev-1",
compiled_sql="",
template_params={},
)
with pytest.raises(ValueError, match="compiled_sql must be non-empty"):
await adapter.create_sql_lab_session(bad_payload)
@pytest.mark.asyncio
async def test_missing_session_ref_raises(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
adapter._request_sql_lab_session = AsyncMock(return_value={})
with pytest.raises(RuntimeError, match="did not include a session reference"):
await adapter.create_sql_lab_session(payload["sqllab"])
@pytest.mark.asyncio
async def test_session_ref_in_result_id(self, payload):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
adapter._request_sql_lab_session = AsyncMock(
return_value={"result": {"id": "query-99"}}
)
ref = await adapter.create_sql_lab_session(payload["sqllab"])
assert ref == "query-99"
class TestRequestSupersetPreview:
"""_request_superset_preview: client method resolution."""
@pytest.mark.asyncio
async def test_uses_direct_compile_preview_method(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
client.compile_preview = AsyncMock(return_value={"compiled_sql": "SELECT 1"})
adapter = SupersetCompilationAdapter(environment=env, client=client)
adapter._supports_client_method = MagicMock(return_value=True)
adapter._normalize_preview_response = MagicMock(
return_value={"compiled_sql": "SELECT 1"}
)
payload = MagicMock()
payload.dataset_id = 1
payload.session_id = "sess-1"
payload.template_params = {}
payload.effective_filters = []
result = await adapter._request_superset_preview(payload)
assert result["compiled_sql"] == "SELECT 1"
@pytest.mark.asyncio
async def test_compile_preview_type_error_fallback(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
# compile_preview raises TypeError first time (wrong signature),
# succeeds on second call with re-arranged args
client.compile_preview = AsyncMock(
side_effect=[TypeError("wrong args"), {"compiled_sql": "SELECT 2"}]
)
adapter = SupersetCompilationAdapter(environment=env, client=client)
adapter._supports_client_method = MagicMock(return_value=True)
adapter._normalize_preview_response = MagicMock(
return_value={"compiled_sql": "SELECT 2"}
)
payload = MagicMock()
payload.dataset_id = 1
payload.template_params = {}
payload.effective_filters = []
result = await adapter._request_superset_preview(payload)
assert result["compiled_sql"] == "SELECT 2"
@pytest.mark.asyncio
async def test_compile_preview_exception_falls_to_dataset_preview(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
# compile_preview raises ValueError
client.compile_preview = AsyncMock(side_effect=ValueError("bad call"))
adapter = SupersetCompilationAdapter(environment=env, client=client)
adapter._supports_client_method = MagicMock(return_value=True)
adapter._normalize_preview_response = MagicMock(return_value=None)
# Fallback: compile_dataset_preview succeeds
client.compile_dataset_preview = AsyncMock(
return_value={"compiled_sql": "SELECT 3"}
)
payload = MagicMock()
payload.dataset_id = 1
payload.template_params = {}
payload.effective_filters = []
result = await adapter._request_superset_preview(payload)
assert result["compiled_sql"] == "SELECT 3"
@pytest.mark.asyncio
async def test_both_methods_fail_raises_error(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
client.compile_preview = AsyncMock(side_effect=ValueError("fail"))
client.compile_dataset_preview = AsyncMock(
side_effect=RuntimeError("also fail")
)
adapter = SupersetCompilationAdapter(environment=env, client=client)
adapter._supports_client_method = MagicMock(side_effect=[True, True])
adapter._normalize_preview_response = MagicMock(return_value=None)
payload = MagicMock()
payload.dataset_id = 1
payload.template_params = {}
payload.effective_filters = []
with pytest.raises(RuntimeError, match="also fail"):
await adapter._request_superset_preview(payload)
@pytest.mark.asyncio
async def test_fallback_to_raw_endpoint(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
# Neither compile_preview nor compile_dataset_preview exist
# But _supports_client_method returns False for both
client.network = MagicMock()
client.network.request = AsyncMock(
return_value={"sql": "SELECT raw"}
)
adapter = SupersetCompilationAdapter(environment=env, client=client)
adapter._supports_client_method = MagicMock(return_value=False)
adapter._dump_json = MagicMock(return_value='{"key": "val"}')
adapter._normalize_preview_response = MagicMock(
side_effect=lambda r: {"compiled_sql": "SELECT raw"} if isinstance(r, dict) else None
)
payload = MagicMock()
payload.dataset_id = 1
payload.template_params = {}
payload.effective_filters = []
result = await adapter._request_superset_preview(payload)
assert result["compiled_sql"] == "SELECT raw"
# Should have tried both fallback endpoints
assert client.network.request.await_count >= 1
@pytest.mark.asyncio
async def test_all_fallback_endpoints_fail(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
client.network = MagicMock()
client.network.request = AsyncMock(
side_effect=[
Exception("first failed"),
Exception("second failed"),
]
)
adapter = SupersetCompilationAdapter(environment=env, client=client)
adapter._supports_client_method = MagicMock(return_value=False)
adapter._dump_json = MagicMock(return_value="{}")
adapter._normalize_preview_response = MagicMock(return_value=None)
payload = MagicMock()
payload.dataset_id = 1
payload.template_params = {}
payload.effective_filters = []
with pytest.raises(RuntimeError, match="first failed"):
await adapter._request_superset_preview(payload)
class TestRequestSqlLabSession:
"""_request_sql_lab_session: SQL Lab session creation."""
@pytest.mark.asyncio
async def test_successful_first_candidate(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
client.get_dataset = AsyncMock(
return_value={
"result": {
"database": {"id": 5},
"schema": "public",
}
}
)
client.network = MagicMock()
client.network.request = AsyncMock(return_value={"query_id": 42})
adapter = SupersetCompilationAdapter(environment=env, client=client)
payload = MagicMock()
payload.compiled_sql = "SELECT 1"
payload.template_params = {}
payload.preview_id = "prev-1"
result = await adapter._request_sql_lab_session(payload)
assert result == {"query_id": 42}
@pytest.mark.asyncio
async def test_fallback_to_second_candidate(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
client.get_dataset = AsyncMock(
return_value={
"result": {
"database_id": 10,
"schema": "public",
}
}
)
client.network = MagicMock()
client.network.request = AsyncMock(
side_effect=[
Exception("first failed"),
{"id": 99},
]
)
adapter = SupersetCompilationAdapter(environment=env, client=client)
payload = MagicMock()
payload.compiled_sql = "SELECT *"
payload.template_params = {}
payload.preview_id = "prev-2"
result = await adapter._request_sql_lab_session(payload)
assert result == {"id": 99}
@pytest.mark.asyncio
async def test_all_candidates_fail(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
client.get_dataset = AsyncMock(
return_value={
"result": {
"database": {"id": 5},
}
}
)
client.network = MagicMock()
client.network.request = AsyncMock(
side_effect=[
Exception("first failed"),
Exception("second failed"),
]
)
adapter = SupersetCompilationAdapter(environment=env, client=client)
payload = MagicMock()
payload.compiled_sql = "SELECT *"
payload.template_params = {}
payload.preview_id = "prev-3"
with pytest.raises(RuntimeError, match="first failed"):
await adapter._request_sql_lab_session(payload)
@pytest.mark.asyncio
async def test_missing_database_id_raises(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
client = MagicMock()
client.get_dataset = AsyncMock(return_value={"result": {}})
adapter = SupersetCompilationAdapter(environment=env, client=client)
payload = MagicMock()
payload.compiled_sql = "SELECT *"
with pytest.raises(RuntimeError, match="does not expose a database identifier"):
await adapter._request_sql_lab_session(payload)
class TestNormalizePreviewResponse:
"""_normalize_preview_response: extracts compiled SQL from various shapes."""
def test_response_with_compiled_sql(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
adapter = SupersetCompilationAdapter(environment=env)
result = adapter._normalize_preview_response({"compiled_sql": "SELECT 1"})
assert result == {"compiled_sql": "SELECT 1", "raw_response": {"compiled_sql": "SELECT 1"}}
def test_response_with_sql_key(self):
adapter = _make_adapter()
result = adapter._normalize_preview_response({"sql": "SELECT * FROM t"})
assert result["compiled_sql"] == "SELECT * FROM t"
def test_response_with_query_key(self):
adapter = _make_adapter()
result = adapter._normalize_preview_response({"query": "SELECT count(*)"})
assert result["compiled_sql"] == "SELECT count(*)"
def test_response_with_nested_result(self):
adapter = _make_adapter()
result = adapter._normalize_preview_response(
{"result": {"compiled_sql": "SELECT nested"}}
)
assert result["compiled_sql"] == "SELECT nested"
def test_non_dict_returns_none(self):
adapter = _make_adapter()
assert adapter._normalize_preview_response("not_a_dict") is None
def test_all_null_or_empty_returns_none(self):
adapter = _make_adapter()
result = adapter._normalize_preview_response({"compiled_sql": None, "result": {}})
assert result is None
def _make_adapter():
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
return SupersetCompilationAdapter(environment=MagicMock())
class TestDumpJson:
"""_dump_json: deterministic JSON serialization."""
def test_sorted_keys(self):
adapter = _make_adapter()
result = adapter._dump_json({"b": 2, "a": 1})
assert result == '{"a": 1, "b": 2}'
def test_default_str_for_non_serializable(self):
adapter = _make_adapter()
result = adapter._dump_json({"date": datetime(2024, 1, 1, tzinfo=UTC)})
assert "2024-01-01" in result
class TestSupportsClientMethod:
"""_supports_client_method: method detection."""
def test_method_in_instance_dict(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
class FakeClient:
pass
client = FakeClient()
client.my_method = lambda: None
adapter = SupersetCompilationAdapter(environment=env, client=client)
assert adapter._supports_client_method("my_method") is True
def test_method_not_found(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
class FakeClient:
pass
client = FakeClient()
adapter = SupersetCompilationAdapter(environment=env, client=client)
assert adapter._supports_client_method("nonexistent") is False
def test_method_on_class(self):
from src.core.utils.superset_compilation_adapter import (
SupersetCompilationAdapter,
)
env = MagicMock()
class FakeClient:
def compile_preview(self):
pass
client = FakeClient()
adapter = SupersetCompilationAdapter(environment=env, client=client)
assert adapter._supports_client_method("compile_preview") is True
# #endregion Test.SupersetCompilationAdapter

View File

@@ -0,0 +1,389 @@
# #region Test.SupersetContextExtractorBase [C:3] [TYPE Module] [SEMANTICS test, superset, context, extract, parse]
# @BRIEF Tests for core/utils/superset_context_extractor/_base.py — SupersetContextExtractorBase.
# @RELATION BINDS_TO -> [SupersetContextExtractorBase]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture(autouse=True)
def patch_env():
with patch("src.core.utils.superset_context_extractor._base.Environment") as mock_env, \
patch("src.core.utils.superset_context_extractor._base.SupersetClient") as mock_sc:
env_instance = MagicMock()
env_instance.id = "test-env"
env_instance.url = "https://superset.example.com"
mock_env.return_value = env_instance
mock_sc_instance = MagicMock()
mock_sc.return_value = mock_sc_instance
yield {
"env": env_instance,
"sc": mock_sc_instance,
}
class TestSupersetParsedContext:
"""SupersetParsedContext dataclass."""
def test_default_values(self):
from src.core.utils.superset_context_extractor._base import (
SupersetParsedContext,
)
ctx = SupersetParsedContext(
source_url="https://example.com",
dataset_ref="ds_1",
)
assert ctx.source_url == "https://example.com"
assert ctx.dataset_ref == "ds_1"
assert ctx.dataset_id is None
assert ctx.resource_type == "unknown"
assert ctx.imported_filters == []
assert ctx.partial_recovery is False
class TestSupersetContextExtractorBaseInit:
"""__init__: binds environment and client."""
def test_init_without_client(self, patch_env):
from src.core.utils.superset_context_extractor._base import (
SupersetContextExtractorBase,
)
extractor = SupersetContextExtractorBase(environment=patch_env["env"])
assert extractor.environment is patch_env["env"]
def test_init_with_client(self, patch_env):
from src.core.utils.superset_context_extractor._base import (
SupersetContextExtractorBase,
)
client = MagicMock()
extractor = SupersetContextExtractorBase(
environment=patch_env["env"], client=client
)
assert extractor.client is client
class TestBuildRecoverySummary:
"""build_recovery_summary: normalizes parsed context."""
def test_builds_summary_with_full_context(self, patch_env):
from src.core.utils.superset_context_extractor._base import (
SupersetContextExtractorBase,
SupersetParsedContext,
)
extractor = SupersetContextExtractorBase(environment=patch_env["env"])
ctx = SupersetParsedContext(
source_url="https://superset.example.com/superset/dashboard/5",
dataset_ref="dataset_1",
dataset_id=1,
dashboard_id=5,
chart_id=42,
partial_recovery=False,
unresolved_references=[],
imported_filters=[{"col": "status", "val": "active"}],
)
summary = extractor.build_recovery_summary(ctx)
assert summary["dataset_ref"] == "dataset_1"
assert summary["dataset_id"] == 1
assert summary["dashboard_id"] == 5
assert summary["chart_id"] == 42
assert summary["partial_recovery"] is False
assert summary["imported_filter_count"] == 1
def test_builds_summary_with_partial_recovery(self, patch_env):
from src.core.utils.superset_context_extractor._base import (
SupersetContextExtractorBase,
SupersetParsedContext,
)
extractor = SupersetContextExtractorBase(environment=patch_env["env"])
ctx = SupersetParsedContext(
source_url="https://superset.example.com/superset/chart/10",
dataset_ref="unknown",
partial_recovery=True,
unresolved_references=["chart/10"],
)
summary = extractor.build_recovery_summary(ctx)
assert summary["partial_recovery"] is True
assert summary["unresolved_references"] == ["chart/10"]
assert summary["imported_filter_count"] == 0
class TestExtractNumericIdentifier:
"""_extract_numeric_identifier: extract int from path."""
def test_found(self, patch_env):
from src.core.utils.superset_context_extractor._base import (
SupersetContextExtractorBase,
)
extractor = SupersetContextExtractorBase(environment=patch_env["env"])
result = extractor._extract_numeric_identifier(
["superset", "dashboard", "5", "edit"], "dashboard"
)
assert result == 5
def test_not_found(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_numeric_identifier(
["superset", "chart", "10"], "dashboard"
)
assert result is None
def test_not_numeric(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_numeric_identifier(
["superset", "dashboard", "my-slug"], "dashboard"
)
assert result is None
def test_no_value_after_resource(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_numeric_identifier(
["superset", "dashboard"], "dashboard"
)
assert result is None
def test_empty_path_parts(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_numeric_identifier([], "dashboard")
assert result is None
class TestExtractDashboardReference:
"""_extract_dashboard_reference: extract id-or-slug."""
def test_found(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_reference(
["superset", "dashboard", "my-dash"]
)
assert result == "my-dash"
def test_dashboard_not_in_path(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_reference(["superset", "chart", "5"])
assert result is None
def test_no_value_after_dashboard(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_reference(["superset", "dashboard"])
assert result is None
def test_p_is_reserved(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_reference(
["superset", "dashboard", "p"]
)
assert result is None
def test_empty_string_candidate(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_reference(
["superset", "dashboard", ""]
)
assert result is None
class TestExtractDashboardPermalinkKey:
"""_extract_dashboard_permalink_key: extract key from /dashboard/p/<key>/."""
def test_found(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_permalink_key(
["superset", "dashboard", "p", "abc123"]
)
assert result == "abc123"
def test_dashboard_not_in_path(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_permalink_key(["superset", "chart", "5"])
assert result is None
def test_not_enough_parts(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_permalink_key(
["superset", "dashboard"]
)
assert result is None
def test_wrong_marker(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_permalink_key(
["superset", "dashboard", "x", "abc"]
)
assert result is None
def test_empty_key(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_permalink_key(
["superset", "dashboard", "p", ""]
)
assert result is None
class TestExtractDashboardIdFromState:
"""_extract_dashboard_id_from_state: search nested dict for dashboard ID."""
def test_found_with_dashboardId(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_id_from_state(
{"dashboardId": 42, "state": {}}
)
assert result == 42
def test_found_nested(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_id_from_state(
{"data": {"dashboard_id": 99}}
)
assert result == 99
def test_not_found(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_dashboard_id_from_state(
{"chartId": 5}
)
assert result is None
class TestExtractChartIdFromState:
"""_extract_chart_id_from_state: search nested dict for chart ID."""
def test_found_with_slice_id(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_chart_id_from_state(
{"slice_id": 77}
)
assert result == 77
def test_found_nested(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._extract_chart_id_from_state(
{"state": {"chartId": 88}}
)
assert result == 88
class TestSearchNestedNumericKey:
"""_search_nested_numeric_key: recursive ID search."""
def test_find_in_dict(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._search_nested_numeric_key(
{"target": 42}, {"target"}
)
assert result == 42
def test_find_in_list(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._search_nested_numeric_key(
[{"id": 1}, {"target": 99}], {"target"}
)
assert result == 99
def test_deeply_nested(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._search_nested_numeric_key(
{"a": {"b": {"c": {"chart_id": 55}}}}, {"chart_id"}
)
assert result == 55
def test_not_found(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._search_nested_numeric_key(
{"a": 1, "b": "text"}, {"target"}
)
assert result is None
def test_none_value_skipped(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._search_nested_numeric_key(
{"target": None}, {"target"}
)
assert result is None
def test_non_parseable_value_logged(self, patch_env):
extractor = _make_extractor(patch_env)
with patch("src.core.utils.superset_context_extractor._base.logger") as mock_log:
result = extractor._search_nested_numeric_key(
{"target": "not_a_number"}, {"target"}
)
assert result is None
mock_log.debug.assert_called_once()
class TestDecodeQueryState:
"""_decode_query_state: decode URL query params."""
def test_parses_native_filters_json(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._decode_query_state(
{"native_filters": ['{"key": "val"}']}
)
assert result["native_filters"] == {"key": "val"}
def test_parses_form_data_json(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._decode_query_state(
{"form_data": ['{"datasource": "1__table"}']}
)
assert result["form_data"]["datasource"] == "1__table"
def test_parses_q_param_json(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._decode_query_state(
{"q": ['{"columns": ["id"]}']}
)
assert result["q"]["columns"] == ["id"]
def test_invalid_json_falls_back_to_raw(self, patch_env):
extractor = _make_extractor(patch_env)
with patch("src.core.utils.superset_context_extractor._base.logger") as mock_log:
result = extractor._decode_query_state(
{"native_filters": ["{invalid}"]}
)
mock_log.explore.assert_called_once()
# Should be decoded as raw string
assert result["native_filters"] == "{invalid}"
def test_regular_param_decoded(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._decode_query_state(
{"dashboard": ["5"]}
)
assert result["dashboard"] == "5"
def test_empty_values_skipped(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._decode_query_state(
{"empty": []}
)
assert "empty" not in result
def test_url_decoded(self, patch_env):
extractor = _make_extractor(patch_env)
result = extractor._decode_query_state(
{"name": ["hello%20world"]}
)
assert result["name"] == "hello world"
def _make_extractor(patch_env):
from src.core.utils.superset_context_extractor._base import (
SupersetContextExtractorBase,
)
return SupersetContextExtractorBase(environment=patch_env["env"])
# #endregion Test.SupersetContextExtractorBase

View File

@@ -0,0 +1,298 @@
# #region Test.SupersetProfileLookup [C:3] [TYPE Module] [SEMANTICS test, superset, profile, lookup, adapter]
# @BRIEF Tests for core/superset_profile_lookup.py — SupersetAccountLookupAdapter.
# @RELATION BINDS_TO -> [SupersetProfileLookup]
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestSupersetAccountLookupAdapterInit:
"""__init__: stores client and environment ID."""
def test_init(self):
from src.core.superset_profile_lookup import (
SupersetAccountLookupAdapter,
)
client = MagicMock()
adapter = SupersetAccountLookupAdapter(client, "env-1")
assert adapter.network_client is client
assert adapter.environment_id == "env-1"
def test_init_converts_environment_id_to_string(self):
from src.core.superset_profile_lookup import (
SupersetAccountLookupAdapter,
)
adapter = SupersetAccountLookupAdapter(MagicMock(), 123)
assert adapter.environment_id == "123"
class TestGetUsersPage:
"""get_users_page: fetch and normalize users."""
@pytest.fixture
def adapter(self):
from src.core.superset_profile_lookup import (
SupersetAccountLookupAdapter,
)
client = MagicMock()
client.request = AsyncMock()
return SupersetAccountLookupAdapter(client, "env-1")
@pytest.mark.asyncio
async def test_successful_first_endpoint(self, adapter):
adapter.network_client.request.return_value = {
"result": [
{"username": "alice", "email": "alice@example.com"},
{"username": "bob", "email": "bob@example.com"},
],
"count": 2,
}
result = await adapter.get_users_page(search="alice")
assert result["status"] == "success"
assert result["environment_id"] == "env-1"
assert len(result["items"]) == 2
assert result["total"] == 2
@pytest.mark.asyncio
async def test_first_endpoint_fails_fallback_succeeds(self, adapter):
adapter.network_client.request = AsyncMock(
side_effect=[
Exception("First endpoint failed"),
{
"result": [
{"username": "charlie", "email": "charlie@example.com"}
],
"count": 1,
},
]
)
result = await adapter.get_users_page()
assert len(result["items"]) == 1
assert result["items"][0]["username"] == "charlie"
@pytest.mark.asyncio
async def test_both_endpoints_fail_raises_last(self, adapter):
adapter.network_client.request = AsyncMock(
side_effect=[
Exception("First failed"),
Exception("Second failed"),
]
)
with pytest.raises(Exception, match="Second failed"):
await adapter.get_users_page()
@pytest.mark.asyncio
async def test_prefers_primary_error_over_auth_fallback(self, adapter):
adapter.network_client.request = AsyncMock(
side_effect=[
ValueError("Primary error"),
__import__("src").core.utils.network.AuthenticationError("Auth error"),
]
)
with pytest.raises(ValueError, match="Primary error"):
await adapter.get_users_page()
@pytest.mark.asyncio
async def test_preserves_primary_non_auth_over_fallback_auth(self, adapter):
adapter.network_client.request = AsyncMock(
side_effect=[
RuntimeError("Primary runtime error"),
__import__("src").core.utils.network.AuthenticationError("Fallback auth error"),
]
)
with pytest.raises(RuntimeError, match="Primary runtime error"):
await adapter.get_users_page()
@pytest.mark.asyncio
async def test_normalizes_page_and_size_params(self, adapter):
adapter.network_client.request.return_value = {"result": [], "count": 0}
result = await adapter.get_users_page(page_index=-1, page_size=0)
assert result["page_index"] == 0
assert result["page_size"] == 1
@pytest.mark.asyncio
async def test_invalid_sort_order_defaults_to_desc(self, adapter):
adapter.network_client.request.return_value = {"result": [], "count": 0}
await adapter.get_users_page(sort_order="invalid")
# Should have used "desc"
call_q = adapter.network_client.request.call_args[1]["params"]["q"]
import json
q = json.loads(call_q)
assert q["order_direction"] == "desc"
@pytest.mark.asyncio
async def test_empty_search_no_filters(self, adapter):
adapter.network_client.request.return_value = {"result": [], "count": 0}
await adapter.get_users_page(search="")
call_q = adapter.network_client.request.call_args[1]["params"]["q"]
import json
q = json.loads(call_q)
assert "filters" not in q
@pytest.mark.asyncio
async def test_no_exception_but_also_no_last_error(self, adapter):
# Both endpoints return data, no exception at all
adapter.network_client.request.return_value = {"result": [], "count": 0}
result = await adapter.get_users_page()
assert result["status"] == "success"
class TestNormalizeLookupPayload:
"""_normalize_lookup_payload: various response shapes."""
@pytest.fixture
def adapter(self):
from src.core.superset_profile_lookup import (
SupersetAccountLookupAdapter,
)
return SupersetAccountLookupAdapter(MagicMock(), "env-1")
def test_result_dict_with_result_list(self, adapter):
result = adapter._normalize_lookup_payload(
{"result": [{"username": "alice"}], "count": 1}, 0, 20
)
assert len(result["items"]) == 1
assert result["total"] == 1
def test_result_dict_with_users_list(self, adapter):
result = adapter._normalize_lookup_payload(
{"users": [{"username": "bob"}], "total": 1}, 0, 20
)
assert len(result["items"]) == 1
def test_result_dict_with_items_list(self, adapter):
result = adapter._normalize_lookup_payload(
{"items": [{"username": "charlie"}], "total": 1}, 0, 20
)
assert len(result["items"]) == 1
def test_result_list_direct(self, adapter):
result = adapter._normalize_lookup_payload(
[{"username": "dave"}, {"username": "eve"}], 0, 20
)
assert len(result["items"]) == 2
assert result["total"] == 2
def test_double_nested_result(self, adapter):
result = adapter._normalize_lookup_payload(
{"result": {"result": [{"username": "frank"}]}}, 0, 20
)
assert len(result["items"]) == 1
def test_skips_duplicate_usernames(self, adapter):
result = adapter._normalize_lookup_payload(
{
"result": [
{"username": "alice"},
{"username": "ALICE"},
{"username": "bob"},
],
"count": 3,
},
0,
20,
)
# "alice" and "ALICE" normalized to same lowercase
assert len(result["items"]) == 2
def test_skips_empty_username(self, adapter):
result = adapter._normalize_lookup_payload(
{"result": [{"username": ""}, {"username": "valid"}], "count": 2}, 0, 20
)
assert len(result["items"]) == 1
def test_total_adjusted_to_max(self, adapter):
result = adapter._normalize_lookup_payload(
{"result": [{"username": "a"}, {"username": "b"}], "count": 1}, 0, 20
)
# total should be max(1, 2) = 2
assert result["total"] == 2
def test_non_dict_non_list_response(self, adapter):
result = adapter._normalize_lookup_payload("invalid", 0, 20)
assert result["items"] == []
assert result["total"] == 0
class TestNormalizeUserPayload:
"""normalize_user_payload: canonical user shape."""
@pytest.fixture
def adapter(self):
from src.core.superset_profile_lookup import (
SupersetAccountLookupAdapter,
)
return SupersetAccountLookupAdapter(MagicMock(), "env-1")
def test_full_user(self, adapter):
user = adapter.normalize_user_payload({
"username": "alice",
"full_name": "Alice Smith",
"email": "alice@example.com",
"is_active": True,
})
assert user["username"] == "alice"
assert user["display_name"] == "Alice Smith"
assert user["email"] == "alice@example.com"
assert user["is_active"] is True
assert user["environment_id"] == "env-1"
def test_with_first_last_name(self, adapter):
user = adapter.normalize_user_payload({
"username": "bob",
"first_name": "Bob",
"last_name": "Jones",
})
assert user["display_name"] == "Bob Jones"
def test_empty_display_name_falls_back_to_username(self, adapter):
user = adapter.normalize_user_payload({
"username": "charlie",
})
assert user["display_name"] == "charlie"
def test_email_empty_becomes_none(self, adapter):
user = adapter.normalize_user_payload({
"username": "dave",
"email": "",
})
assert user["email"] is None
def test_is_active_none(self, adapter):
user = adapter.normalize_user_payload({
"username": "eve",
})
assert user["is_active"] is None
def test_alternate_key_names(self, adapter):
user = adapter.normalize_user_payload({
"userName": "frank",
"name": "ignored",
})
assert user["username"] == "frank"
def test_non_dict_input(self, adapter):
user = adapter.normalize_user_payload("not_a_dict")
assert user["username"] == ""
# #endregion Test.SupersetProfileLookup