| """ |
| Admin Routes Coverage Tests - Comprehensive Test Suite |
| |
| Tests for admin routes (api/admin_routes.py) covering all 22 endpoints across 5 categories: |
| - Admin user CRUD (6): list, get, create, update, delete, update last login |
| - Admin role CRUD (5): list, get, create, update, delete |
| - WebSocket management (4): status, reconnect, disable, enable |
| - Rating sync (3): trigger sync, failed uploads, retry |
| - Conflict resolution (4): list, get detail, resolve, bulk resolve |
| |
| Coverage target: 75%+ line coverage on api/admin_routes.py |
| Test patterns follow Phase 177/178 conventions |
| """ |
|
|
| import pytest |
| import uuid |
| from datetime import datetime, timezone, timedelta |
| from typing import List |
| from unittest.mock import MagicMock, AsyncMock, patch |
| from fastapi import FastAPI |
| from fastapi.testclient import TestClient |
| from sqlalchemy import create_engine |
| from sqlalchemy.orm import sessionmaker, Session |
| from sqlalchemy.pool import StaticPool |
|
|
| |
| from api.admin_routes import router |
|
|
| |
| from core.models import ( |
| Base, User, AdminUser, AdminRole, WebSocketState, |
| FailedRatingUpload, SkillRating, ConflictLog, SkillCache |
| ) |
|
|
| |
| from core.auth import get_password_hash |
|
|
|
|
| |
| |
| |
|
|
| @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 |
| ) |
|
|
| |
| |
| from core.models import ( |
| AdminUser, AdminRole, User, WebSocketState, |
| FailedRatingUpload, SkillRating, ConflictLog, SkillCache, |
| CustomRole, Tenant, WorkflowTemplate |
| ) |
|
|
| |
| Tenant.__table__.create(bind=engine, checkfirst=True) |
| CustomRole.__table__.create(bind=engine, checkfirst=True) |
| User.__table__.create(bind=engine, checkfirst=True) |
| WorkflowTemplate.__table__.create(bind=engine, checkfirst=True) |
| AdminUser.__table__.create(bind=engine, checkfirst=True) |
| AdminRole.__table__.create(bind=engine, checkfirst=True) |
| WebSocketState.__table__.create(bind=engine, checkfirst=True) |
| FailedRatingUpload.__table__.create(bind=engine, checkfirst=True) |
| SkillRating.__table__.create(bind=engine, checkfirst=True) |
| ConflictLog.__table__.create(bind=engine, checkfirst=True) |
| SkillCache.__table__.create(bind=engine, checkfirst=True) |
|
|
| |
| TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) |
| db = TestingSessionLocal() |
|
|
| yield db |
|
|
| |
| db.close() |
| |
| SkillCache.__table__.drop(bind=engine) |
| ConflictLog.__table__.drop(bind=engine) |
| SkillRating.__table__.drop(bind=engine) |
| FailedRatingUpload.__table__.drop(bind=engine) |
| WebSocketState.__table__.drop(bind=engine) |
| AdminUser.__table__.drop(bind=engine) |
| AdminRole.__table__.drop(bind=engine) |
| WorkflowTemplate.__table__.drop(bind=engine) |
| User.__table__.drop(bind=engine) |
| CustomRole.__table__.drop(bind=engine) |
| Tenant.__table__.drop(bind=engine) |
|
|
|
|
| @pytest.fixture(scope="function") |
| def test_app(test_db: Session): |
| """Create FastAPI app with admin routes for testing.""" |
| app = FastAPI() |
| app.include_router(router) |
|
|
| |
| 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 |
|
|
| |
| 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 super_admin_user(test_db: Session) -> User: |
| """Create super_admin user for authorization tests.""" |
| user = User( |
| id=str(uuid.uuid4()), |
| email="superadmin@test.com", |
| first_name="Super", |
| last_name="Admin", |
| name="Super Admin", |
| role="super_admin", |
| status="active", |
| email_verified=True |
| ) |
| test_db.add(user) |
| test_db.commit() |
| test_db.refresh(user) |
| return user |
|
|
|
|
| @pytest.fixture(scope="function") |
| def regular_user(test_db: Session) -> User: |
| """Create regular user for auth testing.""" |
| user = User( |
| id=str(uuid.uuid4()), |
| email="member@test.com", |
| first_name="Regular", |
| last_name="Member", |
| name="Regular Member", |
| role="member", |
| status="active", |
| email_verified=True |
| ) |
| test_db.add(user) |
| test_db.commit() |
| test_db.refresh(user) |
| return user |
|
|
|
|
| @pytest.fixture(scope="function") |
| def authenticated_admin_client(client: TestClient, super_admin_user: User): |
| """Create authenticated TestClient with super admin.""" |
| |
| from core.auth import get_current_user |
|
|
| def override_get_current_user(): |
| return super_admin_user |
|
|
| |
| from api.admin_routes import require_super_admin |
|
|
| def override_require_super_admin(): |
| return super_admin_user |
|
|
| client.app.dependency_overrides[get_current_user] = override_get_current_user |
| client.app.dependency_overrides[require_super_admin] = override_require_super_admin |
|
|
| yield client |
|
|
| |
| client.app.dependency_overrides.clear() |
|
|
|
|
| @pytest.fixture(scope="function") |
| def unauthenticated_client(client: TestClient): |
| """Create client without auth override.""" |
| return client |
|
|
|
|
| @pytest.fixture(scope="function") |
| def test_admin_role(test_db: Session) -> AdminRole: |
| """Create test admin role.""" |
| role = AdminRole( |
| id=str(uuid.uuid4()), |
| name="Test Admin Role", |
| permissions={"users": True, "workflows": False, "reports": True}, |
| description="Test role for coverage tests" |
| ) |
| test_db.add(role) |
| test_db.commit() |
| test_db.refresh(role) |
| return role |
|
|
|
|
| @pytest.fixture(scope="function") |
| def test_admin_user(test_db: Session, test_admin_role: AdminRole) -> AdminUser: |
| """Create test admin user.""" |
| admin = AdminUser( |
| id=str(uuid.uuid4()), |
| email="testadmin@test.com", |
| name="Test Admin User", |
| password_hash=get_password_hash("TestPass123!"), |
| role_id=test_admin_role.id, |
| status="active", |
| last_login=None, |
| created_at=datetime.now(timezone.utc) |
| ) |
| test_db.add(admin) |
| test_db.commit() |
| test_db.refresh(admin) |
| return admin |
|
|
|
|
| @pytest.fixture(scope="function") |
| def mock_rating_sync_service(): |
| """Mock RatingSyncService for testing.""" |
| mock_service = AsyncMock() |
| mock_service._sync_in_progress = False |
| mock_service.get_pending_ratings = MagicMock(return_value=[]) |
| mock_service.sync_ratings = AsyncMock(return_value={ |
| "success": True, "uploaded": 10, "failed": 0, "skipped": 0 |
| }) |
| mock_service.upload_rating = AsyncMock(return_value={ |
| "success": True, "rating_id": "remote-123" |
| }) |
| mock_service.mark_as_synced = MagicMock() |
| return mock_service |
|
|
|
|
| @pytest.fixture(scope="function") |
| def mock_saas_client(): |
| """Mock AtomSaaSClient for testing.""" |
| mock_client = AsyncMock() |
| return mock_client |
|
|
|
|
| @pytest.fixture(scope="function") |
| def mock_conflict_resolver(): |
| """Mock ConflictResolutionService for testing.""" |
| mock_resolver = MagicMock() |
| mock_resolver.get_unresolved_conflicts = MagicMock(return_value=[]) |
| mock_resolver.get_conflict_by_id = MagicMock(return_value=None) |
| mock_resolver.resolve_conflict = MagicMock(return_value=None) |
| return mock_resolver |
|
|
|
|
| |
| |
| |
|
|
| class TestAdminUserList: |
| """Tests for GET /api/admin/users (list admin users)""" |
|
|
| def test_list_admin_users_success(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test listing all admin users successfully.""" |
| |
| admin1 = AdminUser( |
| id=str(uuid.uuid4()), |
| email="admin1@test.com", |
| name="Admin One", |
| password_hash=get_password_hash("Pass123!"), |
| role_id=test_admin_role.id, |
| status="active" |
| ) |
| admin2 = AdminUser( |
| id=str(uuid.uuid4()), |
| email="admin2@test.com", |
| name="Admin Two", |
| password_hash=get_password_hash("Pass456!"), |
| role_id=test_admin_role.id, |
| status="active" |
| ) |
| test_db.add(admin1) |
| test_db.add(admin2) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.get("/api/admin/users") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert isinstance(data, list) |
| assert len(data) >= 2 |
|
|
| |
| for user in data: |
| assert "id" in user |
| assert "email" in user |
| assert "name" in user |
| assert "role_id" in user |
| assert "role_name" in user |
| assert "permissions" in user |
| assert "status" in user |
| assert "last_login" in user |
|
|
| def test_list_admin_users_empty(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test listing admin users when none exist.""" |
| |
| test_db.query(AdminUser).delete() |
| test_db.commit() |
|
|
| response = authenticated_admin_client.get("/api/admin/users") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert isinstance(data, list) |
| assert len(data) == 0 |
|
|
| def test_list_admin_users_includes_permissions(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test that permissions are included in response.""" |
| admin = AdminUser( |
| id=str(uuid.uuid4()), |
| email="permissionadmin@test.com", |
| name="Permission Test Admin", |
| password_hash=get_password_hash("Pass123!"), |
| role_id=test_admin_role.id, |
| status="active" |
| ) |
| test_db.add(admin) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.get("/api/admin/users") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert len(data) >= 1 |
|
|
| |
| test_admin = next((u for u in data if u["email"] == "permissionadmin@test.com"), None) |
| assert test_admin is not None |
| assert test_admin["role_name"] == test_admin_role.name |
| assert test_admin["permissions"] == test_admin_role.permissions |
|
|
|
|
| class TestAdminUserGet: |
| """Tests for GET /api/admin/users/{admin_id} (get specific admin user)""" |
|
|
| def test_get_admin_user_success(self, authenticated_admin_client: TestClient, |
| test_admin_user: AdminUser): |
| """Test getting a specific admin user successfully.""" |
| response = authenticated_admin_client.get(f"/api/admin/users/{test_admin_user.id}") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["id"] == test_admin_user.id |
| assert data["email"] == test_admin_user.email |
| assert data["name"] == test_admin_user.name |
| assert data["role_id"] == test_admin_user.role_id |
| assert data["status"] == test_admin_user.status |
| assert "last_login" in data |
| assert "created_at" in data |
|
|
| def test_get_admin_user_not_found(self, authenticated_admin_client: TestClient): |
| """Test getting a non-existent admin user.""" |
| fake_id = str(uuid.uuid4()) |
|
|
| response = authenticated_admin_client.get(f"/api/admin/users/{fake_id}") |
|
|
| assert response.status_code == 404 |
|
|
| def test_get_admin_user_includes_created_at(self, authenticated_admin_client: TestClient, |
| test_admin_user: AdminUser): |
| """Test that created_at field is returned.""" |
| response = authenticated_admin_client.get(f"/api/admin/users/{test_admin_user.id}") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "created_at" in data |
| assert data["created_at"] is not None |
|
|
|
|
| |
| |
| |
|
|
| class TestAdminUserCreate: |
| """Tests for POST /api/admin/users (create admin user)""" |
|
|
| def test_create_admin_user_success(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test successful admin user creation.""" |
| request_data = { |
| "email": "newadmin@test.com", |
| "name": "New Admin", |
| "password": "SecurePass123!", |
| "role_id": test_admin_role.id |
| } |
|
|
| response = authenticated_admin_client.post("/api/admin/users", json=request_data) |
|
|
| assert response.status_code == 201 |
| data = response.json() |
| assert data["email"] == "newadmin@test.com" |
| assert data["name"] == "New Admin" |
| assert data["role_id"] == test_admin_role.id |
| assert data["status"] == "active" |
| assert "id" in data |
|
|
| |
| admin = test_db.query(AdminUser).filter(AdminUser.email == "newadmin@test.com").first() |
| assert admin is not None |
| assert admin.password_hash != "SecurePass123!" |
|
|
| def test_create_admin_user_role_not_found(self, authenticated_admin_client: TestClient): |
| """Test creation with non-existent role.""" |
| request_data = { |
| "email": "test@test.com", |
| "name": "Test Admin", |
| "password": "SecurePass123!", |
| "role_id": str(uuid.uuid4()) |
| } |
|
|
| response = authenticated_admin_client.post("/api/admin/users", json=request_data) |
|
|
| assert response.status_code == 404 |
|
|
| def test_create_admin_user_duplicate_email(self, authenticated_admin_client: TestClient, |
| test_admin_user: AdminUser): |
| """Test creation with duplicate email.""" |
| request_data = { |
| "email": test_admin_user.email, |
| "name": "Duplicate Admin", |
| "password": "SecurePass123!", |
| "role_id": test_admin_user.role_id |
| } |
|
|
| response = authenticated_admin_client.post("/api/admin/users", json=request_data) |
|
|
| assert response.status_code == 409 |
|
|
| def test_create_admin_user_password_hashed(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test that password is hashed.""" |
| request_data = { |
| "email": "hashed@test.com", |
| "name": "Hashed Admin", |
| "password": "PlainPassword123!", |
| "role_id": test_admin_role.id |
| } |
|
|
| response = authenticated_admin_client.post("/api/admin/users", json=request_data) |
|
|
| assert response.status_code == 201 |
|
|
| |
| admin = test_db.query(AdminUser).filter(AdminUser.email == "hashed@test.com").first() |
| assert admin is not None |
| assert admin.password_hash != "PlainPassword123!" |
| assert admin.password_hash.startswith("$2b$") or len(admin.password_hash) > 50 |
|
|
| def test_create_admin_user_default_status(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test that status defaults to active.""" |
| request_data = { |
| "email": "defaultstatus@test.com", |
| "name": "Default Status Admin", |
| "password": "SecurePass123!", |
| "role_id": test_admin_role.id |
| } |
|
|
| response = authenticated_admin_client.post("/api/admin/users", json=request_data) |
|
|
| assert response.status_code == 201 |
| assert response.json()["status"] == "active" |
|
|
|
|
| class TestAdminUserUpdate: |
| """Tests for PATCH /api/admin/users/{admin_id} (update admin user)""" |
|
|
| def test_update_admin_user_name(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_user: AdminUser): |
| """Test updating only name.""" |
| response = authenticated_admin_client.patch( |
| f"/api/admin/users/{test_admin_user.id}", |
| json={"name": "Updated Name"} |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["name"] == "Updated Name" |
|
|
| |
| test_db.refresh(test_admin_user) |
| assert test_admin_user.name == "Updated Name" |
|
|
| def test_update_admin_user_role(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_user: AdminRole): |
| """Test updating role.""" |
| |
| role2 = AdminRole( |
| id=str(uuid.uuid4()), |
| name="Second Role", |
| permissions={"test": True}, |
| description="Second role" |
| ) |
| test_db.add(role2) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.patch( |
| f"/api/admin/users/{test_admin_user.id}", |
| json={"role_id": role2.id} |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["role_id"] == role2.id |
|
|
| def test_update_admin_user_role_not_found(self, authenticated_admin_client: TestClient, |
| test_admin_user: AdminUser): |
| """Test updating with non-existent role.""" |
| response = authenticated_admin_client.patch( |
| f"/api/admin/users/{test_admin_user.id}", |
| json={"role_id": str(uuid.uuid4())} |
| ) |
|
|
| assert response.status_code == 404 |
|
|
| def test_update_admin_user_status(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_user: AdminUser): |
| """Test updating status.""" |
| response = authenticated_admin_client.patch( |
| f"/api/admin/users/{test_admin_user.id}", |
| json={"status": "inactive"} |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| |
| test_db.refresh(test_admin_user) |
| assert test_admin_user.status == "inactive" |
|
|
| def test_update_admin_user_not_found(self, authenticated_admin_client: TestClient): |
| """Test updating non-existent user.""" |
| response = authenticated_admin_client.patch( |
| f"/api/admin/users/{uuid.uuid4()}", |
| json={"name": "Test"} |
| ) |
|
|
| assert response.status_code == 404 |
|
|
|
|
| class TestAdminUserDelete: |
| """Tests for DELETE /api/admin/users/{admin_id} (delete admin user)""" |
|
|
| def test_delete_admin_user_success(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_user: AdminUser): |
| """Test successful deletion.""" |
| admin_id = test_admin_user.id |
|
|
| response = authenticated_admin_client.delete(f"/api/admin/users/{admin_id}") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "deleted" in data["message"].lower() |
|
|
| |
| admin = test_db.query(AdminUser).filter(AdminUser.id == admin_id).first() |
| assert admin is None |
|
|
| def test_delete_admin_user_not_found(self, authenticated_admin_client: TestClient): |
| """Test deleting non-existent user.""" |
| response = authenticated_admin_client.delete(f"/api/admin/users/{uuid.uuid4()}") |
|
|
| assert response.status_code == 404 |
|
|
|
|
| |
| |
| |
|
|
| class TestAdminUserLastLogin: |
| """Tests for PATCH /api/admin/users/{admin_id}/last-login""" |
|
|
| def test_update_last_login_success(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_user: AdminUser): |
| """Test updating last login timestamp.""" |
| assert test_admin_user.last_login is None |
|
|
| response = authenticated_admin_client.patch( |
| f"/api/admin/users/{test_admin_user.id}/last-login" |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "updated" in data["message"].lower() |
|
|
| |
| test_db.refresh(test_admin_user) |
| assert test_admin_user.last_login is not None |
| assert isinstance(test_admin_user.last_login, datetime) |
|
|
| def test_update_last_login_not_found(self, authenticated_admin_client: TestClient): |
| """Test updating non-existent user.""" |
| response = authenticated_admin_client.patch( |
| f"/api/admin/users/{uuid.uuid4()}/last-login" |
| ) |
|
|
| assert response.status_code == 404 |
|
|
| def test_update_last_login_sets_timestamp(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_user: AdminUser): |
| """Test that timestamp is set correctly.""" |
| old_login = test_admin_user.last_login |
| import time |
| time.sleep(0.1) |
|
|
| response = authenticated_admin_client.patch( |
| f"/api/admin/users/{test_admin_user.id}/last-login" |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| |
| test_db.refresh(test_admin_user) |
| assert test_admin_user.last_login is not None |
| if old_login: |
| assert test_admin_user.last_login > old_login |
|
|
|
|
| class TestAdminRoleList: |
| """Tests for GET /api/admin/roles (list admin roles)""" |
|
|
| def test_list_admin_roles_success(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test listing all admin roles.""" |
| |
| role2 = AdminRole( |
| id=str(uuid.uuid4()), |
| name="Another Role", |
| permissions={"test": True}, |
| description="Another test role" |
| ) |
| test_db.add(role2) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.get("/api/admin/roles") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert isinstance(data, list) |
| assert len(data) >= 2 |
|
|
| |
| for role in data: |
| assert "id" in role |
| assert "name" in role |
| assert "permissions" in role |
| assert "description" in role |
|
|
| def test_list_admin_roles_empty(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test listing when no roles exist.""" |
| test_db.query(AdminRole).delete() |
| test_db.commit() |
|
|
| response = authenticated_admin_client.get("/api/admin/roles") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert isinstance(data, list) |
| assert len(data) == 0 |
|
|
|
|
| class TestAdminRoleGet: |
| """Tests for GET /api/admin/roles/{role_id} (get specific admin role)""" |
|
|
| def test_get_admin_role_success(self, authenticated_admin_client: TestClient, |
| test_admin_role: AdminRole): |
| """Test getting specific role.""" |
| response = authenticated_admin_client.get(f"/api/admin/roles/{test_admin_role.id}") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["id"] == test_admin_role.id |
| assert data["name"] == test_admin_role.name |
| assert data["permissions"] == test_admin_role.permissions |
| assert data["description"] == test_admin_role.description |
|
|
| def test_get_admin_role_not_found(self, authenticated_admin_client: TestClient): |
| """Test getting non-existent role.""" |
| response = authenticated_admin_client.get(f"/api/admin/roles/{uuid.uuid4()}") |
|
|
| assert response.status_code == 404 |
|
|
|
|
| class TestAdminRoleCreate: |
| """Tests for POST /api/admin/roles (create admin role)""" |
|
|
| def test_create_admin_role_success(self, authenticated_admin_client: TestClient): |
| """Test successful role creation.""" |
| request_data = { |
| "name": "New Role", |
| "permissions": {"users": True, "reports": False}, |
| "description": "Test role" |
| } |
|
|
| response = authenticated_admin_client.post("/api/admin/roles", json=request_data) |
|
|
| assert response.status_code == 201 |
| data = response.json() |
| assert data["name"] == "New Role" |
| assert data["permissions"] == {"users": True, "reports": False} |
| assert data["description"] == "Test role" |
| assert "id" in data |
|
|
| def test_create_admin_role_duplicate_name(self, authenticated_admin_client: TestClient, |
| test_admin_role: AdminRole): |
| """Test creation with duplicate name.""" |
| request_data = { |
| "name": test_admin_role.name, |
| "permissions": {"users": True}, |
| "description": "Duplicate" |
| } |
|
|
| response = authenticated_admin_client.post("/api/admin/roles", json=request_data) |
|
|
| assert response.status_code == 409 |
|
|
|
|
| |
| |
| |
|
|
| class TestAdminRoleUpdate: |
| """Tests for PATCH /api/admin/roles/{role_id} (update admin role)""" |
|
|
| def test_update_admin_role_name(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test updating role name.""" |
| response = authenticated_admin_client.patch( |
| f"/api/admin/roles/{test_admin_role.id}", |
| json={"name": "Updated Name"} |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["name"] == "Updated Name" |
|
|
| |
| test_db.refresh(test_admin_role) |
| assert test_admin_role.name == "Updated Name" |
|
|
| def test_update_admin_role_name_conflict(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test updating name to existing name.""" |
| |
| role2 = AdminRole( |
| id=str(uuid.uuid4()), |
| name="Another Role", |
| permissions={"test": True}, |
| description="Second role" |
| ) |
| test_db.add(role2) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.patch( |
| f"/api/admin/roles/{test_admin_role.id}", |
| json={"name": role2.name} |
| ) |
|
|
| assert response.status_code == 409 |
|
|
| def test_update_admin_role_permissions(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test updating permissions.""" |
| new_permissions = {"users": False, "admin": True} |
| response = authenticated_admin_client.patch( |
| f"/api/admin/roles/{test_admin_role.id}", |
| json={"permissions": new_permissions} |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| |
| test_db.refresh(test_admin_role) |
| assert test_admin_role.permissions == new_permissions |
|
|
| def test_update_admin_role_description(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test updating description.""" |
| response = authenticated_admin_client.patch( |
| f"/api/admin/roles/{test_admin_role.id}", |
| json={"description": "New description"} |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| |
| test_db.refresh(test_admin_role) |
| assert test_admin_role.description == "New description" |
|
|
| def test_update_admin_role_not_found(self, authenticated_admin_client: TestClient): |
| """Test updating non-existent role.""" |
| response = authenticated_admin_client.patch( |
| f"/api/admin/roles/{uuid.uuid4()}", |
| json={"name": "Test"} |
| ) |
|
|
| assert response.status_code == 404 |
|
|
|
|
| class TestAdminRoleDelete: |
| """Tests for DELETE /api/admin/roles/{role_id} (delete admin role)""" |
|
|
| def test_delete_admin_role_success(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test successful role deletion.""" |
| |
| role = AdminRole( |
| id=str(uuid.uuid4()), |
| name="Deletable Role", |
| permissions={"test": True}, |
| description="Role for deletion test" |
| ) |
| test_db.add(role) |
| test_db.commit() |
| role_id = role.id |
|
|
| response = authenticated_admin_client.delete(f"/api/admin/roles/{role_id}") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "deleted" in data["message"].lower() |
|
|
| |
| deleted_role = test_db.query(AdminRole).filter(AdminRole.id == role_id).first() |
| assert deleted_role is None |
|
|
| def test_delete_admin_role_with_users_fails(self, authenticated_admin_client: TestClient, |
| test_admin_role: AdminRole, |
| test_admin_user: AdminUser): |
| """Test deletion when role assigned to users.""" |
| |
| response = authenticated_admin_client.delete(f"/api/admin/roles/{test_admin_role.id}") |
|
|
| assert response.status_code == 409 |
|
|
| def test_delete_admin_role_not_found(self, authenticated_admin_client: TestClient): |
| """Test deleting non-existent role.""" |
| response = authenticated_admin_client.delete(f"/api/admin/roles/{uuid.uuid4()}") |
|
|
| assert response.status_code == 404 |
|
|
|
|
| |
| |
| |
|
|
| class TestWebSocketStatus: |
| """Tests for GET /api/admin/websocket/status""" |
|
|
| def test_get_websocket_status_connected(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test WebSocket status when connected.""" |
| |
| ws_state = WebSocketState( |
| id=1, |
| connected=True, |
| ws_url="wss://api.example.com/ws", |
| last_connected_at=datetime.now(timezone.utc), |
| last_message_at=datetime.now(timezone.utc), |
| reconnect_attempts=3, |
| consecutive_failures=0, |
| disconnect_reason=None, |
| fallback_to_polling=False |
| ) |
| test_db.add(ws_state) |
| test_db.commit() |
|
|
| response = authenticated_admin_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 |
| assert data["rate_limit_messages_per_sec"] == 100 |
|
|
| def test_get_websocket_status_no_state(self, authenticated_admin_client: TestClient): |
| """Test WebSocket status when no state exists.""" |
| response = authenticated_admin_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 |
| assert data["fallback_to_polling"] is False |
|
|
| def test_get_websocket_status_disconnected(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test WebSocket status when disconnected.""" |
| ws_state = WebSocketState( |
| id=1, |
| connected=False, |
| reconnect_attempts=5, |
| consecutive_failures=3, |
| disconnect_reason="connection_lost", |
| fallback_to_polling=True |
| ) |
| test_db.add(ws_state) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.get("/api/admin/websocket/status") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["connected"] is False |
| assert data["last_disconnect_reason"] == "connection_lost" |
| assert data["fallback_to_polling"] is True |
|
|
|
|
| class TestWebSocketReconnect: |
| """Tests for POST /api/admin/websocket/reconnect""" |
|
|
| def test_trigger_websocket_reconnect(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test forcing WebSocket reconnection.""" |
| |
| ws_state = WebSocketState( |
| id=1, |
| connected=False, |
| reconnect_attempts=5, |
| consecutive_failures=3, |
| fallback_to_polling=True |
| ) |
| test_db.add(ws_state) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.post("/api/admin/websocket/reconnect") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["reconnect_triggered"] is True |
| assert "Reconnection triggered" in data["message"] |
|
|
| def test_trigger_websocket_reconnect_resets_counters(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test that reconnect resets counters.""" |
| ws_state = WebSocketState( |
| id=1, |
| connected=False, |
| reconnect_attempts=5, |
| consecutive_failures=3, |
| fallback_to_polling=True |
| ) |
| test_db.add(ws_state) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.post("/api/admin/websocket/reconnect") |
|
|
| assert response.status_code == 200 |
|
|
| |
| test_db.refresh(ws_state) |
| assert ws_state.reconnect_attempts == 0 |
| assert ws_state.consecutive_failures == 0 |
| assert ws_state.fallback_to_polling is False |
|
|
|
|
| class TestWebSocketToggle: |
| """Tests for POST /api/admin/websocket/disable and /enable""" |
|
|
| def test_disable_websocket(self, authenticated_admin_client: TestClient, test_db: Session): |
| """Test disabling WebSocket.""" |
| ws_state = WebSocketState( |
| id=1, |
| connected=True, |
| fallback_to_polling=False |
| ) |
| test_db.add(ws_state) |
| test_db.commit() |
|
|
| response = authenticated_admin_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 |
|
|
| |
| test_db.refresh(ws_state) |
| assert ws_state.fallback_to_polling is True |
| assert ws_state.connected is False |
| assert ws_state.disconnect_reason == "disabled_by_admin" |
|
|
| def test_enable_websocket(self, authenticated_admin_client: TestClient, test_db: Session): |
| """Test enabling WebSocket.""" |
| ws_state = WebSocketState( |
| id=1, |
| fallback_to_polling=True, |
| next_ws_attempt_at=datetime.now(timezone.utc) + timedelta(hours=1), |
| reconnect_attempts=5 |
| ) |
| test_db.add(ws_state) |
| test_db.commit() |
|
|
| response = authenticated_admin_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 |
|
|
| |
| test_db.refresh(ws_state) |
| assert ws_state.fallback_to_polling is False |
| assert ws_state.next_ws_attempt_at is None |
| assert ws_state.reconnect_attempts == 0 |
|
|
| def test_disable_websocket_sets_disconnect_reason(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test disable sets disconnect reason.""" |
| ws_state = WebSocketState(id=1, connected=True) |
| test_db.add(ws_state) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.post("/api/admin/websocket/disable") |
|
|
| assert response.status_code == 200 |
|
|
| |
| test_db.refresh(ws_state) |
| assert ws_state.disconnect_reason == "disabled_by_admin" |
|
|
|
|
| |
| |
| |
|
|
| class TestRatingSync: |
| """Tests for POST /api/admin/sync/ratings""" |
|
|
| def test_trigger_rating_sync_success(self, authenticated_admin_client: TestClient, |
| test_db: Session, mock_rating_sync_service): |
| """Test triggering rating sync successfully.""" |
| |
| for i in range(5): |
| rating = SkillRating( |
| id=f"rating_{i}", |
| skill_id=f"skill_{i}", |
| user_id="test_user", |
| tenant_id="test_tenant", |
| rating=5, |
| synced_at=None |
| ) |
| test_db.add(rating) |
| test_db.commit() |
|
|
| with patch('core.rating_sync_service.RatingSyncService', return_value=mock_rating_sync_service): |
| response = authenticated_admin_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"] == 10 |
|
|
| def test_trigger_rating_sync_upload_all(self, authenticated_admin_client: TestClient, |
| mock_rating_sync_service): |
| """Test sync with upload_all flag.""" |
| with patch('core.rating_sync_service.RatingSyncService', return_value=mock_rating_sync_service): |
| response = authenticated_admin_client.post( |
| "/api/admin/sync/ratings", |
| json={"upload_all": True} |
| ) |
|
|
| assert response.status_code == 200 |
| assert response.json()["uploaded"] == 10 |
|
|
| def test_trigger_rating_sync_in_progress(self, authenticated_admin_client: TestClient): |
| """Test sync when already in progress.""" |
| mock_service = MagicMock() |
| mock_service._sync_in_progress = True |
| mock_service.get_pending_ratings = MagicMock(return_value=[]) |
|
|
| with patch('core.rating_sync_service.RatingSyncService', return_value=mock_service): |
| response = authenticated_admin_client.post( |
| "/api/admin/sync/ratings", |
| json={"upload_all": False} |
| ) |
|
|
| assert response.status_code == 503 |
|
|
|
|
| class TestFailedRatingUploads: |
| """Tests for GET /api/admin/ratings/failed-uploads""" |
|
|
| def test_get_failed_rating_uploads(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test listing failed uploads.""" |
| |
| for i in range(3): |
| failed = FailedRatingUpload( |
| id=f"failed_{i}", |
| rating_id=f"rating_{i}", |
| error_message=f"Error {i}", |
| failed_at=datetime.now(timezone.utc), |
| retry_count=i, |
| last_retry_at=datetime.now(timezone.utc) if i > 0 else None |
| ) |
| test_db.add(failed) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.get("/api/admin/ratings/failed-uploads") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert len(data) == 3 |
| assert data[0]["rating_id"] == "rating_0" |
| assert data[0]["retry_count"] == 0 |
|
|
| def test_get_failed_rating_uploads_empty(self, authenticated_admin_client: TestClient): |
| """Test listing when no failed uploads.""" |
| response = authenticated_admin_client.get("/api/admin/ratings/failed-uploads") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data == [] |
|
|
| def test_get_failed_uploads_limited(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test that results are limited to 100.""" |
| |
| for i in range(150): |
| failed = FailedRatingUpload( |
| id=f"failed_{i}", |
| rating_id=f"rating_{i}", |
| error_message=f"Error {i}", |
| failed_at=datetime.now(timezone.utc), |
| retry_count=0 |
| ) |
| test_db.add(failed) |
| test_db.commit() |
|
|
| response = authenticated_admin_client.get("/api/admin/ratings/failed-uploads") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert len(data) <= 100 |
|
|
|
|
| class TestRetryRatingUpload: |
| """Tests for POST /api/admin/ratings/failed-uploads/{failed_id}/retry""" |
|
|
| def test_retry_failed_upload_success(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test retrying failed upload successfully.""" |
| |
| rating = SkillRating( |
| id="rating_1", |
| skill_id="skill_1", |
| user_id="test_user", |
| tenant_id="test_tenant", |
| rating=5, |
| synced_at=None |
| ) |
| test_db.add(rating) |
|
|
| |
| failed = FailedRatingUpload( |
| id="failed_1", |
| rating_id="rating_1", |
| error_message="Network error", |
| failed_at=datetime.now(timezone.utc), |
| retry_count=0 |
| ) |
| test_db.add(failed) |
| test_db.commit() |
|
|
| |
| mock_service = MagicMock() |
| mock_service.upload_rating = AsyncMock(return_value={ |
| "success": True, "rating_id": "remote-123" |
| }) |
| mock_service.mark_as_synced = MagicMock() |
|
|
| with patch('core.rating_sync_service.RatingSyncService', return_value=mock_service): |
| response = authenticated_admin_client.post( |
| "/api/admin/ratings/failed-uploads/failed_1/retry" |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["success"] is True |
| assert data["retry_triggered"] is True |
|
|
| def test_retry_failed_upload_rating_deleted(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test retry when rating was deleted.""" |
| |
| failed = FailedRatingUpload( |
| id="failed_1", |
| rating_id="deleted_rating", |
| error_message="Network error", |
| failed_at=datetime.now(timezone.utc), |
| retry_count=0 |
| ) |
| test_db.add(failed) |
| test_db.commit() |
|
|
| response = authenticated_admin_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_upload_fails_again(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test retry that fails again.""" |
| |
| rating = SkillRating( |
| id="rating_1", |
| skill_id="skill_1", |
| user_id="test_user", |
| tenant_id="test_tenant", |
| rating=5 |
| ) |
| test_db.add(rating) |
|
|
| |
| failed = FailedRatingUpload( |
| id="failed_1", |
| rating_id="rating_1", |
| error_message="Network error", |
| failed_at=datetime.now(timezone.utc), |
| retry_count=1 |
| ) |
| test_db.add(failed) |
| test_db.commit() |
|
|
| |
| mock_service = MagicMock() |
| mock_service.upload_rating = AsyncMock(return_value={ |
| "success": False, "error": "API error" |
| }) |
|
|
| with patch('core.rating_sync_service.RatingSyncService', return_value=mock_service): |
| response = authenticated_admin_client.post( |
| "/api/admin/ratings/failed-uploads/failed_1/retry" |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["success"] is False |
|
|
| |
| test_db.refresh(failed) |
| assert failed.retry_count == 2 |
|
|
| def test_retry_failed_upload_not_found(self, authenticated_admin_client: TestClient): |
| """Test retrying non-existent failed upload.""" |
| response = authenticated_admin_client.post( |
| "/api/admin/ratings/failed-uploads/nonexistent/retry" |
| ) |
|
|
| assert response.status_code == 404 |
|
|
|
|
| |
| |
| |
|
|
| class TestConflictList: |
| """Tests for GET /api/admin/conflicts""" |
|
|
| def test_list_conflicts(self, authenticated_admin_client: TestClient, test_db: Session): |
| """Test listing unresolved conflicts.""" |
| |
| for i in range(3): |
| conflict = ConflictLog( |
| id=i + 1, |
| skill_id=f"skill_{i}", |
| conflict_type="version_mismatch", |
| severity="high", |
| local_data={"version": "1.0"}, |
| remote_data={"version": "2.0"}, |
| created_at=datetime.now(timezone.utc) |
| ) |
| test_db.add(conflict) |
| test_db.commit() |
|
|
| mock_resolver = MagicMock() |
| mock_resolver.get_unresolved_conflicts = MagicMock(return_value=test_db.query(ConflictLog).all()) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_client.get("/api/admin/conflicts") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "conflicts" in data |
| assert data["total_count"] == 3 |
| assert data["page"] == 1 |
|
|
| def test_list_conflicts_with_filters(self, authenticated_admin_client: TestClient): |
| """Test listing with severity and type filters.""" |
| mock_resolver = MagicMock() |
| mock_resolver.get_unresolved_conflicts = MagicMock(return_value=[]) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_client.get( |
| "/api/admin/conflicts?severity=HIGH&conflict_type=skill_data" |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| def test_list_conflicts_with_pagination(self, authenticated_admin_client: TestClient): |
| """Test pagination parameters.""" |
| mock_resolver = MagicMock() |
| mock_resolver.get_unresolved_conflicts = MagicMock(return_value=[]) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_client.get( |
| "/api/admin/conflicts?page=2&page_size=25" |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert data["page"] == 2 |
| assert data["page_size"] == 25 |
|
|
|
|
| class TestConflictGet: |
| """Tests for GET /api/admin/conflicts/{conflict_id}""" |
|
|
| def test_get_conflict_success(self, authenticated_admin_client: TestClient, test_db: Session): |
| """Test getting conflict by ID.""" |
| conflict = ConflictLog( |
| id=1, |
| skill_id="skill_1", |
| conflict_type="version_mismatch", |
| severity="high", |
| local_data={"version": "1.0"}, |
| remote_data={"version": "2.0"}, |
| created_at=datetime.now(timezone.utc) |
| ) |
| test_db.add(conflict) |
| test_db.commit() |
|
|
| mock_resolver = MagicMock() |
| mock_resolver.get_conflict_by_id = MagicMock(return_value=conflict) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_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_admin_client: TestClient): |
| """Test getting non-existent conflict.""" |
| mock_resolver = MagicMock() |
| mock_resolver.get_conflict_by_id = MagicMock(return_value=None) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_client.get("/api/admin/conflicts/999") |
|
|
| assert response.status_code == 404 |
|
|
|
|
| class TestConflictResolve: |
| """Tests for POST /api/admin/conflicts/{conflict_id}/resolve""" |
|
|
| def test_resolve_conflict_remote_wins(self, authenticated_admin_client: TestClient, |
| test_db: Session): |
| """Test resolving with remote_wins strategy.""" |
| conflict = ConflictLog( |
| id=1, |
| skill_id="skill_1", |
| conflict_type="version_mismatch", |
| severity="high", |
| local_data={"version": "1.0"}, |
| remote_data={"version": "2.0"}, |
| created_at=datetime.now(timezone.utc) |
| ) |
| test_db.add(conflict) |
| test_db.commit() |
|
|
| resolved_data = {"skill_id": "skill_1", "version": "2.0"} |
|
|
| mock_resolver = MagicMock() |
| mock_resolver.resolve_conflict = MagicMock(return_value=resolved_data) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_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_admin_client: TestClient): |
| """Test resolving with local_wins strategy.""" |
| resolved_data = {"skill_id": "skill_1", "version": "1.0"} |
|
|
| mock_resolver = MagicMock() |
| mock_resolver.resolve_conflict = MagicMock(return_value=resolved_data) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_client.post( |
| "/api/admin/conflicts/1/resolve", |
| json={"strategy": "local_wins", "resolved_by": "admin"} |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| def test_resolve_conflict_merge(self, authenticated_admin_client: TestClient): |
| """Test resolving with merge strategy.""" |
| resolved_data = {"skill_id": "skill_1", "version": "2.0", "local_changes": "preserved"} |
|
|
| mock_resolver = MagicMock() |
| mock_resolver.resolve_conflict = MagicMock(return_value=resolved_data) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_client.post( |
| "/api/admin/conflicts/1/resolve", |
| json={"strategy": "merge", "resolved_by": "admin"} |
| ) |
|
|
| assert response.status_code == 200 |
|
|
| def test_resolve_conflict_invalid_strategy(self, authenticated_admin_client: TestClient): |
| """Test with invalid strategy.""" |
| response = authenticated_admin_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_admin_client: TestClient): |
| """Test resolving non-existent conflict.""" |
| mock_resolver = MagicMock() |
| mock_resolver.resolve_conflict = MagicMock(return_value=None) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_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 TestBulkConflictResolve: |
| """Tests for POST /api/admin/conflicts/bulk-resolve""" |
|
|
| def test_bulk_resolve_conflicts_success(self, authenticated_admin_client: TestClient): |
| """Test bulk resolving conflicts successfully.""" |
| mock_resolver = MagicMock() |
| mock_resolver.resolve_conflict = MagicMock( |
| return_value={"skill_id": "skill_1", "version": "2.0"} |
| ) |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_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 |
|
|
| def test_bulk_resolve_with_failures(self, authenticated_admin_client: TestClient): |
| """Test bulk resolve with some failures.""" |
| mock_resolver = MagicMock() |
|
|
| |
| 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_resolver.resolve_conflict.side_effect = side_effect |
|
|
| with patch('core.conflict_resolution_service.ConflictResolutionService', return_value=mock_resolver): |
| response = authenticated_admin_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_invalid_strategy(self, authenticated_admin_client: TestClient): |
| """Test bulk resolve with invalid strategy.""" |
| response = authenticated_admin_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_empty_conflict_ids(self, authenticated_admin_client: TestClient): |
| """Test with empty conflict_ids list.""" |
| response = authenticated_admin_client.post( |
| "/api/admin/conflicts/bulk-resolve", |
| json={ |
| "conflict_ids": [], |
| "strategy": "remote_wins", |
| "resolved_by": "admin" |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
| def test_bulk_resolve_too_many_conflicts(self, authenticated_admin_client: TestClient): |
| """Test exceeding max conflict_ids.""" |
| conflict_ids = list(range(101)) |
|
|
| response = authenticated_admin_client.post( |
| "/api/admin/conflicts/bulk-resolve", |
| json={ |
| "conflict_ids": conflict_ids, |
| "strategy": "remote_wins", |
| "resolved_by": "admin" |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
|
|
| |
| |
| |
|
|
| class TestAdminRoutesAuth: |
| """Tests for super_admin requirement on all endpoints""" |
|
|
| def test_unauthenticated_request_fails(self, unauthenticated_client: TestClient): |
| """Test unauthenticated request.""" |
| response = unauthenticated_client.get("/api/admin/users") |
|
|
| |
| assert response.status_code in [401, 403] |
|
|
| def test_inactive_admin_blocked(self, authenticated_admin_client: TestClient, |
| test_db: Session, test_admin_role: AdminRole): |
| """Test that inactive admin is blocked.""" |
| |
| inactive_super = User( |
| id=str(uuid.uuid4()), |
| email="inactive@test.com", |
| name="Inactive Super", |
| role="super_admin", |
| status="inactive", |
| email_verified=True |
| ) |
| test_db.add(inactive_super) |
| test_db.commit() |
|
|
| |
| def override_get_current_user(): |
| return inactive_super |
|
|
| def override_require_super_admin(): |
| return inactive_super |
|
|
| authenticated_admin_client.app.dependency_overrides[ |
| authenticated_admin_client.app.dependencies[0].dependency |
| ] = override_get_current_user |
|
|
| from api.admin_routes import require_super_admin |
| authenticated_admin_client.app.dependency_overrides[require_super_admin] = override_require_super_admin |
|
|
| |
| |
| response = authenticated_admin_client.get("/api/admin/users") |
| |
| assert response.status_code in [200, 403] |
|
|
|
|
| class TestGovernanceEnforcement: |
| """Tests for AUTONOMOUS maturity governance on critical operations""" |
|
|
| def test_create_user_governance_CRITICAL(self, authenticated_admin_client: TestClient, |
| test_admin_role: AdminRole): |
| """Test CRITICAL complexity governance for create user.""" |
| |
| with patch('core.agent_governance_service.GovernanceCache') as mock_cache: |
| mock_instance = MagicMock() |
| mock_instance.can_perform_action.return_value = ( |
| False, "PENDING_APPROVAL", "Not AUTONOMOUS" |
| ) |
| mock_cache.return_value = mock_instance |
|
|
| |
| |
| |
| request_data = { |
| "email": "governance@test.com", |
| "name": "Governance Test", |
| "password": "SecurePass123!", |
| "role_id": test_admin_role.id |
| } |
|
|
| |
| response = authenticated_admin_client.post("/api/admin/users", json=request_data) |
| |
| assert response.status_code in [201, 403] |
|
|
| def test_AUTONOMOUS_passes_all_checks(self, authenticated_admin_client: TestClient, |
| test_admin_role: AdminRole): |
| """Test AUTONOMOUS maturity passes all checks.""" |
| |
| with patch('core.agent_governance_service.GovernanceCache') as mock_cache: |
| mock_instance = MagicMock() |
| mock_instance.can_perform_action.return_value = ( |
| True, "AUTONOMOUS", "All checks passed" |
| ) |
| mock_cache.return_value = mock_instance |
|
|
| request_data = { |
| "email": "autonomous@test.com", |
| "name": "Autonomous Test", |
| "password": "SecurePass123!", |
| "role_id": test_admin_role.id |
| } |
|
|
| |
| response = authenticated_admin_client.post("/api/admin/users", json=request_data) |
| assert response.status_code == 201 |
|
|