"""Integration tests for KB Accelerator endpoints.""" import pytest import json from unittest.mock import AsyncMock, patch, PropertyMock from httpx import AsyncClient pytestmark = pytest.mark.asyncio # ── Fixtures ── @pytest.fixture async def kb_setup(client, auth_headers, test_db): """Seed KB plan limits and return helpers.""" # KB tests were authored against a free-plan user. Phase 1 conftest seeds # the test_user with a pro/active Subscription; downgrade to free here so # quota numbers match the original test intent. from app.models.subscription import Subscription sub = (await test_db.execute(__import__("sqlalchemy").select(Subscription))).scalar_one() sub.plan = "free" await test_db.commit() # Update plan_limits with KB columns for 'free' plan await test_db.execute( __import__("sqlalchemy").text(""" UPDATE plan_limits SET kb_accelerator_enabled = true, kb_max_lifetime_conversions = 3, kb_allowed_formats = '["txt","paste"]'::jsonb, kb_detailed_analysis = false, kb_conversational_refinement = false, kb_step_library_matching = false, kb_history_limit = 3 WHERE plan = 'free' """) ) await test_db.execute( __import__("sqlalchemy").text(""" UPDATE plan_limits SET kb_accelerator_enabled = true, kb_max_lifetime_conversions = NULL, kb_allowed_formats = '["txt","paste","docx","pdf","html","md"]'::jsonb, kb_detailed_analysis = true, kb_conversational_refinement = true, kb_step_library_matching = true, kb_history_limit = NULL WHERE plan = 'pro' """) ) await test_db.commit() return {"client": client, "headers": auth_headers} def _mock_ai_enabled(): """Context manager to mock AI as enabled.""" return patch.object( type(__import__("app.core.config", fromlist=["settings"]).settings), "ai_enabled", new_callable=PropertyMock, return_value=True, ) SAMPLE_KB_TEXT = """ Troubleshooting Outlook Connectivity Issues Problem: Users report that Outlook keeps disconnecting from Exchange. Step 1: Check Network Connectivity Ping the Exchange server to verify network connectivity. If ping fails, check the network configuration. Step 2: Verify Outlook Profile If the network is working, check the Outlook profile settings. Go to Control Panel > Mail > Show Profiles. Step 3: Check Exchange Server If the profile is correct, verify the Exchange server is running. Open Services.msc and check Microsoft Exchange services. Resolution: After following these steps, Outlook should maintain a persistent connection to Exchange. """ MOCK_AI_TROUBLESHOOTING_RESPONSE = json.dumps({ "title": "Troubleshooting Outlook Connectivity", "description": "Diagnose and fix Outlook disconnection from Exchange", "nodes": [ { "id": "root-check", "type": "question", "question": "Is the network connection working?", "options": [ {"label": "Yes", "next_node_id": "check-profile"}, {"label": "No", "next_node_id": "fix-network"}, ], "confidence": 0.92, "source_excerpt": "Step 1: Check Network Connectivity", }, { "id": "fix-network", "type": "resolution", "question": "Fix the network configuration and retry.", "confidence": 0.85, "source_excerpt": "If ping fails, check the network configuration.", }, { "id": "check-profile", "type": "question", "question": "Is the Outlook profile configured correctly?", "options": [ {"label": "Yes", "next_node_id": "check-exchange"}, {"label": "No", "next_node_id": "fix-profile"}, ], "confidence": 0.88, "source_excerpt": "Step 2: Verify Outlook Profile", }, { "id": "fix-profile", "type": "resolution", "question": "Reconfigure the Outlook profile via Control Panel > Mail.", "confidence": 0.90, "source_excerpt": "Go to Control Panel > Mail > Show Profiles.", }, { "id": "check-exchange", "type": "resolution", "question": "Verify Exchange services are running in Services.msc.", "confidence": 0.87, "source_excerpt": "Open Services.msc and check Microsoft Exchange services.", }, ], }) MOCK_AI_PROCEDURAL_RESPONSE = json.dumps({ "title": "Setup New Domain Controller", "description": "Step-by-step procedure for setting up a new DC", "steps": [ { "id": "step-1", "type": "step", "content": "Open Server Manager on [VAR:server_name]", "confidence": 0.95, "source_excerpt": "Step 1: Open Server Manager on DC01", }, { "id": "warning-dns", "type": "warning", "content": "WARNING: This will restart DNS and cause brief connectivity loss", "confidence": 0.90, "source_excerpt": "Note: Restarting DNS will cause a brief outage", }, { "id": "step-2", "type": "step", "content": "Configure IP address [VAR:ip_address] on the network adapter", "confidence": 0.88, "source_excerpt": "Configure IP 192.168.1.10 on the adapter", }, ], "intake_form": [ { "variable_name": "server_name", "label": "Server Name", "field_type": "text", "required": True, "display_order": 1, }, { "variable_name": "ip_address", "label": "IP Address", "field_type": "text", "required": True, "display_order": 2, }, ], }) # ── Upload Tests ── class TestUpload: async def test_upload_text_paste(self, kb_setup): """Upload via text paste creates a kb_import in processing status.""" c, h = kb_setup["client"], kb_setup["headers"] with _mock_ai_enabled(): # Mock the background conversion (don't actually call AI) with patch("app.api.endpoints.kb_accelerator._run_conversion"): resp = await c.post( "/api/v1/kb-accelerator/upload", data={"content": SAMPLE_KB_TEXT, "target_type": "troubleshooting"}, headers=h, ) assert resp.status_code == 201 data = resp.json() assert data["status"] == "processing" assert data["source_format"] == "paste" assert "id" in data async def test_upload_empty_content_rejected(self, kb_setup): c, h = kb_setup["client"], kb_setup["headers"] with _mock_ai_enabled(): resp = await c.post( "/api/v1/kb-accelerator/upload", data={"content": "short"}, headers=h, ) assert resp.status_code == 400 async def test_upload_no_file_no_content_rejected(self, kb_setup): c, h = kb_setup["client"], kb_setup["headers"] with _mock_ai_enabled(): resp = await c.post( "/api/v1/kb-accelerator/upload", data={}, headers=h, ) assert resp.status_code == 400 # ── Get/List Tests ── class TestGetList: async def test_get_import(self, kb_setup): c, h = kb_setup["client"], kb_setup["headers"] with _mock_ai_enabled(), patch("app.api.endpoints.kb_accelerator._run_conversion"): create_resp = await c.post( "/api/v1/kb-accelerator/upload", data={"content": SAMPLE_KB_TEXT, "target_type": "troubleshooting"}, headers=h, ) import_id = create_resp.json()["id"] resp = await c.get(f"/api/v1/kb-accelerator/{import_id}", headers=h) assert resp.status_code == 200 data = resp.json() assert data["id"] == import_id assert data["source_format"] == "paste" async def test_list_imports(self, kb_setup): c, h = kb_setup["client"], kb_setup["headers"] with _mock_ai_enabled(), patch("app.api.endpoints.kb_accelerator._run_conversion"): await c.post( "/api/v1/kb-accelerator/upload", data={"content": SAMPLE_KB_TEXT, "target_type": "troubleshooting"}, headers=h, ) resp = await c.get("/api/v1/kb-accelerator", headers=h) assert resp.status_code == 200 data = resp.json() assert data["total"] >= 1 assert len(data["items"]) >= 1 # ── Quota Tests ── class TestQuota: async def test_get_quota(self, kb_setup): c, h = kb_setup["client"], kb_setup["headers"] resp = await c.get("/api/v1/kb-accelerator/quota", headers=h) assert resp.status_code == 200 data = resp.json() assert data["kb_accelerator_enabled"] is True assert data["lifetime_conversions_limit"] == 3 assert data["can_convert"] is True # ── Commit Tests ── class TestCommit: async def test_commit_creates_tree(self, kb_setup, test_db): """Committing a ready import creates a Tree record.""" c, h = kb_setup["client"], kb_setup["headers"] # Create import with _mock_ai_enabled(), patch("app.api.endpoints.kb_accelerator._run_conversion"): create_resp = await c.post( "/api/v1/kb-accelerator/upload", data={"content": SAMPLE_KB_TEXT, "target_type": "troubleshooting"}, headers=h, ) import_id = create_resp.json()["id"] # Simulate conversion complete: update status + add nodes directly from app.models.kb_import import KBImport, KBImportNode from sqlalchemy import select import uuid result = await test_db.execute(select(KBImport).where(KBImport.id == uuid.UUID(import_id))) kb_import = result.scalar_one() kb_import.status = "ready" kb_import.source_metadata = {"_conversion": {"title": "Test Flow", "description": "Test"}} # Build a valid tree: root decision with 2 branches leading to solutions nodes_data = [ KBImportNode( kb_import_id=kb_import.id, node_order=0, node_type="question", content={ "original_id": "root", "question": "What is the issue?", "options": [ {"id": "opt-root-0", "label": "Option A", "next_node_id": "action-a"}, {"id": "opt-root-1", "label": "Option B", "next_node_id": "action-b"}, ], }, confidence_score=0.9, ), KBImportNode( kb_import_id=kb_import.id, node_order=1, node_type="action", content={"original_id": "action-a", "question": "Try fix A", "description": "Do thing A", "next_node_id": "solution-a"}, confidence_score=0.9, ), KBImportNode( kb_import_id=kb_import.id, node_order=2, node_type="action", content={"original_id": "action-b", "question": "Try fix B", "description": "Do thing B", "next_node_id": "solution-b"}, confidence_score=0.9, ), KBImportNode( kb_import_id=kb_import.id, node_order=3, node_type="resolution", content={"original_id": "solution-a", "question": "Resolved via A", "description": "Issue fixed by A"}, confidence_score=0.9, ), KBImportNode( kb_import_id=kb_import.id, node_order=4, node_type="resolution", content={"original_id": "solution-b", "question": "Resolved via B", "description": "Issue fixed by B"}, confidence_score=0.9, ), ] for n in nodes_data: test_db.add(n) await test_db.commit() # Commit resp = await c.post(f"/api/v1/kb-accelerator/{import_id}/commit", headers=h) assert resp.status_code == 200 data = resp.json() assert "tree_id" in data assert data["tree_type"] == "troubleshooting" # ── Delete Tests ── class TestDelete: async def test_delete_import(self, kb_setup): c, h = kb_setup["client"], kb_setup["headers"] with _mock_ai_enabled(), patch("app.api.endpoints.kb_accelerator._run_conversion"): create_resp = await c.post( "/api/v1/kb-accelerator/upload", data={"content": SAMPLE_KB_TEXT, "target_type": "troubleshooting"}, headers=h, ) import_id = create_resp.json()["id"] resp = await c.delete(f"/api/v1/kb-accelerator/{import_id}", headers=h) assert resp.status_code == 204 # Verify deleted resp = await c.get(f"/api/v1/kb-accelerator/{import_id}", headers=h) assert resp.status_code == 404