- Update all datetime.utcnow() to datetime.now(UTC) for Python 3.12+ compatibility - Update all datetime.utcfromtimestamp() to datetime.fromtimestamp(..., UTC) - Fix dataclass default_factory to use lambda: datetime.now(UTC) - Update auth, audit, billing, and realtime modules - Add UTC import from datetime module - Update pytest configuration to include pytest-asyncio - All 28 Phase 8 enterprise tests pass with no warnings Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
492 lines
14 KiB
Python
492 lines
14 KiB
Python
"""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"])
|