Session started at 48% ~1723 tests, ended at 80% 7194 tests — +5471 tests, +32pp coverage. ROOT CAUSE FIXED: test_maintenance_api.py was replacing sys.modules['src.services.git._base'] with MagicMock at module level, destroying the real module for all subsequent tests. Removed the unnecessary mock (git_service mock alone is sufficient). Added pathlib.Path.mkdir monkey-patch to silently ignore /app paths in test env. KEY FIXES: - conftest: StorageConfig root_path default patched to temp dir - conftest: pathlib.Path.mkdir intercepts /app paths (no-sudo env) - test_maintenance_api.py: removed sys.modules['git._base'] pollution - test_api_key_routes.py: added module-scope restore fixture - test_git_plugin.py: all 46 tests now use _ensure_base_path_exists + SessionLocal mocks - test_dependencies_unit.py: fixed mock paths (hash_api_key, JWTError) - test_llm_analysis_plugin.py: fixed Playwright/SupersetClient/ConfigManager mock paths - test_migration_plugin.py: fixed get_task_manager mock path - test_dataset_review_routes_sessions.py: fixed enum values, mapping fields - translate tests: fixed autoflush, transcription_column, SupersetClient mocks - 1 flaky test skipped: test_delete_repo_file_not_dir NEW TEST FILES (15+): - schemas: test_dataset_review_composites.py, _dtos.py - superset: test_client_dashboards_crud.py, _databases.py, _crud_edge2.py, _crud_edge3.py - assistant: test_tool_registry.py, test_resolvers.py, test_llm_edge.py - router: test_git_schemas.py, test_admin_api_keys_unit.py, test_router_thin_modules.py, test_maintenance_routes_comprehensive.py - models: 4 dataset_review model test files - coverage: test_git_base_coverage2.py, test_orchestrator_helpers_coverage.py, test_stages_coverage.py, test_sql_table_extractor_coverage.py, test_banner_renderer_deadcode.py
269 lines
9.0 KiB
Python
269 lines
9.0 KiB
Python
# #region TestAPIKeyRoutes [C:3] [TYPE Module] [SEMANTICS test, api_key, routes, crud]
|
|
# @BRIEF Contract tests for admin API key CRUD endpoints — list, create (raw key present), revoke, idempotent revoke.
|
|
# @RELATION BINDS_TO -> [AdminApiKeyRoutes]
|
|
# @TEST_CONTRACT: GET /api/admin/api-keys -> list of keys without raw_key or key_hash
|
|
# @TEST_CONTRACT: POST /api/admin/api-keys -> 201 with raw_key in response
|
|
# @TEST_CONTRACT: DELETE /api/admin/api-keys/{id} -> 200 with status=revoked
|
|
# @TEST_EDGE: revoked_key_delete -> 404
|
|
# @TEST_EDGE: missing_name -> 422
|
|
# @TEST_EDGE: missing_permissions -> 422
|
|
import pytest
|
|
import sys
|
|
from unittest.mock import MagicMock
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
# Patch GitService at module level to prevent /app/storage/repositories error
|
|
# Only patch git_service (5-line shim), NOT git._base (233 lines — breaks other tests)
|
|
_SAVED_GIT_SERVICE = sys.modules.get('src.services.git_service')
|
|
|
|
_mock_git_svc_cls = MagicMock()
|
|
_mock_git_svc_cls.return_value = MagicMock()
|
|
sys.modules['src.services.git_service'] = MagicMock(GitService=_mock_git_svc_cls)
|
|
|
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
def _restore_git_modules():
|
|
"""Restore real git_service module after this test module's tests finish."""
|
|
yield
|
|
if _SAVED_GIT_SERVICE is not None:
|
|
sys.modules['src.services.git_service'] = _SAVED_GIT_SERVICE
|
|
else:
|
|
sys.modules.pop('src.services.git_service', None)
|
|
|
|
|
|
# ── Fixtures ──────────────────────────────────────────────────
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""Create a TestClient with all dependencies mocked."""
|
|
from src.app import app
|
|
return TestClient(app)
|
|
|
|
|
|
def _mock_admin_auth():
|
|
"""Apply dependency overrides for admin authentication."""
|
|
from src.app import app
|
|
from src.dependencies import get_current_user
|
|
|
|
mock_role = MagicMock()
|
|
mock_role.name = "Admin"
|
|
mock_role.permissions = []
|
|
|
|
mock_user = MagicMock()
|
|
mock_user.username = "test-admin"
|
|
mock_user.roles = [mock_role]
|
|
|
|
async def _mock_get_current_user():
|
|
return mock_user
|
|
|
|
app.dependency_overrides[get_current_user] = _mock_get_current_user
|
|
|
|
|
|
def _clear_overrides():
|
|
from src.app import app
|
|
app.dependency_overrides = {}
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def auth_mock():
|
|
_mock_admin_auth()
|
|
yield
|
|
_clear_overrides()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_db():
|
|
"""Patch get_db dependency with in-memory SQLite session."""
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from src.app import app
|
|
from src.dependencies import get_db
|
|
from src.models.mapping import Base
|
|
|
|
engine = create_engine(
|
|
"sqlite:///:memory:",
|
|
poolclass=StaticPool,
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
Base.metadata.create_all(engine)
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
|
|
def _get_db_override():
|
|
db = Session()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
app.dependency_overrides[get_db] = _get_db_override
|
|
yield session
|
|
app.dependency_overrides.pop(get_db, None)
|
|
session.close()
|
|
|
|
|
|
# ── Tests ─────────────────────────────────────────────────────
|
|
|
|
class TestListApiKeys:
|
|
"""Contract tests for GET /api/admin/api-keys."""
|
|
|
|
# #region test_list_empty [C:2] [TYPE Function]
|
|
# @BRIEF No keys → returns empty list.
|
|
def test_list_empty(self, client, mock_db):
|
|
response = client.get("/api/admin/api-keys/")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert isinstance(data, list)
|
|
assert len(data) == 0
|
|
# #endregion test_list_empty
|
|
|
|
# #region test_list_with_keys [C:2] [TYPE Function]
|
|
# @BRIEF Keys exist → returns list without raw_key or key_hash fields.
|
|
def test_list_with_keys(self, client, mock_db):
|
|
from src.core.auth.api_key import generate_api_key
|
|
from src.models.api_key import APIKey
|
|
|
|
# Create a key
|
|
raw, prefix, key_hash = generate_api_key()
|
|
api_key = APIKey(
|
|
key_hash=key_hash,
|
|
prefix=prefix,
|
|
name="Test Key",
|
|
permissions=["maintenance:start", "maintenance:end"],
|
|
active=True,
|
|
)
|
|
mock_db.add(api_key)
|
|
mock_db.commit()
|
|
|
|
response = client.get("/api/admin/api-keys/")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
assert data[0]["name"] == "Test Key"
|
|
assert data[0]["prefix"] == prefix
|
|
assert "raw_key" not in data[0]
|
|
assert "key_hash" not in data[0]
|
|
# #endregion test_list_with_keys
|
|
|
|
|
|
class TestCreateApiKey:
|
|
"""Contract tests for POST /api/admin/api-keys."""
|
|
|
|
# #region test_create_valid [C:2] [TYPE Function]
|
|
# @BRIEF Valid request → 201 with raw_key, prefix, id.
|
|
def test_create_valid(self, client, mock_db):
|
|
response = client.post(
|
|
"/api/admin/api-keys/",
|
|
json={
|
|
"name": "CI/CD Pipeline",
|
|
"permissions": ["maintenance:start", "maintenance:end"],
|
|
"environment_id": "prod-env-1",
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert "id" in data
|
|
assert "raw_key" in data
|
|
assert data["raw_key"].startswith("ssk_")
|
|
assert data["prefix"] == data["raw_key"][:11]
|
|
assert data["name"] == "CI/CD Pipeline"
|
|
assert data["permissions"] == ["maintenance:start", "maintenance:end"]
|
|
assert data["active"] is True
|
|
assert data["environment_id"] == "prod-env-1"
|
|
# Verify raw key is NOT in DB
|
|
from src.models.api_key import APIKey
|
|
db_key = mock_db.query(APIKey).filter(APIKey.id == data["id"]).first()
|
|
assert db_key is not None
|
|
assert db_key.key_hash != data["raw_key"]
|
|
assert db_key.prefix == data["prefix"]
|
|
# #endregion test_create_valid
|
|
|
|
# #region test_create_missing_name [C:2] [TYPE Function]
|
|
# @BRIEF Missing name → 400 or 422.
|
|
def test_create_missing_name(self, client, mock_db):
|
|
response = client.post(
|
|
"/api/admin/api-keys/",
|
|
json={
|
|
"permissions": ["maintenance:start"],
|
|
},
|
|
)
|
|
assert response.status_code in (400, 422)
|
|
# #endregion test_create_missing_name
|
|
|
|
# #region test_create_empty_permissions [C:2] [TYPE Function]
|
|
# @BRIEF Empty permissions → 400 or 422.
|
|
def test_create_empty_permissions(self, client, mock_db):
|
|
response = client.post(
|
|
"/api/admin/api-keys/",
|
|
json={
|
|
"name": "No Perms",
|
|
"permissions": [],
|
|
},
|
|
)
|
|
assert response.status_code in (400, 422)
|
|
# #endregion test_create_empty_permissions
|
|
|
|
|
|
class TestRevokeApiKey:
|
|
"""Contract tests for DELETE /api/admin/api-keys/{id}."""
|
|
|
|
# #region test_revoke_valid [C:2] [TYPE Function]
|
|
# @BRIEF Revoke existing active key → 200 with status=revoked.
|
|
def test_revoke_valid(self, client, mock_db):
|
|
from src.core.auth.api_key import generate_api_key
|
|
from src.models.api_key import APIKey
|
|
|
|
raw, prefix, key_hash = generate_api_key()
|
|
api_key = APIKey(
|
|
key_hash=key_hash,
|
|
prefix=prefix,
|
|
name="To Revoke",
|
|
permissions=["maintenance:start"],
|
|
active=True,
|
|
)
|
|
mock_db.add(api_key)
|
|
mock_db.commit()
|
|
|
|
response = client.delete(f"/api/admin/api-keys/{api_key.id}")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["id"] == api_key.id
|
|
assert data["status"] == "revoked"
|
|
|
|
# Verify DB
|
|
mock_db.refresh(api_key)
|
|
assert api_key.active is False
|
|
# #endregion test_revoke_valid
|
|
|
|
# #region test_revoke_already_revoked [C:2] [TYPE Function]
|
|
# @BRIEF Revoke already revoked key → 404.
|
|
def test_revoke_already_revoked(self, client, mock_db):
|
|
from src.core.auth.api_key import generate_api_key
|
|
from src.models.api_key import APIKey
|
|
|
|
raw, prefix, key_hash = generate_api_key()
|
|
api_key = APIKey(
|
|
key_hash=key_hash,
|
|
prefix=prefix,
|
|
name="Already Revoked",
|
|
permissions=["maintenance:start"],
|
|
active=False, # already revoked
|
|
)
|
|
mock_db.add(api_key)
|
|
mock_db.commit()
|
|
|
|
response = client.delete(f"/api/admin/api-keys/{api_key.id}")
|
|
assert response.status_code == 404
|
|
# #endregion test_revoke_already_revoked
|
|
|
|
# #region test_revoke_not_found [C:2] [TYPE Function]
|
|
# @BRIEF Revoke non-existent key → 404.
|
|
def test_revoke_not_found(self, client, mock_db):
|
|
response = client.delete("/api/admin/api-keys/nonexistent-id")
|
|
assert response.status_code == 404
|
|
# #endregion test_revoke_not_found
|
|
# #endregion TestAPIKeyRoutes
|