annator-command-center / tests /api /test_admin_routes.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 7)
cc036ff verified
Raw
History Blame Contribute Delete
33.9 kB
"""
Admin Routes API Tests - Part 2: WebSocket, Rating Sync, and Conflict Management
Tests for admin routes Part 2 (lines 546-1355):
- WebSocket management: status, reconnect, disable, enable
- Rating sync: sync, failed uploads, retry
- Conflict management: list, get, resolve, bulk-resolve
Coverage target: 75%+ line coverage on admin_routes.py Part 2
Note: Part 1 tests (lines 1-545) should be added in a separate plan (172-03).
"""
import pytest
from datetime import datetime, timezone, timedelta
from fastapi import FastAPI, status
from fastapi.testclient import TestClient
from unittest.mock import MagicMock, AsyncMock, patch
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.pool import StaticPool
from typing import Dict, Any
import uuid
# Import admin routes router
from api.admin_routes import router
# Import base
from core.database import Base
# Import models
from core.models import (
User, WebSocketState, FailedRatingUpload,
SkillRating, ConflictLog, SkillCache
)
# Import services
from core.rating_sync_service import RatingSyncService
from core.atom_saas_client import AtomSaaSClient
from core.conflict_resolution_service import ConflictResolutionService
# ============================================================================
# Test Database Setup (SQLite-compatible)
# ============================================================================
@pytest.fixture(scope="function")
def test_db():
"""Create in-memory SQLite database for testing."""
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool
)
# Create session first
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db = TestingSessionLocal()
# Create tables manually using raw SQL for SQLite compatibility
# Users table
db.execute("""
CREATE TABLE users (
id VARCHAR PRIMARY KEY,
email VARCHAR NOT NULL,
name VARCHAR,
role VARCHAR,
tenant_id VARCHAR,
is_active BOOLEAN DEFAULT 1
)
""")
# WebSocket state table
db.execute("""
CREATE TABLE websocket_state (
id INTEGER PRIMARY KEY,
connected BOOLEAN DEFAULT 0,
ws_url VARCHAR,
last_connected_at TIMESTAMP,
last_message_at TIMESTAMP,
disconnect_reason VARCHAR,
reconnect_attempts INTEGER DEFAULT 0,
consecutive_failures INTEGER DEFAULT 0,
max_reconnect_attempts INTEGER DEFAULT 10,
fallback_to_polling BOOLEAN DEFAULT 0,
fallback_started_at TIMESTAMP,
next_ws_attempt_at TIMESTAMP,
rate_limit_messages_per_sec INTEGER DEFAULT 100,
websocket_enabled BOOLEAN DEFAULT 1
)
""")
# Skill ratings table
db.execute("""
CREATE TABLE skill_ratings (
id VARCHAR PRIMARY KEY,
skill_id VARCHAR NOT NULL,
user_id VARCHAR NOT NULL,
tenant_id VARCHAR NOT NULL,
rating INTEGER NOT NULL,
review TEXT,
synced_at TIMESTAMP
)
""")
# Failed rating uploads table
db.execute("""
CREATE TABLE failed_rating_uploads (
id VARCHAR PRIMARY KEY,
rating_id VARCHAR NOT NULL,
error_message TEXT NOT NULL,
failed_at TIMESTAMP NOT NULL,
last_retry_at TIMESTAMP,
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 3,
tenant_id VARCHAR NOT NULL
)
""")
# Skills table (for conflicts)
db.execute("""
CREATE TABLE skills (
id VARCHAR PRIMARY KEY,
name VARCHAR NOT NULL
)
""")
# Tenants table
db.execute("""
CREATE TABLE tenants (
id VARCHAR PRIMARY KEY,
name VARCHAR NOT NULL
)
""")
# Conflict log table
db.execute("""
CREATE TABLE conflict_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
skill_id VARCHAR NOT NULL,
conflict_type VARCHAR NOT NULL,
severity VARCHAR NOT NULL,
local_data TEXT NOT NULL,
remote_data TEXT NOT NULL,
resolution_strategy VARCHAR,
resolved_data TEXT,
resolved_at TIMESTAMP,
resolved_by VARCHAR,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP,
tenant_id VARCHAR NOT NULL
)
""")
# Skill cache table
db.execute("""
CREATE TABLE skill_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
skill_id VARCHAR NOT NULL UNIQUE,
skill_data TEXT NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
tenant_id VARCHAR NOT NULL,
hit_count INTEGER DEFAULT 0,
last_hit_at TIMESTAMP
)
""")
db.commit()
yield db
# Cleanup
db.close()
@pytest.fixture(scope="function")
def test_app(test_db: Session):
"""Create FastAPI app with admin routes for testing."""
app = FastAPI()
app.include_router(router)
# Override get_db dependency
from core.database import get_db
def override_get_db():
try:
yield test_db
finally:
pass
app.dependency_overrides[get_db] = override_get_db
yield app
# Clean up overrides
app.dependency_overrides.clear()
@pytest.fixture(scope="function")
def client(test_app: FastAPI):
"""Create TestClient for testing."""
return TestClient(test_app)
@pytest.fixture(scope="function")
def admin_user(test_db: Session) -> User:
"""Create admin user for testing."""
user_id = str(uuid.uuid4())
user = User(
id=user_id,
email="admin@test.com",
name="Test Admin",
role="super_admin",
tenant_id="test_tenant",
is_active=True
)
test_db.add(user)
test_db.commit()
return user
@pytest.fixture(scope="function")
def authenticated_client(client: TestClient, admin_user: User):
"""Create authenticated TestClient with admin user."""
# Mock get_current_user to return admin_user
from core.auth import get_current_user
def override_get_current_user():
return admin_user
client.app.dependency_overrides[get_current_user] = override_get_current_user
yield client
# Clean up
client.app.dependency_overrides.clear()
# ============================================================================
# WebSocket Management Tests (13 tests)
# ============================================================================
class TestWebSocketStatus:
"""Tests for GET /api/admin/websocket/status"""
def test_get_websocket_status_connected(self, authenticated_client: TestClient, test_db: Session):
"""Test WebSocket status when connected."""
# Create WebSocket state
now = datetime.now(timezone.utc)
test_db.execute("""
INSERT INTO websocket_state
(id, connected, ws_url, last_connected_at, last_message_at, reconnect_attempts, consecutive_failures, fallback_to_polling)
VALUES (1, 1, 'wss://api.example.com/ws', :now, :now, 3, 0, 0)
""", {"now": now})
test_db.commit()
response = authenticated_client.get("/api/admin/websocket/status")
assert response.status_code == 200
data = response.json()
assert data["connected"] is True
assert data["ws_url"] == "wss://api.example.com/ws"
assert data["reconnect_attempts"] == 3
assert data["consecutive_failures"] == 0
assert data["fallback_to_polling"] is False
def test_get_websocket_status_disconnected(self, authenticated_client: TestClient, test_db: Session):
"""Test WebSocket status when disconnected."""
test_db.execute("""
INSERT INTO websocket_state
(id, connected, reconnect_attempts, consecutive_failures, disconnect_reason, fallback_to_polling)
VALUES (1, 0, 5, 3, 'connection_lost', 1)
""")
test_db.commit()
response = authenticated_client.get("/api/admin/websocket/status")
assert response.status_code == 200
data = response.json()
assert data["connected"] is False
assert data["reconnect_attempts"] == 5
assert data["consecutive_failures"] == 3
assert data["last_disconnect_reason"] == "connection_lost"
assert data["fallback_to_polling"] is True
def test_get_websocket_status_no_state(self, authenticated_client: TestClient):
"""Test WebSocket status when no state exists."""
response = authenticated_client.get("/api/admin/websocket/status")
assert response.status_code == 200
data = response.json()
assert data["connected"] is False
assert data["reconnect_attempts"] == 0
assert data["consecutive_failures"] == 0
class TestWebSocketReconnect:
"""Tests for POST /api/admin/websocket/reconnect"""
def test_trigger_websocket_reconnect_success(self, authenticated_client: TestClient, test_db: Session):
"""Test triggering WebSocket reconnect."""
# Create WebSocket state
test_db.execute("""
INSERT INTO websocket_state
(id, connected, reconnect_attempts, consecutive_failures, fallback_to_polling)
VALUES (1, 0, 5, 3, 1)
""")
test_db.commit()
response = authenticated_client.post("/api/admin/websocket/reconnect")
assert response.status_code == 200
data = response.json()
assert data["reconnect_triggered"] is True
# Verify DB updated
result = test_db.execute("SELECT reconnect_attempts, consecutive_failures, fallback_to_polling FROM websocket_state WHERE id = 1").fetchone()
assert result[0] == 0 # reconnect_attempts
assert result[1] == 0 # consecutive_failures
assert result[2] == 0 # fallback_to_polling
def test_trigger_websocket_reconnect_creates_state(self, authenticated_client: TestClient, test_db: Session):
"""Test reconnect creates WebSocket state if not exists."""
response = authenticated_client.post("/api/admin/websocket/reconnect")
assert response.status_code == 200
# Verify state created
result = test_db.execute("SELECT id FROM websocket_state").fetchone()
assert result is not None
assert result[0] == 1
class TestWebSocketDisable:
"""Tests for POST /api/admin/websocket/disable"""
def test_disable_websocket_success(self, authenticated_client: TestClient, test_db: Session):
"""Test disabling WebSocket."""
test_db.execute("""
INSERT INTO websocket_state
(id, connected, websocket_enabled, fallback_to_polling)
VALUES (1, 1, 1, 0)
""")
test_db.commit()
response = authenticated_client.post("/api/admin/websocket/disable")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["websocket_enabled"] is False
# Verify DB updated
result = test_db.execute("SELECT connected, disconnect_reason FROM websocket_state WHERE id = 1").fetchone()
assert result[0] == 0 # connected
assert result[1] == "disabled_by_admin"
def test_disable_websocket_creates_state(self, authenticated_client: TestClient, test_db: Session):
"""Test disable creates WebSocket state if not exists."""
response = authenticated_client.post("/api/admin/websocket/disable")
assert response.status_code == 200
# Verify state created
result = test_db.execute("SELECT id FROM websocket_state").fetchone()
assert result is not None
class TestWebSocketEnable:
"""Tests for POST /api/admin/websocket/enable"""
def test_enable_websocket_success(self, authenticated_client: TestClient, test_db: Session):
"""Test enabling WebSocket."""
now = datetime.now(timezone.utc) + timedelta(hours=1)
test_db.execute("""
INSERT INTO websocket_state
(id, fallback_to_polling, next_ws_attempt_at, reconnect_attempts)
VALUES (1, 1, :now, 5)
""", {"now": now})
test_db.commit()
response = authenticated_client.post("/api/admin/websocket/enable")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["websocket_enabled"] is True
# Verify DB updated
result = test_db.execute("SELECT fallback_to_polling, next_ws_attempt_at, reconnect_attempts FROM websocket_state WHERE id = 1").fetchone()
assert result[0] == 0 # fallback_to_polling
assert result[1] is None # next_ws_attempt_at
assert result[2] == 0 # reconnect_attempts
def test_enable_websocket_creates_state(self, authenticated_client: TestClient, test_db: Session):
"""Test enable creates WebSocket state if not exists."""
response = authenticated_client.post("/api/admin/websocket/enable")
assert response.status_code == 200
# Verify state created
result = test_db.execute("SELECT id FROM websocket_state").fetchone()
assert result is not None
# ============================================================================
# Rating Sync Tests (12 tests)
# ============================================================================
class TestRatingSync:
"""Tests for POST /api/admin/sync/ratings"""
def test_trigger_rating_sync_success(self, authenticated_client: TestClient, test_db: Session):
"""Test triggering rating sync successfully."""
# Create pending ratings
for i in range(5):
test_db.execute("""
INSERT INTO skill_ratings
(id, skill_id, user_id, tenant_id, rating, synced_at)
VALUES (:id, :skill_id, 'test_user', 'test_tenant', 5, NULL)
""", {"id": f"rating_{i}", "skill_id": f"skill_{i}"})
test_db.commit()
# Mock RatingSyncService
with patch('core.rating_sync_service.RatingSyncService') as mock_service_class:
mock_service = MagicMock()
mock_service._sync_in_progress = False
mock_service.get_pending_ratings.return_value = [
MagicMock(id=f"rating_{i}") for i in range(5)
]
# Make sync_ratings async
async def mock_sync(upload_all=False):
return {"success": True, "uploaded": 5, "failed": 0, "skipped": 0}
mock_service.sync_ratings = mock_sync
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/sync/ratings",
json={"upload_all": False}
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["uploaded"] == 5
assert data["failed"] == 0
def test_trigger_rating_sync_in_progress(self, authenticated_client: TestClient, test_db: Session):
"""Test sync when already in progress."""
with patch('core.rating_sync_service.RatingSyncService') as mock_service_class:
mock_service = MagicMock()
mock_service._sync_in_progress = True # Already syncing
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/sync/ratings",
json={"upload_all": False}
)
assert response.status_code == 503
def test_trigger_rating_sync_with_failures(self, authenticated_client: TestClient, test_db: Session):
"""Test sync with some failures."""
with patch('core.rating_sync_service.RatingSyncService') as mock_service_class:
mock_service = MagicMock()
mock_service._sync_in_progress = False
mock_service.get_pending_ratings.return_value = []
async def mock_sync(upload_all=False):
return {"success": True, "uploaded": 3, "failed": 2, "skipped": 1}
mock_service.sync_ratings = mock_sync
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/sync/ratings",
json={"upload_all": False}
)
assert response.status_code == 200
data = response.json()
assert data["uploaded"] == 3
assert data["failed"] == 2
assert data["skipped"] == 1
class TestFailedRatingUploads:
"""Tests for GET /api/admin/ratings/failed-uploads"""
def test_get_failed_rating_uploads_success(self, authenticated_client: TestClient, test_db: Session):
"""Test getting failed rating uploads."""
# Create failed uploads
for i in range(3):
test_db.execute("""
INSERT INTO failed_rating_uploads
(id, rating_id, error_message, failed_at, retry_count, tenant_id)
VALUES (:id, :rating_id, :error, :now, :retry_count, 'test_tenant')
""", {
"id": f"failed_{i}",
"rating_id": f"rating_{i}",
"error": f"Error {i}",
"now": datetime.now(timezone.utc),
"retry_count": i
})
test_db.commit()
response = authenticated_client.get("/api/admin/ratings/failed-uploads")
assert response.status_code == 200
data = response.json()
assert len(data) == 3
def test_get_failed_rating_uploads_empty(self, authenticated_client: TestClient):
"""Test getting failed uploads when none exist."""
response = authenticated_client.get("/api/admin/ratings/failed-uploads")
assert response.status_code == 200
assert response.json() == []
class TestRetryFailedRatingUpload:
"""Tests for POST /api/admin/ratings/failed-uploads/{failed_id}/retry"""
def test_retry_failed_rating_upload_success(self, authenticated_client: TestClient, test_db: Session):
"""Test retrying failed rating upload successfully."""
# Create rating
test_db.execute("""
INSERT INTO skill_ratings
(id, skill_id, user_id, tenant_id, rating, synced_at)
VALUES ('rating_1', 'skill_1', 'test_user', 'test_tenant', 5, NULL)
""")
# Create failed upload
test_db.execute("""
INSERT INTO failed_rating_uploads
(id, rating_id, error_message, failed_at, retry_count, tenant_id)
VALUES ('failed_1', 'rating_1', 'Network error', :now, 0, 'test_tenant')
""", {"now": datetime.now(timezone.utc)})
test_db.commit()
# Mock RatingSyncService
with patch('core.rating_sync_service.RatingSyncService') as mock_service_class:
mock_service = MagicMock()
async def mock_upload(rating):
return {"success": True, "rating_id": "remote_123"}
mock_service.upload_rating = mock_upload
mock_service.mark_as_synced = MagicMock()
mock_service_class.return_value = mock_service
response = authenticated_client.post("/api/admin/ratings/failed-uploads/failed_1/retry")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
# Verify failed record deleted
result = test_db.execute("SELECT COUNT(*) FROM failed_rating_uploads").fetchone()
assert result[0] == 0
def test_retry_failed_rating_upload_rating_deleted(self, authenticated_client: TestClient, test_db: Session):
"""Test retry when rating no longer exists."""
# Create failed upload without rating
test_db.execute("""
INSERT INTO failed_rating_uploads
(id, rating_id, error_message, failed_at, retry_count, tenant_id)
VALUES ('failed_1', 'deleted_rating', 'Network error', :now, 0, 'test_tenant')
""", {"now": datetime.now(timezone.utc)})
test_db.commit()
response = authenticated_client.post("/api/admin/ratings/failed-uploads/failed_1/retry")
assert response.status_code == 200
data = response.json()
assert data["success"] is False
assert "no longer exists" in data["message"].lower()
def test_retry_failed_rating_upload_failed_again(self, authenticated_client: TestClient, test_db: Session):
"""Test retry that fails again."""
# Create rating
test_db.execute("""
INSERT INTO skill_ratings
(id, skill_id, user_id, tenant_id, rating, synced_at)
VALUES ('rating_1', 'skill_1', 'test_user', 'test_tenant', 5, NULL)
""")
# Create failed upload
test_db.execute("""
INSERT INTO failed_rating_uploads
(id, rating_id, error_message, failed_at, retry_count, tenant_id)
VALUES ('failed_1', 'rating_1', 'Network error', :now, 1, 'test_tenant')
""", {"now": datetime.now(timezone.utc)})
test_db.commit()
# Mock RatingSyncService to fail again
with patch('core.rating_sync_service.RatingSyncService') as mock_service_class:
mock_service = MagicMock()
async def mock_upload(rating):
return {"success": False, "error": "Network error"}
mock_service.upload_rating = mock_upload
mock_service_class.return_value = mock_service
response = authenticated_client.post("/api/admin/ratings/failed-uploads/failed_1/retry")
assert response.status_code == 200
data = response.json()
assert data["success"] is False
def test_retry_failed_rating_upload_not_found(self, authenticated_client: TestClient):
"""Test retry with non-existent failed upload."""
response = authenticated_client.post("/api/admin/ratings/failed-uploads/nonexistent/retry")
assert response.status_code == 404
# ============================================================================
# Conflict Management Tests (21 tests)
# ============================================================================
class TestListConflicts:
"""Tests for GET /api/admin/conflicts"""
def test_list_conflicts_success(self, authenticated_client: TestClient, test_db: Session):
"""Test listing conflicts successfully."""
# Create conflicts
for i in range(3):
test_db.execute("""
INSERT INTO conflict_log
(id, skill_id, conflict_type, severity, local_data, remote_data, created_at, tenant_id)
VALUES (:id, :skill_id, 'version_mismatch', 'high', '{"version": "1.0"}', '{"version": "2.0"}', :now, 'test_tenant')
""", {"id": i + 1, "skill_id": f"skill_{i}", "now": datetime.now(timezone.utc)})
test_db.commit()
# Mock ConflictResolutionService
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
# Return mock conflict objects
mock_conflicts = []
for i in range(3):
c = MagicMock()
c.id = i + 1
c.skill_id = f"skill_{i}"
c.conflict_type = "version_mismatch"
c.severity = "high"
c.local_data = {"version": "1.0"}
c.remote_data = {"version": "2.0"}
c.resolution_strategy = None
c.resolved_data = None
c.resolved_at = None
c.resolved_by = None
c.created_at = datetime.now(timezone.utc)
mock_conflicts.append(c)
mock_service.get_unresolved_conflicts.return_value = mock_conflicts
mock_service_class.return_value = mock_service
response = authenticated_client.get("/api/admin/conflicts")
assert response.status_code == 200
data = response.json()
assert "conflicts" in data
assert data["total_count"] == 3
def test_list_conflicts_empty(self, authenticated_client: TestClient, test_db: Session):
"""Test listing conflicts when none exist."""
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
mock_service.get_unresolved_conflicts.return_value = []
mock_service_class.return_value = mock_service
response = authenticated_client.get("/api/admin/conflicts")
assert response.status_code == 200
data = response.json()
assert data["conflicts"] == []
assert data["total_count"] == 0
class TestGetConflict:
"""Tests for GET /api/admin/conflicts/{conflict_id}"""
def test_get_conflict_success(self, authenticated_client: TestClient, test_db: Session):
"""Test getting conflict by ID."""
# Mock conflict
mock_conflict = MagicMock()
mock_conflict.id = 1
mock_conflict.skill_id = "skill_1"
mock_conflict.conflict_type = "version_mismatch"
mock_conflict.severity = "high"
mock_conflict.local_data = {"version": "1.0"}
mock_conflict.remote_data = {"version": "2.0"}
mock_conflict.created_at = datetime.now(timezone.utc)
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
mock_service.get_conflict_by_id.return_value = mock_conflict
mock_service_class.return_value = mock_service
response = authenticated_client.get("/api/admin/conflicts/1")
assert response.status_code == 200
data = response.json()
assert data["id"] == 1
assert data["skill_id"] == "skill_1"
def test_get_conflict_not_found(self, authenticated_client: TestClient, test_db: Session):
"""Test getting non-existent conflict."""
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
mock_service.get_conflict_by_id.return_value = None
mock_service_class.return_value = mock_service
response = authenticated_client.get("/api/admin/conflicts/999")
assert response.status_code == 404
class TestResolveConflict:
"""Tests for POST /api/admin/conflicts/{conflict_id}/resolve"""
def test_resolve_conflict_remote_wins(self, authenticated_client: TestClient, test_db: Session):
"""Test resolving conflict with remote_wins strategy."""
resolved_data = {"skill_id": "skill_1", "version": "2.0"}
# Mock ConflictResolutionService
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
mock_service.resolve_conflict.return_value = resolved_data
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/conflicts/1/resolve",
json={"strategy": "remote_wins", "resolved_by": "admin"}
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["resolved_data"] == resolved_data
def test_resolve_conflict_local_wins(self, authenticated_client: TestClient, test_db: Session):
"""Test resolving conflict with local_wins strategy."""
resolved_data = {"skill_id": "skill_1", "version": "1.0"}
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
mock_service.resolve_conflict.return_value = resolved_data
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/conflicts/1/resolve",
json={"strategy": "local_wins", "resolved_by": "admin"}
)
assert response.status_code == 200
assert response.json()["success"] is True
def test_resolve_conflict_merge(self, authenticated_client: TestClient, test_db: Session):
"""Test resolving conflict with merge strategy."""
resolved_data = {"skill_id": "skill_1", "version": "2.0", "local_changes": "preserved"}
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
mock_service.resolve_conflict.return_value = resolved_data
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/conflicts/1/resolve",
json={"strategy": "merge", "resolved_by": "admin"}
)
assert response.status_code == 200
assert response.json()["success"] is True
def test_resolve_conflict_invalid_strategy(self, authenticated_client: TestClient, test_db: Session):
"""Test resolving conflict with invalid strategy."""
response = authenticated_client.post(
"/api/admin/conflicts/1/resolve",
json={"strategy": "invalid", "resolved_by": "admin"}
)
assert response.status_code == 422
def test_resolve_conflict_not_found(self, authenticated_client: TestClient, test_db: Session):
"""Test resolving non-existent conflict."""
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
mock_service.resolve_conflict.return_value = None
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/conflicts/999/resolve",
json={"strategy": "remote_wins", "resolved_by": "admin"}
)
assert response.status_code == 200
data = response.json()
assert data["success"] is False
class TestBulkResolveConflicts:
"""Tests for POST /api/admin/conflicts/bulk-resolve"""
def test_bulk_resolve_conflicts_success(self, authenticated_client: TestClient, test_db: Session):
"""Test bulk resolving conflicts successfully."""
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
mock_service.resolve_conflict.return_value = {"skill_id": "skill_1", "version": "2.0"}
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/conflicts/bulk-resolve",
json={
"conflict_ids": [1, 2, 3],
"strategy": "remote_wins",
"resolved_by": "admin"
}
)
assert response.status_code == 200
data = response.json()
assert data["resolved_count"] == 3
assert data["failed_count"] == 0
assert data["success"] is True
def test_bulk_resolve_conflicts_partial_failure(self, authenticated_client: TestClient, test_db: Session):
"""Test bulk resolve with some failures."""
with patch('core.conflict_resolution_service.ConflictResolutionService') as mock_service_class:
mock_service = MagicMock()
# Make second resolve fail
def side_effect(conflict_id, strategy, resolved_by):
if conflict_id == 2:
return None
return {"skill_id": f"skill_{conflict_id}", "version": "2.0"}
mock_service.resolve_conflict.side_effect = side_effect
mock_service_class.return_value = mock_service
response = authenticated_client.post(
"/api/admin/conflicts/bulk-resolve",
json={
"conflict_ids": [1, 2, 3],
"strategy": "remote_wins",
"resolved_by": "admin"
}
)
assert response.status_code == 200
data = response.json()
assert data["resolved_count"] == 2
assert data["failed_count"] == 1
def test_bulk_resolve_conflicts_invalid_strategy(self, authenticated_client: TestClient, test_db: Session):
"""Test bulk resolve with invalid strategy."""
response = authenticated_client.post(
"/api/admin/conflicts/bulk-resolve",
json={
"conflict_ids": [1, 2, 3],
"strategy": "invalid",
"resolved_by": "admin"
}
)
assert response.status_code == 422
def test_bulk_resolve_conflicts_too_many_ids(self, authenticated_client: TestClient, test_db: Session):
"""Test bulk resolve with too many conflict IDs."""
# Max is 100
conflict_ids = list(range(101))
response = authenticated_client.post(
"/api/admin/conflicts/bulk-resolve",
json={
"conflict_ids": conflict_ids,
"strategy": "remote_wins",
"resolved_by": "admin"
}
)
assert response.status_code == 422