fix: commit verified semantic repair changes
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
# [DEF:backend.tests.core.test_mapping_service:Module]
|
||||
# [DEF:TestMappingService:Module]
|
||||
#
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.
|
||||
# @LAYER: Domain
|
||||
# @RELATION: VERIFIES -> backend.src.core.mapping_service
|
||||
# @RELATION: VERIFIES ->[src.core.mapping_service.IdMappingService]
|
||||
#
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
@@ -21,16 +21,18 @@ if backend_dir not in sys.path:
|
||||
from src.models.mapping import Base, ResourceMapping, ResourceType
|
||||
from src.core.mapping_service import IdMappingService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
# In-memory SQLite for testing
|
||||
engine = create_engine('sqlite:///:memory:')
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(engine)
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.close()
|
||||
|
||||
|
||||
class MockSupersetClient:
|
||||
def __init__(self, resources):
|
||||
self.resources = resources
|
||||
@@ -38,16 +40,25 @@ class MockSupersetClient:
|
||||
def get_all_resources(self, endpoint, since_dttm=None):
|
||||
return self.resources.get(endpoint, [])
|
||||
|
||||
|
||||
# [DEF:test_sync_environment_upserts_correctly:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_sync_environment_upserts_correctly(db_session):
|
||||
service = IdMappingService(db_session)
|
||||
mock_client = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 42, "uuid": "123e4567-e89b-12d3-a456-426614174000", "slice_name": "Test Chart"}
|
||||
]
|
||||
})
|
||||
mock_client = MockSupersetClient(
|
||||
{
|
||||
"chart": [
|
||||
{
|
||||
"id": 42,
|
||||
"uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"slice_name": "Test Chart",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
service.sync_environment("test-env", mock_client)
|
||||
|
||||
|
||||
mapping = db_session.query(ResourceMapping).first()
|
||||
assert mapping is not None
|
||||
assert mapping.environment_id == "test-env"
|
||||
@@ -56,6 +67,12 @@ def test_sync_environment_upserts_correctly(db_session):
|
||||
assert mapping.remote_integer_id == "42"
|
||||
assert mapping.resource_name == "Test Chart"
|
||||
|
||||
|
||||
# [/DEF:test_sync_environment_upserts_correctly:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_remote_id_returns_integer:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_get_remote_id_returns_integer(db_session):
|
||||
service = IdMappingService(db_session)
|
||||
mapping = ResourceMapping(
|
||||
@@ -64,7 +81,7 @@ def test_get_remote_id_returns_integer(db_session):
|
||||
uuid="uuid-1",
|
||||
remote_integer_id="99",
|
||||
resource_name="Test DS",
|
||||
last_synced_at=datetime.now(timezone.utc)
|
||||
last_synced_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db_session.add(mapping)
|
||||
db_session.commit()
|
||||
@@ -72,80 +89,126 @@ def test_get_remote_id_returns_integer(db_session):
|
||||
result = service.get_remote_id("test-env", ResourceType.DATASET, "uuid-1")
|
||||
assert result == 99
|
||||
|
||||
|
||||
# [/DEF:test_get_remote_id_returns_integer:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_remote_ids_batch_returns_dict:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_get_remote_ids_batch_returns_dict(db_session):
|
||||
service = IdMappingService(db_session)
|
||||
m1 = ResourceMapping(
|
||||
environment_id="test-env",
|
||||
resource_type=ResourceType.DASHBOARD,
|
||||
uuid="uuid-1",
|
||||
remote_integer_id="11"
|
||||
remote_integer_id="11",
|
||||
)
|
||||
m2 = ResourceMapping(
|
||||
environment_id="test-env",
|
||||
resource_type=ResourceType.DASHBOARD,
|
||||
uuid="uuid-2",
|
||||
remote_integer_id="22"
|
||||
remote_integer_id="22",
|
||||
)
|
||||
db_session.add_all([m1, m2])
|
||||
db_session.commit()
|
||||
|
||||
result = service.get_remote_ids_batch("test-env", ResourceType.DASHBOARD, ["uuid-1", "uuid-2", "uuid-missing"])
|
||||
|
||||
result = service.get_remote_ids_batch(
|
||||
"test-env", ResourceType.DASHBOARD, ["uuid-1", "uuid-2", "uuid-missing"]
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result["uuid-1"] == 11
|
||||
assert result["uuid-2"] == 22
|
||||
assert "uuid-missing" not in result
|
||||
|
||||
|
||||
# [/DEF:test_get_remote_ids_batch_returns_dict:Function]
|
||||
|
||||
|
||||
# [DEF:test_sync_environment_updates_existing_mapping:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_sync_environment_updates_existing_mapping(db_session):
|
||||
"""Verify that sync_environment updates an existing mapping (upsert UPDATE path)."""
|
||||
from src.models.mapping import ResourceMapping
|
||||
|
||||
# Pre-populate a mapping
|
||||
existing = ResourceMapping(
|
||||
environment_id="test-env",
|
||||
resource_type=ResourceType.CHART,
|
||||
uuid="123e4567-e89b-12d3-a456-426614174000",
|
||||
remote_integer_id="10",
|
||||
resource_name="Old Name"
|
||||
resource_name="Old Name",
|
||||
)
|
||||
db_session.add(existing)
|
||||
db_session.commit()
|
||||
|
||||
service = IdMappingService(db_session)
|
||||
mock_client = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 42, "uuid": "123e4567-e89b-12d3-a456-426614174000", "slice_name": "Updated Name"}
|
||||
]
|
||||
})
|
||||
mock_client = MockSupersetClient(
|
||||
{
|
||||
"chart": [
|
||||
{
|
||||
"id": 42,
|
||||
"uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"slice_name": "Updated Name",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
service.sync_environment("test-env", mock_client)
|
||||
|
||||
mapping = db_session.query(ResourceMapping).filter_by(
|
||||
uuid="123e4567-e89b-12d3-a456-426614174000"
|
||||
).first()
|
||||
mapping = (
|
||||
db_session.query(ResourceMapping)
|
||||
.filter_by(uuid="123e4567-e89b-12d3-a456-426614174000")
|
||||
.first()
|
||||
)
|
||||
assert mapping.remote_integer_id == "42"
|
||||
assert mapping.resource_name == "Updated Name"
|
||||
# Should still be only one record (updated, not duplicated)
|
||||
count = db_session.query(ResourceMapping).count()
|
||||
assert count == 1
|
||||
|
||||
|
||||
# [/DEF:test_sync_environment_updates_existing_mapping:Function]
|
||||
|
||||
|
||||
# [DEF:test_sync_environment_skips_resources_without_uuid:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_sync_environment_skips_resources_without_uuid(db_session):
|
||||
"""Resources missing uuid or having id=None should be silently skipped."""
|
||||
service = IdMappingService(db_session)
|
||||
mock_client = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 42, "slice_name": "No UUID"}, # Missing 'uuid' -> skipped
|
||||
{"id": None, "uuid": "valid-uuid", "slice_name": "ID is None"}, # id=None -> skipped
|
||||
{"id": None, "uuid": None, "slice_name": "Both None"}, # both None -> skipped
|
||||
]
|
||||
})
|
||||
mock_client = MockSupersetClient(
|
||||
{
|
||||
"chart": [
|
||||
{"id": 42, "slice_name": "No UUID"}, # Missing 'uuid' -> skipped
|
||||
{
|
||||
"id": None,
|
||||
"uuid": "valid-uuid",
|
||||
"slice_name": "ID is None",
|
||||
}, # id=None -> skipped
|
||||
{
|
||||
"id": None,
|
||||
"uuid": None,
|
||||
"slice_name": "Both None",
|
||||
}, # both None -> skipped
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
service.sync_environment("test-env", mock_client)
|
||||
|
||||
count = db_session.query(ResourceMapping).count()
|
||||
assert count == 0
|
||||
|
||||
|
||||
# [/DEF:test_sync_environment_skips_resources_without_uuid:Function]
|
||||
|
||||
|
||||
# [DEF:test_sync_environment_handles_api_error_gracefully:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_sync_environment_handles_api_error_gracefully(db_session):
|
||||
"""If one resource type fails, others should still sync."""
|
||||
|
||||
class FailingClient:
|
||||
def get_all_resources(self, endpoint, since_dttm=None):
|
||||
if endpoint == "chart":
|
||||
@@ -162,12 +225,24 @@ def test_sync_environment_handles_api_error_gracefully(db_session):
|
||||
mapping = db_session.query(ResourceMapping).first()
|
||||
assert mapping.resource_type == ResourceType.DATASET
|
||||
|
||||
|
||||
# [/DEF:test_sync_environment_handles_api_error_gracefully:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_remote_id_returns_none_for_missing:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_get_remote_id_returns_none_for_missing(db_session):
|
||||
"""get_remote_id should return None when no mapping exists."""
|
||||
service = IdMappingService(db_session)
|
||||
result = service.get_remote_id("test-env", ResourceType.CHART, "nonexistent-uuid")
|
||||
assert result is None
|
||||
|
||||
|
||||
# [/DEF:test_get_remote_id_returns_none_for_missing:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_remote_ids_batch_returns_empty_for_empty_input:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_get_remote_ids_batch_returns_empty_for_empty_input(db_session):
|
||||
"""get_remote_ids_batch should return {} for an empty list of UUIDs."""
|
||||
service = IdMappingService(db_session)
|
||||
@@ -175,70 +250,90 @@ def test_get_remote_ids_batch_returns_empty_for_empty_input(db_session):
|
||||
assert result == {}
|
||||
|
||||
|
||||
# [/DEF:test_get_remote_ids_batch_returns_empty_for_empty_input:Function]
|
||||
|
||||
|
||||
# [DEF:test_mapping_service_alignment_with_test_data:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_mapping_service_alignment_with_test_data(db_session):
|
||||
"""**@TEST_DATA**: Verifies that the service aligns with the resource_mapping_record contract."""
|
||||
# Contract: {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}
|
||||
contract_data = {
|
||||
'environment_id': 'prod-env-1',
|
||||
'resource_type': ResourceType.CHART,
|
||||
'uuid': '123e4567-e89b-12d3-a456-426614174000',
|
||||
'remote_integer_id': '42'
|
||||
"environment_id": "prod-env-1",
|
||||
"resource_type": ResourceType.CHART,
|
||||
"uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"remote_integer_id": "42",
|
||||
}
|
||||
|
||||
|
||||
mapping = ResourceMapping(**contract_data)
|
||||
db_session.add(mapping)
|
||||
db_session.commit()
|
||||
|
||||
service = IdMappingService(db_session)
|
||||
result = service.get_remote_id(
|
||||
contract_data['environment_id'],
|
||||
contract_data['resource_type'],
|
||||
contract_data['uuid']
|
||||
contract_data["environment_id"],
|
||||
contract_data["resource_type"],
|
||||
contract_data["uuid"],
|
||||
)
|
||||
|
||||
|
||||
assert result == 42
|
||||
|
||||
|
||||
# [/DEF:test_mapping_service_alignment_with_test_data:Function]
|
||||
|
||||
|
||||
# [DEF:test_sync_environment_requires_existing_env:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_sync_environment_requires_existing_env(db_session):
|
||||
"""**@PRE**: Verify behavior when environment_id is invalid/missing in DB.
|
||||
Note: The current implementation doesn't strictly check for environment existencia in the DB
|
||||
Note: The current implementation doesn't strictly check for environment existencia in the DB
|
||||
before polling, but it should handle it gracefully or follow the contract.
|
||||
"""
|
||||
service = IdMappingService(db_session)
|
||||
mock_client = MockSupersetClient({"chart": []})
|
||||
|
||||
# Even if environment doesn't exist in a hypothetical 'environments' table,
|
||||
|
||||
# Even if environment doesn't exist in a hypothetical 'environments' table,
|
||||
# the service should still complete or fail according to defined error handling.
|
||||
# In GRACE-Poly, @PRE is a hard requirement. If we don't have an Env model check,
|
||||
# we simulate the intent.
|
||||
|
||||
|
||||
service.sync_environment("non-existent-env", mock_client)
|
||||
# If no error raised, at least verify no mappings were created for other envs
|
||||
assert db_session.query(ResourceMapping).count() == 0
|
||||
|
||||
|
||||
# [/DEF:test_sync_environment_requires_existing_env:Function]
|
||||
|
||||
|
||||
# [DEF:test_sync_environment_deletes_stale_mappings:Function]
|
||||
# @RELATION: BINDS_TO ->[TestMappingService]
|
||||
def test_sync_environment_deletes_stale_mappings(db_session):
|
||||
"""Verify that mappings for resources deleted from the remote environment
|
||||
are removed from the local DB on the next sync cycle."""
|
||||
service = IdMappingService(db_session)
|
||||
|
||||
# First sync: 2 charts exist
|
||||
client_v1 = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
||||
{"id": 2, "uuid": "bbb", "slice_name": "Chart B"},
|
||||
]
|
||||
})
|
||||
client_v1 = MockSupersetClient(
|
||||
{
|
||||
"chart": [
|
||||
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
||||
{"id": 2, "uuid": "bbb", "slice_name": "Chart B"},
|
||||
]
|
||||
}
|
||||
)
|
||||
service.sync_environment("env1", client_v1)
|
||||
assert db_session.query(ResourceMapping).filter_by(environment_id="env1").count() == 2
|
||||
assert (
|
||||
db_session.query(ResourceMapping).filter_by(environment_id="env1").count() == 2
|
||||
)
|
||||
|
||||
# Second sync: user deleted Chart B from superset
|
||||
client_v2 = MockSupersetClient({
|
||||
"chart": [
|
||||
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
||||
]
|
||||
})
|
||||
client_v2 = MockSupersetClient(
|
||||
{
|
||||
"chart": [
|
||||
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
||||
]
|
||||
}
|
||||
)
|
||||
service.sync_environment("env1", client_v2)
|
||||
|
||||
remaining = db_session.query(ResourceMapping).filter_by(environment_id="env1").all()
|
||||
@@ -246,4 +341,5 @@ def test_sync_environment_deletes_stale_mappings(db_session):
|
||||
assert remaining[0].uuid == "aaa"
|
||||
|
||||
|
||||
# [/DEF:backend.tests.core.test_mapping_service:Module]
|
||||
# [/DEF:test_sync_environment_deletes_stale_mappings:Function]
|
||||
# [/DEF:TestMappingService:Module]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# [DEF:backend.tests.test_dashboards_api:Module]
|
||||
# [DEF:TestDashboardsApi:Module]
|
||||
# @RELATION: VERIFIES ->[src.api.routes.dashboards]
|
||||
# @COMPLEXITY: 3
|
||||
# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API
|
||||
# @LAYER: Domain (Tests)
|
||||
@@ -8,8 +9,20 @@ from fastapi.testclient import TestClient
|
||||
from unittest.mock import MagicMock, patch, AsyncMock
|
||||
from datetime import datetime, timezone
|
||||
from src.app import app
|
||||
from src.api.routes.dashboards import DashboardsResponse, DashboardDetailResponse, DashboardTaskHistoryResponse, DatabaseMappingsResponse
|
||||
from src.dependencies import get_current_user, has_permission, get_config_manager, get_task_manager, get_resource_service, get_mapping_service
|
||||
from src.api.routes.dashboards import (
|
||||
DashboardsResponse,
|
||||
DashboardDetailResponse,
|
||||
DashboardTaskHistoryResponse,
|
||||
DatabaseMappingsResponse,
|
||||
)
|
||||
from src.dependencies import (
|
||||
get_current_user,
|
||||
has_permission,
|
||||
get_config_manager,
|
||||
get_task_manager,
|
||||
get_resource_service,
|
||||
get_mapping_service,
|
||||
)
|
||||
|
||||
# Global mock user
|
||||
mock_user = MagicMock()
|
||||
@@ -19,55 +32,73 @@ admin_role = MagicMock()
|
||||
admin_role.name = "Admin"
|
||||
mock_user.roles.append(admin_role)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_deps():
|
||||
config_manager = MagicMock()
|
||||
task_manager = MagicMock()
|
||||
resource_service = MagicMock()
|
||||
mapping_service = MagicMock()
|
||||
|
||||
|
||||
app.dependency_overrides[get_config_manager] = lambda: config_manager
|
||||
app.dependency_overrides[get_task_manager] = lambda: task_manager
|
||||
app.dependency_overrides[get_resource_service] = lambda: resource_service
|
||||
app.dependency_overrides[get_mapping_service] = lambda: mapping_service
|
||||
app.dependency_overrides[get_current_user] = lambda: mock_user
|
||||
|
||||
|
||||
# Overrides for specific permission checks
|
||||
app.dependency_overrides[has_permission("plugin:migration", "READ")] = lambda: mock_user
|
||||
app.dependency_overrides[has_permission("plugin:migration", "EXECUTE")] = lambda: mock_user
|
||||
app.dependency_overrides[has_permission("plugin:backup", "EXECUTE")] = lambda: mock_user
|
||||
app.dependency_overrides[has_permission("plugin:migration", "READ")] = (
|
||||
lambda: mock_user
|
||||
)
|
||||
app.dependency_overrides[has_permission("plugin:migration", "EXECUTE")] = (
|
||||
lambda: mock_user
|
||||
)
|
||||
app.dependency_overrides[has_permission("plugin:backup", "EXECUTE")] = (
|
||||
lambda: mock_user
|
||||
)
|
||||
app.dependency_overrides[has_permission("tasks", "READ")] = lambda: mock_user
|
||||
app.dependency_overrides[has_permission("dashboards", "READ")] = lambda: mock_user
|
||||
|
||||
|
||||
yield {
|
||||
"config": config_manager,
|
||||
"task": task_manager,
|
||||
"resource": resource_service,
|
||||
"mapping": mapping_service
|
||||
"mapping": mapping_service,
|
||||
}
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# --- 1. get_dashboards tests ---
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_success:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboards_success(mock_deps):
|
||||
"""Uses @TEST_FIXTURE: dashboard_list_happy data."""
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_deps["task"].get_all_tasks.return_value = []
|
||||
|
||||
|
||||
# @TEST_FIXTURE: dashboard_list_happy -> {"id": 1, "title": "Main Revenue"}
|
||||
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
||||
{"id": 1, "title": "Main Revenue", "slug": "main-revenue", "git_status": {"branch": "main", "sync_status": "OK"}}
|
||||
])
|
||||
mock_deps["resource"].get_dashboards_with_status = AsyncMock(
|
||||
return_value=[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Main Revenue",
|
||||
"slug": "main-revenue",
|
||||
"git_status": {"branch": "main", "sync_status": "OK"},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
response = client.get("/api/dashboards?env_id=prod&page=1&page_size=10")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
|
||||
# exhaustive @POST assertions
|
||||
assert "dashboards" in data
|
||||
assert len(data["dashboards"]) == 1 # @TEST_FIXTURE: expected_count: 1
|
||||
@@ -76,26 +107,40 @@ def test_get_dashboards_success(mock_deps):
|
||||
assert data["page"] == 1
|
||||
assert data["page_size"] == 10
|
||||
assert data["total_pages"] == 1
|
||||
|
||||
|
||||
# schema validation
|
||||
DashboardsResponse(**data)
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_with_search:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboards_with_search(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_deps["task"].get_all_tasks.return_value = []
|
||||
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[
|
||||
{"id": 1, "title": "Sales Report", "slug": "sales"},
|
||||
{"id": 2, "title": "Marketing", "slug": "marketing"}
|
||||
])
|
||||
|
||||
mock_deps["resource"].get_dashboards_with_status = AsyncMock(
|
||||
return_value=[
|
||||
{"id": 1, "title": "Sales Report", "slug": "sales"},
|
||||
{"id": 2, "title": "Marketing", "slug": "marketing"},
|
||||
]
|
||||
)
|
||||
|
||||
response = client.get("/api/dashboards?env_id=prod&search=sales")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["dashboards"]) == 1
|
||||
assert data["dashboards"][0]["title"] == "Sales Report"
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_with_search:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_empty:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboards_empty(mock_deps):
|
||||
"""@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}"""
|
||||
mock_env = MagicMock()
|
||||
@@ -103,7 +148,7 @@ def test_get_dashboards_empty(mock_deps):
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_deps["task"].get_all_tasks.return_value = []
|
||||
mock_deps["resource"].get_dashboards_with_status = AsyncMock(return_value=[])
|
||||
|
||||
|
||||
response = client.get("/api/dashboards?env_id=empty_env")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
@@ -112,6 +157,12 @@ def test_get_dashboards_empty(mock_deps):
|
||||
assert data["total_pages"] == 1
|
||||
DashboardsResponse(**data)
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_empty:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_superset_failure:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboards_superset_failure(mock_deps):
|
||||
"""@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}"""
|
||||
mock_env = MagicMock()
|
||||
@@ -121,42 +172,62 @@ def test_get_dashboards_superset_failure(mock_deps):
|
||||
mock_deps["resource"].get_dashboards_with_status = AsyncMock(
|
||||
side_effect=Exception("Connection refused")
|
||||
)
|
||||
|
||||
|
||||
response = client.get("/api/dashboards?env_id=bad_conn")
|
||||
assert response.status_code == 503
|
||||
assert "Failed to fetch dashboards" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_superset_failure:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_env_not_found:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboards_env_not_found(mock_deps):
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.get("/api/dashboards?env_id=nonexistent")
|
||||
assert response.status_code == 404
|
||||
assert "Environment not found" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_env_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_invalid_pagination:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboards_invalid_pagination(mock_deps):
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
|
||||
|
||||
# page < 1
|
||||
assert client.get("/api/dashboards?env_id=prod&page=0").status_code == 400
|
||||
assert client.get("/api/dashboards?env_id=prod&page=-1").status_code == 400
|
||||
|
||||
|
||||
# page_size < 1
|
||||
assert client.get("/api/dashboards?env_id=prod&page_size=0").status_code == 400
|
||||
|
||||
|
||||
# page_size > 100
|
||||
assert client.get("/api/dashboards?env_id=prod&page_size=101").status_code == 400
|
||||
|
||||
|
||||
# --- 2. get_database_mappings tests ---
|
||||
|
||||
# [/DEF:test_get_dashboards_invalid_pagination:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_database_mappings_success:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_database_mappings_success(mock_deps):
|
||||
mock_s = MagicMock(); mock_s.id = "s"
|
||||
mock_t = MagicMock(); mock_t.id = "t"
|
||||
mock_s = MagicMock()
|
||||
mock_s.id = "s"
|
||||
mock_t = MagicMock()
|
||||
mock_t.id = "t"
|
||||
mock_deps["config"].get_environments.return_value = [mock_s, mock_t]
|
||||
|
||||
mock_deps["mapping"].get_suggestions = AsyncMock(return_value=[
|
||||
{"source_db": "src", "target_db": "dst", "confidence": 0.9}
|
||||
])
|
||||
|
||||
mock_deps["mapping"].get_suggestions = AsyncMock(
|
||||
return_value=[{"source_db": "src", "target_db": "dst", "confidence": 0.9}]
|
||||
)
|
||||
response = client.get("/api/dashboards/db-mappings?source_env_id=s&target_env_id=t")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
@@ -164,23 +235,41 @@ def test_get_database_mappings_success(mock_deps):
|
||||
assert data["mappings"][0]["confidence"] == 0.9
|
||||
DatabaseMappingsResponse(**data)
|
||||
|
||||
|
||||
# [/DEF:test_get_database_mappings_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_database_mappings_env_not_found:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_database_mappings_env_not_found(mock_deps):
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.get("/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t")
|
||||
response = client.get(
|
||||
"/api/dashboards/db-mappings?source_env_id=ghost&target_env_id=t"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# --- 3. get_dashboard_detail tests ---
|
||||
|
||||
# [/DEF:test_get_database_mappings_env_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_detail_success:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboard_detail_success(mock_deps):
|
||||
with patch("src.api.routes.dashboards.SupersetClient") as mock_client_cls:
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
|
||||
|
||||
mock_client = MagicMock()
|
||||
detail_payload = {
|
||||
"id": 42, "title": "Detail", "charts": [], "datasets": [],
|
||||
"chart_count": 0, "dataset_count": 0
|
||||
"id": 42,
|
||||
"title": "Detail",
|
||||
"charts": [],
|
||||
"datasets": [],
|
||||
"chart_count": 0,
|
||||
"dataset_count": 0,
|
||||
}
|
||||
mock_client.get_dashboard_detail.return_value = detail_payload
|
||||
mock_client_cls.return_value = mock_client
|
||||
@@ -191,16 +280,36 @@ def test_get_dashboard_detail_success(mock_deps):
|
||||
assert data["id"] == 42
|
||||
DashboardDetailResponse(**data)
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboard_detail_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_detail_env_not_found:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboard_detail_env_not_found(mock_deps):
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.get("/api/dashboards/42?env_id=missing")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# --- 4. get_dashboard_tasks_history tests ---
|
||||
|
||||
# [/DEF:test_get_dashboard_detail_env_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_tasks_history_success:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboard_tasks_history_success(mock_deps):
|
||||
now = datetime.now(timezone.utc)
|
||||
task1 = MagicMock(id="t1", plugin_id="superset-backup", status="SUCCESS", started_at=now, finished_at=None, params={"env": "prod", "dashboards": [42]}, result={})
|
||||
task1 = MagicMock(
|
||||
id="t1",
|
||||
plugin_id="superset-backup",
|
||||
status="SUCCESS",
|
||||
started_at=now,
|
||||
finished_at=None,
|
||||
params={"env": "prod", "dashboards": [42]},
|
||||
result={},
|
||||
)
|
||||
mock_deps["task"].get_all_tasks.return_value = [task1]
|
||||
|
||||
response = client.get("/api/dashboards/42/tasks?env_id=prod")
|
||||
@@ -210,20 +319,39 @@ def test_get_dashboard_tasks_history_success(mock_deps):
|
||||
assert len(data["items"]) == 1
|
||||
DashboardTaskHistoryResponse(**data)
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboard_tasks_history_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_tasks_history_sorting:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboard_tasks_history_sorting(mock_deps):
|
||||
"""@POST: Response contains sorted task history (newest first)."""
|
||||
from datetime import timedelta
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
older = now - timedelta(hours=2)
|
||||
newest = now
|
||||
|
||||
task_old = MagicMock(id="t-old", plugin_id="superset-backup", status="SUCCESS",
|
||||
started_at=older, finished_at=None,
|
||||
params={"env": "prod", "dashboards": [42]}, result={})
|
||||
task_new = MagicMock(id="t-new", plugin_id="superset-backup", status="RUNNING",
|
||||
started_at=newest, finished_at=None,
|
||||
params={"env": "prod", "dashboards": [42]}, result={})
|
||||
|
||||
|
||||
task_old = MagicMock(
|
||||
id="t-old",
|
||||
plugin_id="superset-backup",
|
||||
status="SUCCESS",
|
||||
started_at=older,
|
||||
finished_at=None,
|
||||
params={"env": "prod", "dashboards": [42]},
|
||||
result={},
|
||||
)
|
||||
task_new = MagicMock(
|
||||
id="t-new",
|
||||
plugin_id="superset-backup",
|
||||
status="RUNNING",
|
||||
started_at=newest,
|
||||
finished_at=None,
|
||||
params={"env": "prod", "dashboards": [42]},
|
||||
result={},
|
||||
)
|
||||
|
||||
# Provide in wrong order to verify the endpoint sorts
|
||||
mock_deps["task"].get_all_tasks.return_value = [task_old, task_new]
|
||||
|
||||
@@ -235,38 +363,67 @@ def test_get_dashboard_tasks_history_sorting(mock_deps):
|
||||
assert data["items"][0]["id"] == "t-new"
|
||||
assert data["items"][1]["id"] == "t-old"
|
||||
|
||||
|
||||
# --- 5. get_dashboard_thumbnail tests ---
|
||||
|
||||
# [/DEF:test_get_dashboard_tasks_history_sorting:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_thumbnail_success:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboard_thumbnail_success(mock_deps):
|
||||
with patch("src.api.routes.dashboards.SupersetClient") as mock_client_cls:
|
||||
mock_env = MagicMock(); mock_env.id = "prod"
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock(status_code=200, content=b"img", headers={"Content-Type": "image/png"})
|
||||
mock_client.network.request.side_effect = lambda method, endpoint, **kw: {"image_url": "url"} if method == "POST" else mock_response
|
||||
mock_response = MagicMock(
|
||||
status_code=200, content=b"img", headers={"Content-Type": "image/png"}
|
||||
)
|
||||
mock_client.network.request.side_effect = (
|
||||
lambda method, endpoint, **kw: {"image_url": "url"}
|
||||
if method == "POST"
|
||||
else mock_response
|
||||
)
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
response = client.get("/api/dashboards/42/thumbnail?env_id=prod")
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"img"
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboard_thumbnail_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_thumbnail_env_not_found:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboard_thumbnail_env_not_found(mock_deps):
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.get("/api/dashboards/42/thumbnail?env_id=missing")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboard_thumbnail_env_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboard_thumbnail_202:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_get_dashboard_thumbnail_202(mock_deps):
|
||||
"""@POST: Returns 202 when thumbnail is being prepared by Superset."""
|
||||
with patch("src.api.routes.dashboards.SupersetClient") as mock_client_cls:
|
||||
mock_env = MagicMock(); mock_env.id = "prod"
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_client = MagicMock()
|
||||
|
||||
|
||||
# POST cache_dashboard_screenshot returns image_url
|
||||
mock_client.network.request.side_effect = [
|
||||
{"image_url": "/api/v1/dashboard/42/thumbnail/abc123/"}, # POST
|
||||
MagicMock(status_code=202, json=lambda: {"message": "Thumbnail is being generated"},
|
||||
headers={"Content-Type": "application/json"}) # GET thumbnail -> 202
|
||||
MagicMock(
|
||||
status_code=202,
|
||||
json=lambda: {"message": "Thumbnail is being generated"},
|
||||
headers={"Content-Type": "application/json"},
|
||||
), # GET thumbnail -> 202
|
||||
]
|
||||
mock_client_cls.return_value = mock_client
|
||||
|
||||
@@ -274,93 +431,156 @@ def test_get_dashboard_thumbnail_202(mock_deps):
|
||||
assert response.status_code == 202
|
||||
assert "Thumbnail is being generated" in response.json()["message"]
|
||||
|
||||
|
||||
# --- 6. migrate_dashboards tests ---
|
||||
|
||||
# [/DEF:test_get_dashboard_thumbnail_202:Function]
|
||||
|
||||
|
||||
# [DEF:test_migrate_dashboards_success:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_migrate_dashboards_success(mock_deps):
|
||||
mock_s = MagicMock(); mock_s.id = "s"
|
||||
mock_t = MagicMock(); mock_t.id = "t"
|
||||
mock_s = MagicMock()
|
||||
mock_s.id = "s"
|
||||
mock_t = MagicMock()
|
||||
mock_t.id = "t"
|
||||
mock_deps["config"].get_environments.return_value = [mock_s, mock_t]
|
||||
mock_deps["task"].create_task = AsyncMock(return_value=MagicMock(id="task-123"))
|
||||
|
||||
response = client.post("/api/dashboards/migrate", json={
|
||||
"source_env_id": "s", "target_env_id": "t", "dashboard_ids": [1]
|
||||
})
|
||||
response = client.post(
|
||||
"/api/dashboards/migrate",
|
||||
json={"source_env_id": "s", "target_env_id": "t", "dashboard_ids": [1]},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["task_id"] == "task-123"
|
||||
|
||||
|
||||
# [/DEF:test_migrate_dashboards_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_migrate_dashboards_pre_checks:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_migrate_dashboards_pre_checks(mock_deps):
|
||||
# Missing IDs
|
||||
response = client.post("/api/dashboards/migrate", json={
|
||||
"source_env_id": "s", "target_env_id": "t", "dashboard_ids": []
|
||||
})
|
||||
response = client.post(
|
||||
"/api/dashboards/migrate",
|
||||
json={"source_env_id": "s", "target_env_id": "t", "dashboard_ids": []},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "At least one dashboard ID must be provided" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_migrate_dashboards_pre_checks:Function]
|
||||
|
||||
|
||||
# [DEF:test_migrate_dashboards_env_not_found:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_migrate_dashboards_env_not_found(mock_deps):
|
||||
"""@PRE: source_env_id and target_env_id are valid environment IDs."""
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.post("/api/dashboards/migrate", json={
|
||||
"source_env_id": "ghost", "target_env_id": "t", "dashboard_ids": [1]
|
||||
})
|
||||
response = client.post(
|
||||
"/api/dashboards/migrate",
|
||||
json={"source_env_id": "ghost", "target_env_id": "t", "dashboard_ids": [1]},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert "Source environment not found" in response.json()["detail"]
|
||||
|
||||
|
||||
# --- 7. backup_dashboards tests ---
|
||||
|
||||
# [/DEF:test_migrate_dashboards_env_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_backup_dashboards_success:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_backup_dashboards_success(mock_deps):
|
||||
mock_env = MagicMock(); mock_env.id = "prod"
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_deps["task"].create_task = AsyncMock(return_value=MagicMock(id="backup-123"))
|
||||
|
||||
response = client.post("/api/dashboards/backup", json={
|
||||
"env_id": "prod", "dashboard_ids": [1]
|
||||
})
|
||||
response = client.post(
|
||||
"/api/dashboards/backup", json={"env_id": "prod", "dashboard_ids": [1]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["task_id"] == "backup-123"
|
||||
|
||||
|
||||
# [/DEF:test_backup_dashboards_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_backup_dashboards_pre_checks:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_backup_dashboards_pre_checks(mock_deps):
|
||||
response = client.post("/api/dashboards/backup", json={
|
||||
"env_id": "prod", "dashboard_ids": []
|
||||
})
|
||||
response = client.post(
|
||||
"/api/dashboards/backup", json={"env_id": "prod", "dashboard_ids": []}
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# [/DEF:test_backup_dashboards_pre_checks:Function]
|
||||
|
||||
|
||||
# [DEF:test_backup_dashboards_env_not_found:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_backup_dashboards_env_not_found(mock_deps):
|
||||
"""@PRE: env_id is a valid environment ID."""
|
||||
mock_deps["config"].get_environments.return_value = []
|
||||
response = client.post("/api/dashboards/backup", json={
|
||||
"env_id": "ghost", "dashboard_ids": [1]
|
||||
})
|
||||
response = client.post(
|
||||
"/api/dashboards/backup", json={"env_id": "ghost", "dashboard_ids": [1]}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
assert "Environment not found" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_backup_dashboards_env_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_backup_dashboards_with_schedule:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_backup_dashboards_with_schedule(mock_deps):
|
||||
"""@POST: If schedule is provided, a scheduled task is created."""
|
||||
mock_env = MagicMock(); mock_env.id = "prod"
|
||||
mock_env = MagicMock()
|
||||
mock_env.id = "prod"
|
||||
mock_deps["config"].get_environments.return_value = [mock_env]
|
||||
mock_deps["task"].create_task = AsyncMock(return_value=MagicMock(id="sched-456"))
|
||||
|
||||
response = client.post("/api/dashboards/backup", json={
|
||||
"env_id": "prod", "dashboard_ids": [1], "schedule": "0 0 * * *"
|
||||
})
|
||||
response = client.post(
|
||||
"/api/dashboards/backup",
|
||||
json={"env_id": "prod", "dashboard_ids": [1], "schedule": "0 0 * * *"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["task_id"] == "sched-456"
|
||||
|
||||
|
||||
# Verify schedule was propagated to create_task
|
||||
call_kwargs = mock_deps["task"].create_task.call_args
|
||||
task_params = call_kwargs.kwargs.get("params") or call_kwargs[1].get("params", {})
|
||||
assert task_params["schedule"] == "0 0 * * *"
|
||||
|
||||
|
||||
# --- 8. Internal logic: _task_matches_dashboard ---
|
||||
# [/DEF:test_backup_dashboards_with_schedule:Function]
|
||||
|
||||
from src.api.routes.dashboards import _task_matches_dashboard
|
||||
|
||||
|
||||
# [DEF:test_task_matches_dashboard_logic:Function]
|
||||
# @RELATION: BINDS_TO ->[TestDashboardsApi]
|
||||
def test_task_matches_dashboard_logic():
|
||||
task = MagicMock(plugin_id="superset-backup", params={"dashboards": [42], "env": "prod"})
|
||||
task = MagicMock(
|
||||
plugin_id="superset-backup", params={"dashboards": [42], "env": "prod"}
|
||||
)
|
||||
assert _task_matches_dashboard(task, 42, "prod") is True
|
||||
assert _task_matches_dashboard(task, 43, "prod") is False
|
||||
assert _task_matches_dashboard(task, 42, "dev") is False
|
||||
|
||||
llm_task = MagicMock(plugin_id="llm_dashboard_validation", params={"dashboard_id": 42, "environment_id": "prod"})
|
||||
llm_task = MagicMock(
|
||||
plugin_id="llm_dashboard_validation",
|
||||
params={"dashboard_id": 42, "environment_id": "prod"},
|
||||
)
|
||||
assert _task_matches_dashboard(llm_task, 42, "prod") is True
|
||||
assert _task_matches_dashboard(llm_task, 42, None) is True
|
||||
|
||||
# [/DEF:backend.tests.test_dashboards_api:Module]
|
||||
|
||||
# [/DEF:test_task_matches_dashboard_logic:Function]
|
||||
# [/DEF:TestDashboardsApi:Module]
|
||||
|
||||
Reference in New Issue
Block a user