Files
ss-tools/backend/tests/test_api_key_routes.py
busya 4205618ee6 chore: eliminate all deprecation warnings from tests and linter
Warnings fixed:
- datetime.utcnow() → datetime.now(UTC) across 48+ files (src/ + tests/)
- datetime.utcnow (callback ref) → lambda: datetime.now(UTC) in model fields (18 files)
- Pydantic class Config → model_config = ConfigDict(...) (16 files)
- Pydantic .dict() → .model_dump() (8 files)
- ConfigDict(allow_population_by_field_name=True) → validate_by_name=True
- SQLAlchemy declarative_base() import path updated
- FastAPI on_event → lifespan context manager (app.py)
- Import sorting (ruff I001) auto-fixed across all files
- Fixed broken re-export chains that ruff F401 cleanup broke:
  _validate_bcp47: service.py now imports from dictionary_validation directly
  job_to_response: _job_routes.py and test imports from service_utils directly
  fetch_datasource_metadata: restored re-export in service.py
- Added missing TranslateJobService import in _job_routes.py (was deleted by F401)
- Added ConfigDict(protected_namespaces=()) for DashboardDatasetItem schema field
- pytest.ini: replaced deprecated importmode with asyncio_mode

All 440 tests pass with zero deprecation warnings.
2026-05-26 19:18:28 +03:00

257 lines
8.6 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
_mock_git_svc_cls = MagicMock()
_mock_git_svc_cls.return_value = MagicMock()
sys.modules['src.services.git._base'] = MagicMock(GitService=_mock_git_svc_cls)
sys.modules['src.services.git_service'] = MagicMock(GitService=_mock_git_svc_cls)
# ── 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