Files
awoooi/apps/api/tests/integration/test_knowledge_repository.py
OG T 5e836bde24
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 7m18s
test(integration): 新增真實 DB 整合測試 — knowledge_repository + API E2E (2026-04-04 ogt)
- tests/integration/conftest.py: 連接 awoooi_dev PostgreSQL,每個測試後 rollback
- tests/integration/test_knowledge_repository.py: 23 個真實 DB 測試
  - create/get_by_id/list/update/delete(軟刪除)/search/categories/view_count
- tests/integration/test_incident_api.py: 7 個 HTTPS 端點測試
  - health check + knowledge API smoke test
- 遵循禁止 Mock 鐵律 (feedback_no_mock_testing.md)
- 本地驗證: 30/30 PASSED

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 02:35:38 +08:00

305 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Knowledge Repository 整合測試
==============================
使用真實 awoooi_dev PostgreSQL禁止 Mock
建立時間: 2026-04-04 (台北時區)
建立者: Claude Code (整合測試 Phase 1)
測試覆蓋:
- create: 建立知識條目
- get_by_id: 根據 ID 查詢
- list_entries: 列表 + 分類/關鍵字篩選
- update: 更新欄位
- delete: 軟刪除 (status → archived)
- search: 關鍵字搜尋
- get_categories: 分類統計
- increment_view_count: 瀏覽數遞增
"""
import pytest
from src.models.knowledge import EntrySource, EntryStatus, EntryType, KnowledgeEntryCreate
from src.repositories.knowledge_repository import KnowledgeDBRepository
# =============================================================================
# Helpers
# =============================================================================
def make_entry(**kwargs) -> KnowledgeEntryCreate:
defaults = {
"title": "測試 Runbook",
"content": "## 測試內容\n這是整合測試建立的條目。",
"entry_type": EntryType.RUNBOOK,
"category": "infrastructure",
"tags": ["test", "integration"],
"source": EntrySource.HUMAN,
}
defaults.update(kwargs)
return KnowledgeEntryCreate(**defaults)
# =============================================================================
# Tests
# =============================================================================
class TestKnowledgeRepositoryCreate:
@pytest.mark.asyncio
async def test_create_returns_entry_with_id(self, db_session):
repo = KnowledgeDBRepository(db_session)
entry = await repo.create(make_entry())
assert entry.id is not None
assert entry.title == "測試 Runbook"
assert entry.status == EntryStatus.DRAFT
assert entry.view_count == 0
assert entry.source == EntrySource.HUMAN
@pytest.mark.asyncio
async def test_create_stores_tags(self, db_session):
repo = KnowledgeDBRepository(db_session)
entry = await repo.create(make_entry(tags=["k8s", "alert", "infra"]))
assert set(entry.tags) == {"k8s", "alert", "infra"}
@pytest.mark.asyncio
async def test_create_optional_fields_none(self, db_session):
repo = KnowledgeDBRepository(db_session)
entry = await repo.create(make_entry(related_incident_id=None, created_by=None))
assert entry.related_incident_id is None
assert entry.created_by is None
class TestKnowledgeRepositoryGetById:
@pytest.mark.asyncio
async def test_get_by_id_returns_entry(self, db_session):
repo = KnowledgeDBRepository(db_session)
created = await repo.create(make_entry(title="可查詢條目"))
fetched = await repo.get_by_id(created.id)
assert fetched is not None
assert fetched.id == created.id
assert fetched.title == "可查詢條目"
@pytest.mark.asyncio
async def test_get_by_id_nonexistent_returns_none(self, db_session):
repo = KnowledgeDBRepository(db_session)
result = await repo.get_by_id("00000000-0000-0000-0000-000000000000")
assert result is None
@pytest.mark.asyncio
async def test_get_by_id_archived_returns_none(self, db_session):
repo = KnowledgeDBRepository(db_session)
created = await repo.create(make_entry(title="即將封存"))
await repo.delete(created.id)
result = await repo.get_by_id(created.id)
assert result is None
class TestKnowledgeRepositoryList:
@pytest.mark.asyncio
async def test_list_returns_entries(self, db_session):
repo = KnowledgeDBRepository(db_session)
await repo.create(make_entry(title="條目 A", category="infrastructure"))
await repo.create(make_entry(title="條目 B", category="infrastructure"))
entries, total = await repo.list_entries()
titles = [e.title for e in entries]
assert "條目 A" in titles
assert "條目 B" in titles
assert total >= 2
@pytest.mark.asyncio
async def test_list_filter_by_category(self, db_session):
repo = KnowledgeDBRepository(db_session)
await repo.create(make_entry(title="基礎設施條目", category="infrastructure"))
await repo.create(make_entry(title="安全條目", category="security"))
entries, total = await repo.list_entries(category="security")
assert all(e.category == "security" for e in entries)
assert any(e.title == "安全條目" for e in entries)
@pytest.mark.asyncio
async def test_list_filter_by_entry_type(self, db_session):
repo = KnowledgeDBRepository(db_session)
await repo.create(make_entry(title="Runbook 條目", entry_type=EntryType.RUNBOOK))
await repo.create(make_entry(title="最佳實踐條目", entry_type=EntryType.BEST_PRACTICE))
entries, _ = await repo.list_entries(entry_type=EntryType.BEST_PRACTICE)
assert all(e.entry_type == EntryType.BEST_PRACTICE for e in entries)
assert any(e.title == "最佳實踐條目" for e in entries)
@pytest.mark.asyncio
async def test_list_keyword_search(self, db_session):
repo = KnowledgeDBRepository(db_session)
await repo.create(make_entry(title="Kubernetes 告警處理", content="K8s pod crash 處理步驟"))
await repo.create(make_entry(title="資料庫備份", content="PostgreSQL 備份策略"))
entries, total = await repo.list_entries(q="Kubernetes")
assert total >= 1
assert any("Kubernetes" in e.title for e in entries)
@pytest.mark.asyncio
async def test_list_excludes_archived(self, db_session):
repo = KnowledgeDBRepository(db_session)
created = await repo.create(make_entry(title="封存後不顯示"))
await repo.delete(created.id)
entries, _ = await repo.list_entries()
assert all(e.id != created.id for e in entries)
@pytest.mark.asyncio
async def test_list_limit(self, db_session):
repo = KnowledgeDBRepository(db_session)
for i in range(5):
await repo.create(make_entry(title=f"分頁測試 {i}"))
entries, _ = await repo.list_entries(limit=3)
assert len(entries) <= 3
class TestKnowledgeRepositoryUpdate:
@pytest.mark.asyncio
async def test_update_title(self, db_session):
repo = KnowledgeDBRepository(db_session)
created = await repo.create(make_entry(title="原始標題"))
updated = await repo.update(created.id, {"title": "更新後標題"})
assert updated is not None
assert updated.title == "更新後標題"
@pytest.mark.asyncio
async def test_update_status_to_approved(self, db_session):
repo = KnowledgeDBRepository(db_session)
created = await repo.create(make_entry())
updated = await repo.update(created.id, {"status": EntryStatus.APPROVED})
assert updated is not None
assert updated.status == EntryStatus.APPROVED
@pytest.mark.asyncio
async def test_update_nonexistent_returns_none(self, db_session):
repo = KnowledgeDBRepository(db_session)
result = await repo.update("00000000-0000-0000-0000-000000000000", {"title": "X"})
assert result is None
class TestKnowledgeRepositoryDelete:
@pytest.mark.asyncio
async def test_delete_soft_deletes(self, db_session):
repo = KnowledgeDBRepository(db_session)
created = await repo.create(make_entry())
success = await repo.delete(created.id)
assert success is True
# 軟刪除後 get_by_id 應返回 None
assert await repo.get_by_id(created.id) is None
@pytest.mark.asyncio
async def test_delete_nonexistent_returns_false(self, db_session):
repo = KnowledgeDBRepository(db_session)
result = await repo.delete("00000000-0000-0000-0000-000000000000")
assert result is False
class TestKnowledgeRepositorySearch:
@pytest.mark.asyncio
async def test_search_by_title(self, db_session):
repo = KnowledgeDBRepository(db_session)
await repo.create(make_entry(title="Redis 連線問題排查"))
await repo.create(make_entry(title="PostgreSQL 效能調優"))
results = await repo.search("Redis")
assert any("Redis" in e.title for e in results)
assert all("Redis" in e.title or "Redis" in e.content for e in results)
@pytest.mark.asyncio
async def test_search_by_content(self, db_session):
repo = KnowledgeDBRepository(db_session)
await repo.create(make_entry(
title="告警處理",
content="當 Prometheus 發出 AlertFiring 時的處理步驟"
))
results = await repo.search("AlertFiring")
assert len(results) >= 1
assert any("AlertFiring" in e.content for e in results)
@pytest.mark.asyncio
async def test_search_no_results(self, db_session):
repo = KnowledgeDBRepository(db_session)
results = await repo.search("XYZABCNOTEXIST12345")
assert results == []
class TestKnowledgeRepositoryCategories:
@pytest.mark.asyncio
async def test_get_categories_returns_counts(self, db_session):
repo = KnowledgeDBRepository(db_session)
await repo.create(make_entry(category="infrastructure"))
await repo.create(make_entry(category="infrastructure"))
await repo.create(make_entry(category="security"))
categories = await repo.get_categories()
cat_dict = dict(categories)
assert "infrastructure" in cat_dict
assert "security" in cat_dict
assert cat_dict["infrastructure"] >= 2
assert cat_dict["security"] >= 1
class TestKnowledgeRepositoryViewCount:
@pytest.mark.asyncio
async def test_increment_view_count(self, db_session):
repo = KnowledgeDBRepository(db_session)
created = await repo.create(make_entry())
assert created.view_count == 0
await repo.increment_view_count(created.id)
await repo.increment_view_count(created.id)
await db_session.flush()
fetched = await repo.get_by_id(created.id)
assert fetched is not None
assert fetched.view_count == 2
@pytest.mark.asyncio
async def test_increment_view_count_nonexistent_returns_false(self, db_session):
repo = KnowledgeDBRepository(db_session)
result = await repo.increment_view_count("00000000-0000-0000-0000-000000000000")
assert result is False