"""Phase 8 엔터프라이즈 기능 테스트. 테스트: - 멀티테넌트 인증 - 감시 로그 - WebSocket 실시간 - 비용 관리 """ import pytest from datetime import datetime, timedelta from ont_platform.auth.models import Organization, User, APIKey, CurrentUser from ont_platform.auth.auth import JWTAuth, APIKeyAuth, PasswordHasher from ont_platform.auth.rbac import RBAC, Role, Permission from ont_platform.audit.logger import AuditLogger from ont_platform.audit.models import AuditLog, AuditAction, ResourceType from ont_platform.billing.calculator import CostCalculator from ont_platform.billing.models import OperationType, SubscriptionTier, Subscription from ont_platform.realtime.websocket import ConnectionManager from ont_platform.realtime.broadcaster import EventBroadcaster # ============================================================================ # 인증 테스트 # ============================================================================ class TestMultitenantAuth: """멀티테넌트 인증.""" def test_organization_creation(self): """조직 생성.""" org = Organization(name="Test Organization") assert org.name == "Test Organization" assert org.subscription_tier == "free" assert org.is_active def test_user_creation(self): """사용자 생성.""" user = User( org_id="org_123", email="user@example.com", username="testuser", role="editor", ) assert user.org_id == "org_123" assert user.email == "user@example.com" assert user.role == "editor" def test_api_key_generation(self): """API 키 생성.""" api_key = APIKeyAuth.generate_key() assert api_key.startswith("sk_") assert len(api_key) > 20 def test_api_key_hashing(self): """API 키 해싱.""" api_key = "sk_test123" hash1 = APIKeyAuth.hash_key(api_key) hash2 = APIKeyAuth.hash_key(api_key) assert hash1 == hash2 # 같은 키는 같은 해시 def test_password_hashing(self): """비밀번호 해싱.""" password = "my_secure_password" hashed = PasswordHasher.hash_password(password) assert hashed != password assert PasswordHasher.verify_password(password, hashed) assert not PasswordHasher.verify_password("wrong_password", hashed) def test_jwt_token_creation(self): """JWT 토큰 생성.""" token = JWTAuth.create_token( user_id="user_123", org_id="org_123", email="user@example.com", role="editor", ) assert isinstance(token, str) assert len(token) > 50 def test_jwt_token_verification(self): """JWT 토큰 검증.""" token = JWTAuth.create_token( user_id="user_123", org_id="org_123", email="user@example.com", role="editor", ) payload = JWTAuth.verify_token(token) assert payload.user_id == "user_123" assert payload.org_id == "org_123" assert payload.role == "editor" def test_jwt_token_expiration(self): """JWT 토큰 만료.""" token = JWTAuth.create_token( user_id="user_123", org_id="org_123", email="user@example.com", role="editor", expires_delta=timedelta(seconds=-1), # 이미 만료됨 ) from fastapi import HTTPException with pytest.raises(HTTPException): JWTAuth.verify_token(token) def test_current_user_creation(self): """현재 사용자 객체 생성.""" user = CurrentUser( user_id="user_123", org_id="org_123", email="user@example.com", username="testuser", role="editor", is_active=True, ) assert user.user_id == "user_123" assert user.org_id == "org_123" # ============================================================================ # RBAC 테스트 # ============================================================================ class TestRBAC: """역할 기반 액세스 제어.""" def test_admin_permissions(self): """관리자 권한.""" rbac = RBAC() permissions = rbac.get_permissions(Role.ADMIN.value) assert Permission.READ_ENTITY in permissions assert Permission.DELETE_ENTITY in permissions assert Permission.MANAGE_USERS in permissions assert Permission.VIEW_AUDIT_LOG in permissions def test_editor_permissions(self): """편집자 권한.""" rbac = RBAC() permissions = rbac.get_permissions(Role.EDITOR.value) assert Permission.READ_ENTITY in permissions assert Permission.CREATE_ENTITY in permissions assert Permission.DELETE_ENTITY in permissions assert Permission.MANAGE_USERS not in permissions def test_viewer_permissions(self): """뷰어 권한.""" rbac = RBAC() permissions = rbac.get_permissions(Role.VIEWER.value) assert Permission.READ_ENTITY in permissions assert Permission.CREATE_ENTITY not in permissions assert Permission.DELETE_ENTITY not in permissions def test_permission_check(self): """권한 확인.""" rbac = RBAC() assert rbac.has_permission(Role.ADMIN.value, Permission.DELETE_ENTITY.value) assert not rbac.has_permission( Role.VIEWER.value, Permission.DELETE_ENTITY.value ) def test_all_permissions_retrieval(self): """모든 권한 조회.""" rbac = RBAC() all_perms = rbac.get_all_permissions() assert "admin" in all_perms assert "editor" in all_perms assert "viewer" in all_perms assert "api" in all_perms # ============================================================================ # 감시 로그 테스트 # ============================================================================ class TestAuditLogging: """감시 로깅.""" @pytest.mark.asyncio async def test_audit_log_creation(self): """감시 로그 생성.""" logger = AuditLogger() log = await logger.log_action( org_id="org_123", user_id="user_456", action=AuditAction.CREATE, resource_type=ResourceType.ENTITY, resource_id="entity_789", ) assert log.org_id == "org_123" assert log.user_id == "user_456" assert log.action == AuditAction.CREATE @pytest.mark.asyncio async def test_audit_log_retrieval(self): """감시 로그 조회.""" logger = AuditLogger() await logger.log_action( org_id="org_123", user_id="user_456", action=AuditAction.UPDATE, resource_type=ResourceType.ENTITY, resource_id="entity_789", ) logs = await logger.get_audit_trail( org_id="org_123", resource_id="entity_789", ) assert len(logs) > 0 assert logs[0].resource_id == "entity_789" @pytest.mark.asyncio async def test_audit_statistics(self): """감시 통계.""" logger = AuditLogger() for i in range(3): await logger.log_action( org_id="org_123", user_id="user_456", action=AuditAction.READ, resource_type=ResourceType.ENTITY, resource_id="entity_789", ) stats = await logger.get_statistics(org_id="org_123") assert stats["total_logs"] >= 3 assert "by_action" in stats # ============================================================================ # 비용 관리 테스트 # ============================================================================ class TestBillingAndQuota: """비용 관리 및 할당량.""" @pytest.mark.asyncio async def test_cost_calculation(self): """비용 계산.""" calc = CostCalculator() cost = await calc.calculate_cost( operation_type=OperationType.LLM_CALL, quantity=1000, # 1000 토큰 ) assert cost == 1.0 # 1000 * $0.001 @pytest.mark.asyncio async def test_usage_recording(self): """사용량 기록.""" calc = CostCalculator() usage = await calc.record_usage( org_id="org_123", user_id="user_456", operation_type=OperationType.LLM_CALL, quantity=500, ) assert usage.org_id == "org_123" assert usage.quantity == 500 assert usage.cost == 0.5 @pytest.mark.asyncio async def test_quota_check_within_limit(self): """할당량 확인 (범위 내).""" calc = CostCalculator() subscription = Subscription( org_id="org_123", tier=SubscriptionTier.PRO, monthly_limit=100.0, current_month_cost=50.0, ) allowed, msg = await calc.check_quota( org_id="org_123", subscription=subscription, estimated_cost=30.0, ) assert allowed assert "OK" in msg @pytest.mark.asyncio async def test_quota_check_exceeded(self): """할당량 확인 (초과).""" calc = CostCalculator() subscription = Subscription( org_id="org_123", tier=SubscriptionTier.FREE, monthly_limit=10.0, current_month_cost=9.0, ) allowed, msg = await calc.check_quota( org_id="org_123", subscription=subscription, estimated_cost=5.0, ) assert not allowed assert "Quota exceeded" in msg @pytest.mark.asyncio async def test_usage_statistics(self): """사용량 통계.""" calc = CostCalculator() await calc.record_usage( org_id="org_123", user_id="user_456", operation_type=OperationType.API_CALL, quantity=10, ) await calc.record_usage( org_id="org_123", user_id="user_456", operation_type=OperationType.LLM_CALL, quantity=1000, ) stats = await calc.get_usage_statistics(org_id="org_123") assert stats.total_cost > 0 assert stats.api_calls >= 1 assert stats.llm_tokens >= 1000 # ============================================================================ # WebSocket 테스트 # ============================================================================ class TestWebSocketAndBroadcasting: """WebSocket 및 브로드캐스팅.""" @pytest.mark.asyncio async def test_connection_tracking(self): """연결 추적.""" manager = ConnectionManager() # 연결 수 확인 assert manager.get_connection_count("org_123") == 0 assert "org_123" not in manager.get_org_ids() @pytest.mark.asyncio async def test_broadcaster_entity_created(self): """엔티티 생성 이벤트.""" manager = ConnectionManager() broadcaster = EventBroadcaster(manager) entity = {"id": "entity_123", "label": "Test Entity"} sent = await broadcaster.broadcast_entity_created( org_id="org_123", entity=entity, user_id="user_456", ) # 연결이 없으므로 0 assert sent == 0 @pytest.mark.asyncio async def test_broadcaster_graph_analyzed(self): """그래프 분석 이벤트.""" manager = ConnectionManager() broadcaster = EventBroadcaster(manager) results = {"centrality": {"entity_1": 0.95}} sent = await broadcaster.broadcast_graph_analyzed( org_id="org_123", analysis_type="pagerank", results=results, ) assert sent == 0 # 연결 없음 @pytest.mark.asyncio async def test_broadcaster_notification(self): """일반 알림.""" manager = ConnectionManager() broadcaster = EventBroadcaster(manager) sent = await broadcaster.broadcast_notification( org_id="org_123", title="Test Alert", message="This is a test", severity="info", ) assert sent == 0 # ============================================================================ # 통합 테스트 # ============================================================================ class TestPhase8Integration: """Phase 8 통합 시나리오.""" @pytest.mark.asyncio async def test_complete_workflow(self): """완전한 워크플로우.""" # 1. 조직 생성 org = Organization(name="Test Org") assert org.is_active # 2. 사용자 생성 user = User( org_id=org.id, email="user@example.com", username="testuser", role="editor", ) # 3. 토큰 생성 token = JWTAuth.create_token( user_id=user.id, org_id=org.id, email=user.email, role=user.role, ) assert token # 4. 토큰 검증 payload = JWTAuth.verify_token(token) assert payload.org_id == org.id # 5. 감시 로그 logger = AuditLogger() log = await logger.log_action( org_id=org.id, user_id=user.id, action=AuditAction.CREATE, resource_type=ResourceType.ENTITY, resource_id="entity_123", ) assert log.org_id == org.id # 6. 비용 기록 calc = CostCalculator() usage = await calc.record_usage( org_id=org.id, user_id=user.id, operation_type=OperationType.API_CALL, quantity=1, ) assert usage.cost > 0 def test_rbac_integration(self): """RBAC 통합.""" rbac = RBAC() # 역할별 권한 확인 assert rbac.has_permission(Role.ADMIN.value, Permission.MANAGE_USERS.value) assert not rbac.has_permission( Role.VIEWER.value, Permission.DELETE_ENTITY.value ) if __name__ == "__main__": pytest.main([__file__, "-v"])