fix: finalize semantic repair and test updates
This commit is contained in:
@@ -1,42 +1,71 @@
|
||||
# [DEF:TestResourceHubs:Module]
|
||||
# @RELATION: BELONGS_TO -> SrcRoot
|
||||
# @COMPLEXITY: 3
|
||||
# @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api
|
||||
# @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.
|
||||
# @LAYER: Domain (Tests)
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
from src.app import app
|
||||
from src.dependencies import get_config_manager, get_task_manager, get_resource_service, has_permission
|
||||
from src.dependencies import (
|
||||
get_config_manager,
|
||||
get_task_manager,
|
||||
get_resource_service,
|
||||
has_permission,
|
||||
)
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
# [DEF:test_dashboards_api:Test]
|
||||
# @RELATION: BINDS_TO -> SrcRoot
|
||||
# @RELATION: BINDS_TO -> TestResourceHubs
|
||||
# @PURPOSE: Verify GET /api/dashboards contract compliance
|
||||
# @TEST: Valid env_id returns 200 and dashboard list
|
||||
# @TEST: Invalid env_id returns 404
|
||||
# @TEST: Search filter works
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_deps():
|
||||
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
config_manager = MagicMock()
|
||||
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
task_manager = MagicMock()
|
||||
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
|
||||
resource_service = MagicMock()
|
||||
|
||||
|
||||
# Mock environment
|
||||
env = MagicMock()
|
||||
env.id = "env1"
|
||||
config_manager.get_environments.return_value = [env]
|
||||
|
||||
|
||||
# Mock tasks
|
||||
task_manager.get_all_tasks.return_value = []
|
||||
|
||||
|
||||
# Mock dashboards
|
||||
resource_service.get_dashboards_with_status = AsyncMock(return_value=[
|
||||
{"id": 1, "title": "Sales", "slug": "sales", "git_status": {"branch": "main", "sync_status": "OK"}, "last_task": None},
|
||||
{"id": 2, "title": "Marketing", "slug": "mkt", "git_status": None, "last_task": {"task_id": "t1", "status": "SUCCESS"}}
|
||||
])
|
||||
|
||||
resource_service.get_dashboards_with_status = AsyncMock(
|
||||
return_value=[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Sales",
|
||||
"slug": "sales",
|
||||
"git_status": {"branch": "main", "sync_status": "OK"},
|
||||
"last_task": None,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Marketing",
|
||||
"slug": "mkt",
|
||||
"git_status": None,
|
||||
"last_task": {"task_id": "t1", "status": "SUCCESS"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
# Bypass permission check
|
||||
mock_user = MagicMock()
|
||||
mock_user.username = "testadmin"
|
||||
@@ -44,24 +73,25 @@ def mock_deps():
|
||||
admin_role = MagicMock()
|
||||
admin_role.name = "Admin"
|
||||
mock_user.roles.append(admin_role)
|
||||
|
||||
|
||||
# Override both get_current_user and has_permission
|
||||
from src.dependencies import get_current_user
|
||||
|
||||
app.dependency_overrides[get_current_user] = lambda: mock_user
|
||||
|
||||
|
||||
# We need to override the specific instance returned by has_permission
|
||||
app.dependency_overrides[has_permission("plugin:migration", "READ")] = lambda: mock_user
|
||||
|
||||
yield {
|
||||
"config": config_manager,
|
||||
"task": task_manager,
|
||||
"resource": resource_service
|
||||
}
|
||||
|
||||
app.dependency_overrides[has_permission("plugin:migration", "READ")] = (
|
||||
lambda: mock_user
|
||||
)
|
||||
|
||||
yield {"config": config_manager, "task": task_manager, "resource": resource_service}
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_success:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_dashboards_api
|
||||
# @PURPOSE: Verify dashboards endpoint returns 200 with expected dashboard payload fields.
|
||||
def test_get_dashboards_success(mock_deps):
|
||||
response = client.get("/api/dashboards?env_id=env1")
|
||||
assert response.status_code == 200
|
||||
@@ -71,18 +101,24 @@ def test_get_dashboards_success(mock_deps):
|
||||
assert data["dashboards"][0]["title"] == "Sales"
|
||||
assert data["dashboards"][0]["git_status"]["sync_status"] == "OK"
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_not_found:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_dashboards_api
|
||||
# @PURPOSE: Verify dashboards endpoint returns 404 for unknown environment identifier.
|
||||
def test_get_dashboards_not_found(mock_deps):
|
||||
response = client.get("/api/dashboards?env_id=invalid")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_search:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_dashboards_api
|
||||
# @PURPOSE: Verify dashboards endpoint search filter returns matching subset.
|
||||
def test_get_dashboards_search(mock_deps):
|
||||
response = client.get("/api/dashboards?env_id=env1&search=Sales")
|
||||
assert response.status_code == 200
|
||||
@@ -90,25 +126,37 @@ def test_get_dashboards_search(mock_deps):
|
||||
assert len(data["dashboards"]) == 1
|
||||
assert data["dashboards"][0]["title"] == "Sales"
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_search:Function]
|
||||
# [/DEF:test_dashboards_api:Test]
|
||||
|
||||
|
||||
# [DEF:test_datasets_api:Test]
|
||||
# @RELATION: BINDS_TO -> SrcRoot
|
||||
# @RELATION: BINDS_TO -> TestResourceHubs
|
||||
# @PURPOSE: Verify GET /api/datasets contract compliance
|
||||
# @TEST: Valid env_id returns 200 and dataset list
|
||||
# @TEST: Invalid env_id returns 404
|
||||
# @TEST: Search filter works
|
||||
# @TEST: Negative - Service failure returns 503
|
||||
|
||||
# [/DEF:test_get_dashboards_search:Function]
|
||||
|
||||
# [DEF:test_get_datasets_success:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_datasets_api
|
||||
# @PURPOSE: Verify datasets endpoint returns 200 with mapped fields payload.
|
||||
def test_get_datasets_success(mock_deps):
|
||||
mock_deps["resource"].get_datasets_with_status = AsyncMock(return_value=[
|
||||
{"id": 1, "table_name": "orders", "schema": "public", "database": "db1", "mapped_fields": {"total": 10, "mapped": 5}, "last_task": None}
|
||||
])
|
||||
|
||||
mock_deps["resource"].get_datasets_with_status = AsyncMock(
|
||||
return_value=[
|
||||
{
|
||||
"id": 1,
|
||||
"table_name": "orders",
|
||||
"schema": "public",
|
||||
"database": "db1",
|
||||
"mapped_fields": {"total": 10, "mapped": 5},
|
||||
"last_task": None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
response = client.get("/api/datasets?env_id=env1")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
@@ -117,86 +165,126 @@ def test_get_datasets_success(mock_deps):
|
||||
assert data["datasets"][0]["table_name"] == "orders"
|
||||
assert data["datasets"][0]["mapped_fields"]["mapped"] == 5
|
||||
|
||||
|
||||
# [/DEF:test_get_datasets_success:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_datasets_not_found:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_datasets_api
|
||||
# @PURPOSE: Verify datasets endpoint returns 404 for unknown environment identifier.
|
||||
def test_get_datasets_not_found(mock_deps):
|
||||
response = client.get("/api/datasets?env_id=invalid")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# [/DEF:test_get_datasets_not_found:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_datasets_search:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_datasets_api
|
||||
# @PURPOSE: Verify datasets endpoint search filter returns matching dataset subset.
|
||||
def test_get_datasets_search(mock_deps):
|
||||
mock_deps["resource"].get_datasets_with_status = AsyncMock(return_value=[
|
||||
{"id": 1, "table_name": "orders", "schema": "public", "database": "db1", "mapped_fields": {"total": 10, "mapped": 5}, "last_task": None},
|
||||
{"id": 2, "table_name": "users", "schema": "public", "database": "db1", "mapped_fields": {"total": 5, "mapped": 5}, "last_task": None}
|
||||
])
|
||||
|
||||
mock_deps["resource"].get_datasets_with_status = AsyncMock(
|
||||
return_value=[
|
||||
{
|
||||
"id": 1,
|
||||
"table_name": "orders",
|
||||
"schema": "public",
|
||||
"database": "db1",
|
||||
"mapped_fields": {"total": 10, "mapped": 5},
|
||||
"last_task": None,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"table_name": "users",
|
||||
"schema": "public",
|
||||
"database": "db1",
|
||||
"mapped_fields": {"total": 5, "mapped": 5},
|
||||
"last_task": None,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
response = client.get("/api/datasets?env_id=env1&search=orders")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data["datasets"]) == 1
|
||||
assert data["datasets"][0]["table_name"] == "orders"
|
||||
|
||||
|
||||
# [/DEF:test_get_datasets_search:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_datasets_service_failure:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_datasets_api
|
||||
# @PURPOSE: Verify datasets endpoint surfaces backend fetch failure as HTTP 503.
|
||||
def test_get_datasets_service_failure(mock_deps):
|
||||
mock_deps["resource"].get_datasets_with_status = AsyncMock(side_effect=Exception("Superset down"))
|
||||
|
||||
mock_deps["resource"].get_datasets_with_status = AsyncMock(
|
||||
side_effect=Exception("Superset down")
|
||||
)
|
||||
|
||||
response = client.get("/api/datasets?env_id=env1")
|
||||
assert response.status_code == 503
|
||||
assert "Failed to fetch datasets" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_get_datasets_service_failure:Function]
|
||||
# [/DEF:test_datasets_api:Test]
|
||||
|
||||
|
||||
# [DEF:test_pagination_boundaries:Test]
|
||||
# @RELATION: BINDS_TO -> SrcRoot
|
||||
# @RELATION: BINDS_TO -> TestResourceHubs
|
||||
# @PURPOSE: Verify pagination validation for GET endpoints
|
||||
# @TEST: page<1 and page_size>100 return 400
|
||||
|
||||
# [/DEF:test_get_datasets_service_failure:Function]
|
||||
|
||||
# [DEF:test_get_dashboards_pagination_zero_page:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_pagination_boundaries
|
||||
# @PURPOSE: Verify dashboards endpoint rejects page=0 with HTTP 400 validation error.
|
||||
def test_get_dashboards_pagination_zero_page(mock_deps):
|
||||
"""@TEST_EDGE: pagination_zero_page -> {page:0, status:400}"""
|
||||
response = client.get("/api/dashboards?env_id=env1&page=0")
|
||||
assert response.status_code == 400
|
||||
assert "Page must be >= 1" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_pagination_zero_page:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_dashboards_pagination_oversize:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_pagination_boundaries
|
||||
# @PURPOSE: Verify dashboards endpoint rejects oversized page_size with HTTP 400.
|
||||
def test_get_dashboards_pagination_oversize(mock_deps):
|
||||
"""@TEST_EDGE: pagination_oversize -> {page_size:101, status:400}"""
|
||||
response = client.get("/api/dashboards?env_id=env1&page_size=101")
|
||||
assert response.status_code == 400
|
||||
assert "Page size must be between 1 and 100" in response.json()["detail"]
|
||||
|
||||
|
||||
# [/DEF:test_get_dashboards_pagination_oversize:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_datasets_pagination_zero_page:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_pagination_boundaries
|
||||
# @PURPOSE: Verify datasets endpoint rejects page=0 with HTTP 400.
|
||||
def test_get_datasets_pagination_zero_page(mock_deps):
|
||||
"""@TEST_EDGE: pagination_zero_page on datasets"""
|
||||
response = client.get("/api/datasets?env_id=env1&page=0")
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# [/DEF:test_get_datasets_pagination_zero_page:Function]
|
||||
|
||||
|
||||
# [DEF:test_get_datasets_pagination_oversize:Function]
|
||||
# @RELATION: BINDS_TO -> UnknownModule
|
||||
# @RELATION: BINDS_TO -> test_pagination_boundaries
|
||||
# @PURPOSE: Verify datasets endpoint rejects oversized page_size with HTTP 400.
|
||||
def test_get_datasets_pagination_oversize(mock_deps):
|
||||
"""@TEST_EDGE: pagination_oversize on datasets"""
|
||||
response = client.get("/api/datasets?env_id=env1&page_size=101")
|
||||
assert response.status_code == 400
|
||||
|
||||
# [/DEF:test_pagination_boundaries:Test]
|
||||
|
||||
# [/DEF:test_get_datasets_pagination_oversize:Function]
|
||||
# [/DEF:test_pagination_boundaries:Test]
|
||||
# [/DEF:TestResourceHubs:Module]
|
||||
|
||||
Reference in New Issue
Block a user