"""GUARDiA ITSM 단위 테스트 — tenant_ai + workflow_engine""" import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) import json import pytest from datetime import datetime # ── TenantAIModel / TenantKBDoc ORM 모델 ──────────────────────────────────── class TestTenantAIModelORM: def test_import_orm_models(self): from models import TenantAIModel, TenantKBDoc assert TenantAIModel.__tablename__ == "tb_tenant_ai_model" assert TenantKBDoc.__tablename__ == "tb_tenant_kb_doc" def test_tenant_ai_model_columns(self): from models import TenantAIModel cols = {c.name for c in TenantAIModel.__table__.columns} required = {"id", "tenant_id", "model_name", "base_model", "dataset_size", "status", "accuracy", "created_at"} assert required.issubset(cols), f"누락 컬럼: {required - cols}" def test_tenant_kb_doc_columns(self): from models import TenantKBDoc cols = {c.name for c in TenantKBDoc.__table__.columns} required = {"id", "tenant_id", "title", "content", "created_at"} assert required.issubset(cols), f"누락 컬럼: {required - cols}" def test_tenant_id_indexed(self): from models import TenantAIModel indexed_cols = { c.name for c in TenantAIModel.__table__.columns if c.index } assert "tenant_id" in indexed_cols def test_kb_tenant_id_indexed(self): from models import TenantKBDoc indexed_cols = { c.name for c in TenantKBDoc.__table__.columns if c.index } assert "tenant_id" in indexed_cols # ── WorkflowDefinition / WorkflowRun ORM 모델 ─────────────────────────────── class TestWorkflowORM: def test_import_workflow_models(self): from models import WorkflowDefinition, WorkflowRun assert WorkflowDefinition.__tablename__ == "tb_workflow_definition" assert WorkflowRun.__tablename__ == "tb_workflow_run" def test_workflow_definition_columns(self): from models import WorkflowDefinition cols = {c.name for c in WorkflowDefinition.__table__.columns} required = {"id", "name", "trigger", "steps", "active", "created_at"} assert required.issubset(cols), f"누락 컬럼: {required - cols}" def test_workflow_run_columns(self): from models import WorkflowRun cols = {c.name for c in WorkflowRun.__table__.columns} required = {"id", "definition_id", "trigger_data", "step_results", "status", "started_at", "finished_at"} assert required.issubset(cols), f"누락 컬럼: {required - cols}" def test_workflow_run_fk_to_definition(self): from models import WorkflowRun fk_cols = {fk.column.table.name for fk in WorkflowRun.__table__.foreign_keys} assert "tb_workflow_definition" in fk_cols def test_workflow_definition_relationship(self): from models import WorkflowDefinition # relationship 'runs' 존재 확인 assert hasattr(WorkflowDefinition, "runs") def test_workflow_run_relationship(self): from models import WorkflowRun # relationship 'definition' 존재 확인 assert hasattr(WorkflowRun, "definition") def test_workflow_active_default_false(self): from models import WorkflowDefinition # active 컬럼 기본값 False active_col = WorkflowDefinition.__table__.columns["active"] assert active_col.default.arg is False def test_workflow_run_status_default(self): from models import WorkflowRun status_col = WorkflowRun.__table__.columns["status"] assert status_col.default.arg == "running" # ── tenant_ai 라우터 Pydantic 스키마 ───────────────────────────────────────── class TestTenantAIPydantic: def test_train_request_valid(self): from routers.tenant_ai import TrainRequest req = TrainRequest(model_name="my-llama", base_model="llama3") assert req.model_name == "my-llama" assert req.base_model == "llama3" def test_train_request_defaults(self): from routers.tenant_ai import TrainRequest req = TrainRequest(model_name="model-x", base_model="llama3") assert req.description is None def test_query_request_valid(self): from routers.tenant_ai import QueryRequest req = QueryRequest(question="서버 재시작 절차는?") assert req.use_kb is True assert req.top_k == 3 def test_query_request_top_k_limit(self): from routers.tenant_ai import QueryRequest import pydantic with pytest.raises((ValueError, pydantic.ValidationError)): QueryRequest(question="질문", top_k=11) # max 10 def test_kb_doc_create_valid(self): from routers.tenant_ai import KBDocCreate doc = KBDocCreate(title="서버 운영 가이드", content="서버 운영 절차...") assert doc.title == "서버 운영 가이드" def test_query_request_min_length(self): from routers.tenant_ai import QueryRequest import pydantic with pytest.raises((ValueError, pydantic.ValidationError)): QueryRequest(question="") # min_length=1 # ── workflow_engine 라우터 Pydantic 스키마 ─────────────────────────────────── class TestWorkflowEnginePydantic: def test_workflow_create_valid(self): from routers.workflow_engine import WorkflowCreate, WorkflowStep req = WorkflowCreate( name="테스트 워크플로우", trigger={"event": "SR_CREATED"}, steps=[WorkflowStep(seq=1, type="notify", params={"channel": "messenger"})], ) assert req.name == "테스트 워크플로우" assert req.active is False assert len(req.steps) == 1 def test_workflow_update_partial(self): from routers.workflow_engine import WorkflowUpdate upd = WorkflowUpdate(active=True) assert upd.active is True assert upd.name is None assert upd.steps is None def test_trigger_request_valid(self): from routers.workflow_engine import TriggerRequest req = TriggerRequest(definition_id=1, payload={"server_id": "svr-01"}) assert req.definition_id == 1 assert req.payload["server_id"] == "svr-01" def test_workflow_create_requires_steps(self): from routers.workflow_engine import WorkflowCreate, WorkflowStep import pydantic with pytest.raises((ValueError, pydantic.ValidationError)): WorkflowCreate(name="빈 워크플로우", steps=[]) # min_length=1 def test_workflow_step_defaults(self): from routers.workflow_engine import WorkflowStep step = WorkflowStep(seq=1, type="notify") assert step.params == {} # ── 내장 템플릿 시드 데이터 검증 ──────────────────────────────────────────── class TestBuiltinTemplates: def test_template_count(self): from routers.workflow_engine import BUILTIN_TEMPLATES assert len(BUILTIN_TEMPLATES) == 5 def test_all_templates_have_required_fields(self): from routers.workflow_engine import BUILTIN_TEMPLATES for tpl in BUILTIN_TEMPLATES: assert "name" in tpl assert "description" in tpl assert "trigger" in tpl assert "steps" in tpl def test_template_names(self): from routers.workflow_engine import BUILTIN_TEMPLATES names = {tpl["name"] for tpl in BUILTIN_TEMPLATES} expected = { "SR 자동처리", "SLA 에스컬레이션", "SSL 인증서 갱신", "서버 이상 감지 → SR 생성", "정기 보고서 생성", } assert expected == names def test_all_templates_steps_are_list(self): from routers.workflow_engine import BUILTIN_TEMPLATES for tpl in BUILTIN_TEMPLATES: assert isinstance(tpl["steps"], list) assert len(tpl["steps"]) >= 1 def test_steps_json_serializable(self): from routers.workflow_engine import BUILTIN_TEMPLATES for tpl in BUILTIN_TEMPLATES: serialized = json.dumps(tpl["steps"]) parsed = json.loads(serialized) assert isinstance(parsed, list) def test_cron_templates_have_cron_expr(self): from routers.workflow_engine import BUILTIN_TEMPLATES cron_templates = [t for t in BUILTIN_TEMPLATES if t["trigger"].get("event") == "CRON"] for tpl in cron_templates: assert "cron_expr" in tpl["trigger"], f"{tpl['name']} CRON 트리거에 cron_expr 누락" # ── _get_tenant_id 헬퍼 ────────────────────────────────────────────────────── class TestTenantIdHelper: def test_returns_inst_code_when_present(self): from routers.tenant_ai import _get_tenant_id class FakeUser: inst_code = "INST001" username = "admin" assert _get_tenant_id(FakeUser()) == "INST001" def test_falls_back_to_username(self): from routers.tenant_ai import _get_tenant_id class FakeUser: inst_code = None username = "admin" assert _get_tenant_id(FakeUser()) == "admin" def test_empty_inst_code_falls_back(self): from routers.tenant_ai import _get_tenant_id class FakeUser: inst_code = "" username = "engineer1" # 빈 문자열은 falsy → username 사용 result = _get_tenant_id(FakeUser()) assert result == "engineer1" # ── _execute_step 단위 테스트 ──────────────────────────────────────────────── class TestExecuteStep: @pytest.mark.asyncio async def test_notify_step(self): from routers.workflow_engine import _execute_step step = {"type": "notify", "params": {"channel": "messenger", "message": "테스트 알림"}} result = await _execute_step(step, {}, None) assert result["type"] == "notify" assert result["result"] == "ok" assert result["channel"] == "messenger" @pytest.mark.asyncio async def test_auto_assign_step(self): from routers.workflow_engine import _execute_step step = {"type": "auto_assign", "params": {"role": "ENGINEER"}} result = await _execute_step(step, {}, None) assert result["result"] == "ok" assert "ENGINEER" in result["detail"] @pytest.mark.asyncio async def test_escalate_step(self): from routers.workflow_engine import _execute_step step = {"type": "escalate", "params": {"target_role": "PM"}} result = await _execute_step(step, {}, None) assert result["result"] == "ok" assert result["target"] == "PM" @pytest.mark.asyncio async def test_unknown_step_skipped(self): from routers.workflow_engine import _execute_step step = {"type": "unknown_action", "params": {}} result = await _execute_step(step, {}, None) assert result["result"] == "skipped" @pytest.mark.asyncio async def test_create_sr_step_formats_title(self): from routers.workflow_engine import _execute_step step = {"type": "create_sr", "params": {"title": "이상 감지: {server_id}", "priority": "CRITICAL"}} result = await _execute_step(step, {"server_id": "svr-99"}, None) assert result["result"] == "ok" assert "svr-99" in result["title"] @pytest.mark.asyncio async def test_generate_report_step(self): from routers.workflow_engine import _execute_step step = {"type": "generate_report", "params": {"type": "monthly", "format": "pdf"}} result = await _execute_step(step, {}, None) assert result["result"] == "ok" assert result["report_type"] == "monthly"