""" Integration Test Suite for Agentic CitationEdge Pipeline Tests: Neo4j connectivity, data seeding, graph queries, MongoDB, health endpoints, and full pipeline integration. Usage: python tests/test_integration.py """ import asyncio import os import sys import time import json # Add project root to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv load_dotenv() class IntegrationTestRunner: """Runs all integration tests and reports results.""" def __init__(self): self.results = [] self.passed = 0 self.failed = 0 self.skipped = 0 def log(self, msg, level="INFO"): timestamp = time.strftime("%H:%M:%S") symbols = {"INFO": "ℹ️", "PASS": "✅", "FAIL": "❌", "WARN": "⚠️", "SKIP": "⏭️"} print(f" {symbols.get(level, 'ℹ️')} [{timestamp}] {msg}") def record(self, test_name, passed, detail=""): status = "PASS" if passed else "FAIL" self.results.append({"test": test_name, "status": status, "detail": detail}) if passed: self.passed += 1 self.log(f"{test_name}: PASSED {detail}", "PASS") else: self.failed += 1 self.log(f"{test_name}: FAILED — {detail}", "FAIL") def skip(self, test_name, reason=""): self.skipped += 1 self.results.append({"test": test_name, "status": "SKIP", "detail": reason}) self.log(f"{test_name}: SKIPPED — {reason}", "SKIP") def print_report(self): print("\n" + "=" * 70) print(" INTEGRATION TEST REPORT") print("=" * 70) for r in self.results: icon = {"PASS": "✅", "FAIL": "❌", "SKIP": "⏭️"}[r["status"]] print(f" {icon} {r['test']}: {r['status']} {r['detail']}") print("-" * 70) print(f" Total: {len(self.results)} | Passed: {self.passed} | Failed: {self.failed} | Skipped: {self.skipped}") print("=" * 70) return self.failed == 0 def run_tests(): runner = IntegrationTestRunner() print("\n" + "=" * 70) print(" 🧪 AGENTIC CITATIONEDGE — INTEGRATION TEST SUITE") print("=" * 70) # ═══════════════════════════════════════════════════════════════════ # TEST 1: Environment Variables # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 1: Environment Variables ───") neo4j_uri = os.getenv("NEO4J_URI", "") neo4j_user = os.getenv("NEO4J_USERNAME", "") or os.getenv("NEO4J_USER", "") neo4j_pass = os.getenv("NEO4J_PASSWORD", "") neo4j_db = os.getenv("NEO4J_DATABASE", "neo4j") runner.record("ENV: NEO4J_URI set", bool(neo4j_uri), f"value={neo4j_uri[:30]}...") runner.record("ENV: NEO4J_USERNAME set", bool(neo4j_user), f"user={neo4j_user}") runner.record("ENV: NEO4J_PASSWORD set", bool(neo4j_pass), "***" if neo4j_pass else "empty") runner.record("ENV: NEO4J_DATABASE set", bool(neo4j_db), f"db={neo4j_db}") # ═══════════════════════════════════════════════════════════════════ # TEST 2: Neo4j Driver Import # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 2: Neo4j Driver ───") try: import neo4j runner.record("neo4j package import", True, f"version={neo4j.__version__}") except ImportError as e: runner.record("neo4j package import", False, str(e)) runner.log("Install neo4j: pip install neo4j", "WARN") # ═══════════════════════════════════════════════════════════════════ # TEST 3: Neo4j Connectivity # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 3: Neo4j Connectivity ───") neo4j_connected = False neo4j_svc = None try: from services.neo4j_service import Neo4jService neo4j_svc = Neo4jService( uri=neo4j_uri, username=neo4j_user, password=neo4j_pass, database=neo4j_db, ) driver = neo4j_svc.connect() runner.record("Neo4j connection", True, f"driver={type(driver).__name__}") neo4j_connected = True except Exception as e: runner.record("Neo4j connection", False, str(e)) # ═══════════════════════════════════════════════════════════════════ # TEST 4: Neo4j Basic Queries # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 4: Neo4j Queries ───") if neo4j_connected and neo4j_svc: # Ping try: result = neo4j_svc.run("RETURN 1 AS ping") runner.record("Neo4j RETURN 1 query", result[0]["ping"] == 1, f"result={result}") except Exception as e: runner.record("Neo4j RETURN 1 query", False, str(e)) # Server info try: result = neo4j_svc.run("CALL dbms.components() YIELD name, versions RETURN name, versions") if result: runner.record("Neo4j server info", True, f"name={result[0].get('name')}, versions={result[0].get('versions')}") else: runner.record("Neo4j server info", True, "no dbms.components response (AuraDB)") except Exception as e: runner.record("Neo4j server info", True, f"query not supported (expected for AuraDB): {str(e)[:50]}") # Labels try: result = neo4j_svc.run("CALL db.labels() YIELD label RETURN collect(label) AS labels") labels = result[0]["labels"] if result else [] runner.record("Neo4j db.labels()", True, f"labels={labels}") except Exception as e: runner.record("Neo4j db.labels()", False, str(e)) # Node count try: result = neo4j_svc.run("MATCH (n) RETURN count(n) AS cnt") cnt = result[0]["cnt"] if result else 0 runner.record("Neo4j node count", True, f"count={cnt}") except Exception as e: runner.record("Neo4j node count", False, str(e)) else: runner.skip("Neo4j queries", "No connection") # ═══════════════════════════════════════════════════════════════════ # TEST 5: Neo4j Index Creation # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 5: Neo4j Index Creation ───") if neo4j_connected and neo4j_svc: try: neo4j_svc.create_indexes() runner.record("Neo4j index creation", True, "All indexes ensured") except Exception as e: runner.record("Neo4j index creation", False, str(e)) else: runner.skip("Neo4j index creation", "No connection") # ═══════════════════════════════════════════════════════════════════ # TEST 6: Neo4j Graph Stats # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 6: Neo4j Graph Stats ───") if neo4j_connected and neo4j_svc: try: stats = neo4j_svc.get_graph_stats() runner.record("Neo4j graph stats", "error" not in stats, f"nodes={stats.get('node_count')}, rels={stats.get('relationship_count')}, labels={stats.get('labels')}") except Exception as e: runner.record("Neo4j graph stats", False, str(e)) else: runner.skip("Neo4j graph stats", "No connection") # ═══════════════════════════════════════════════════════════════════ # TEST 7: Neo4j Seed Sample Data # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 7: Neo4j Seed Sample Data ───") test_doc_id = None test_job_id = None if neo4j_connected and neo4j_svc: try: from utils.helpers import generate_id test_job_id = generate_id("test_job") test_doc_id = generate_id("test_doc") result = neo4j_svc.seed_sample_data(doc_id=test_doc_id, job_id=test_job_id) runner.record("Neo4j seed sample data", "error" not in result, f"nodes={result.get('nodes')}, rels={result.get('relationships')}") except Exception as e: runner.record("Neo4j seed sample data", False, str(e)) else: runner.skip("Neo4j seed sample data", "No connection") # ═══════════════════════════════════════════════════════════════════ # TEST 8: Neo4j Query Seeded Data # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 8: Query Seeded Data ───") if neo4j_connected and neo4j_svc and test_doc_id: # Document node exists try: result = neo4j_svc.run("MATCH (d:Document {doc_id: $doc_id}) RETURN d", doc_id=test_doc_id) runner.record("Query: Document node exists", len(result) == 1, f"found={len(result)} node(s)") except Exception as e: runner.record("Query: Document node exists", False, str(e)) # Keywords linked try: result = neo4j_svc.run( "MATCH (d:Document {doc_id: $doc_id})-[:HAS_KEYWORD]->(k:Keyword) RETURN k.term AS term, k.score AS score", doc_id=test_doc_id ) runner.record("Query: Keywords linked", len(result) > 0, f"found={len(result)} keywords: {[r['term'] for r in result[:3]]}...") except Exception as e: runner.record("Query: Keywords linked", False, str(e)) # Claims linked try: result = neo4j_svc.run( "MATCH (d:Document {doc_id: $doc_id})-[:HAS_CLAIM]->(c:Claim) RETURN c.text AS text, c.confidence AS confidence", doc_id=test_doc_id ) runner.record("Query: Claims linked", len(result) > 0, f"found={len(result)} claims") except Exception as e: runner.record("Query: Claims linked", False, str(e)) # Citation gaps try: result = neo4j_svc.run( "MATCH (d:Document {doc_id: $doc_id})-[:HAS_GAP]->(g:CitationGap) RETURN g.description AS desc, g.severity AS severity", doc_id=test_doc_id ) runner.record("Query: Citation gaps linked", len(result) > 0, f"found={len(result)} gaps") except Exception as e: runner.record("Query: Citation gaps linked", False, str(e)) # Evidence linked to claims try: result = neo4j_svc.run( "MATCH (c:Claim)-[:SUPPORTED_BY]->(e:Evidence) WHERE c.claim_id STARTS WITH 'claim_' + $doc_id RETURN c.text AS claim, e.text AS evidence", doc_id=test_doc_id ) runner.record("Query: Claim→Evidence links", len(result) > 0, f"found={len(result)} links") except Exception as e: runner.record("Query: Claim→Evidence links", False, str(e)) # Scores linked try: result = neo4j_svc.run( "MATCH (d:Document {doc_id: $doc_id})-[:HAS_SCORE]->(sc:Score) RETURN sc", doc_id=test_doc_id ) if result: scores = dict(result[0]["sc"]) runner.record("Query: Scores linked", "overall_score" in scores, f"overall={scores.get('overall_score')}, novelty={scores.get('novelty')}") else: runner.record("Query: Scores linked", False, "No score nodes found") except Exception as e: runner.record("Query: Scores linked", False, str(e)) # Full subgraph try: result = neo4j_svc.run( """ MATCH (d:Document {doc_id: $doc_id})-[r]->(n) RETURN type(r) AS rel, labels(n)[0] AS label, count(*) AS cnt """, doc_id=test_doc_id ) summary = {r["rel"]: r["cnt"] for r in result} runner.record("Query: Full subgraph", len(result) > 0, f"relationships: {summary}") except Exception as e: runner.record("Query: Full subgraph", False, str(e)) else: runner.skip("Query seeded data", "No connection or no test data") # ═══════════════════════════════════════════════════════════════════ # TEST 9: MongoDB Connectivity # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 9: MongoDB ───") mongo_connected = False try: from services.mongo_service import MongoService mongo_svc = MongoService() async def test_mongo(): nonlocal mongo_connected db = mongo_svc._get_db() await db.command("ping") mongo_connected = True return True try: asyncio.get_event_loop().run_until_complete(test_mongo()) runner.record("MongoDB connection", True, f"db={mongo_svc.db_name}") except RuntimeError: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(test_mongo()) runner.record("MongoDB connection", True, f"db={mongo_svc.db_name}") except Exception as e: runner.record("MongoDB connection", False, str(e)) runner.log("Make sure MongoDB is running: mongod or docker-compose up -d", "WARN") # ═══════════════════════════════════════════════════════════════════ # TEST 10: MongoDB Job Operations # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 10: MongoDB Job Operations ───") if mongo_connected and test_job_id: async def test_mongo_ops(): # Upsert job await mongo_svc.upsert_job(test_job_id, { "job_id": test_job_id, "doc_id": test_doc_id, "status": "completed", "filename": "test_paper.pdf", "duration_s": 42.5, "scores": {"overall_score": 7.8, "novelty": 8.2}, }) # Get job job = await mongo_svc.get_job(test_job_id) return job try: loop = asyncio.get_event_loop() if loop.is_closed(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) job = loop.run_until_complete(test_mongo_ops()) runner.record("MongoDB upsert + get job", job is not None and job.get("status") == "completed", f"job_id={job.get('job_id')}, status={job.get('status')}") except Exception as e: runner.record("MongoDB upsert + get job", False, str(e)) # List jobs try: jobs = loop.run_until_complete(mongo_svc.list_jobs(limit=5)) runner.record("MongoDB list jobs", len(jobs) >= 1, f"found={len(jobs)} jobs") except Exception as e: runner.record("MongoDB list jobs", False, str(e)) # Save result try: async def save_and_get_result(): await mongo_svc.save_result(test_job_id, { "job_id": test_job_id, "doc_id": test_doc_id, "status": "completed", "keywords": ["deep learning", "NLP", "transformer"], "claims": [{"text": "Test claim", "confidence": 0.9}], "scores": {"overall_score": 7.8}, }) return await mongo_svc.get_result(test_job_id) result = loop.run_until_complete(save_and_get_result()) runner.record("MongoDB save + get result", result is not None, f"keywords={len(result.get('keywords', []))}, claims={len(result.get('claims', []))}") except Exception as e: runner.record("MongoDB save + get result", False, str(e)) else: runner.skip("MongoDB job operations", "No MongoDB connection or no test data") # ═══════════════════════════════════════════════════════════════════ # TEST 11: Backend Config # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 11: Backend Config ───") try: from backend.config import get_settings settings = get_settings() runner.record("Settings load", True, f"env={settings.env}, neo4j_uri={settings.neo4j_uri[:30]}...") runner.record("Settings neo4j_database", hasattr(settings, 'neo4j_database'), f"database={getattr(settings, 'neo4j_database', 'N/A')}") except Exception as e: runner.record("Settings load", False, str(e)) # ═══════════════════════════════════════════════════════════════════ # TEST 12: HTTP Endpoints (if server is running) # ═══════════════════════════════════════════════════════════════════ print("\n─── Test Group 12: HTTP Endpoint Smoke Test ───") try: import httpx client = httpx.Client(base_url="http://localhost:8000", timeout=5.0) # Health try: r = client.get("/health") runner.record("HTTP GET /health", r.status_code == 200, f"status={r.status_code}, body={r.json()}") except Exception as e: runner.record("HTTP GET /health", False, f"Server not running? {e}") # Health ready try: r = client.get("/health/ready") body = r.json() runner.record("HTTP GET /health/ready", r.status_code == 200, f"status={body.get('status')}, checks={body.get('checks')}") except Exception as e: runner.record("HTTP GET /health/ready", False, str(e)) # Graph stats try: r = client.get("/graph/stats") body = r.json() runner.record("HTTP GET /graph/stats", r.status_code == 200, f"connected={body.get('connected')}, nodes={body.get('node_count')}") except Exception as e: runner.record("HTTP GET /graph/stats", False, str(e)) # Jobs list try: r = client.get("/api/jobs") runner.record("HTTP GET /jobs", r.status_code == 200, f"jobs_count={r.json().get('total', 0)}") except Exception as e: runner.record("HTTP GET /jobs", False, str(e)) # Seed via HTTP try: r = client.post("/graph/seed", json={}) body = r.json() runner.record("HTTP POST /graph/seed", r.status_code == 200 and body.get("success"), f"job_id={body.get('job_id')}, doc_id={body.get('doc_id')}") # Try loading the graph if body.get("doc_id"): r2 = client.get(f"/graph/{body['doc_id']}") body2 = r2.json() runner.record("HTTP GET /graph/{doc_id}", r2.status_code == 200, f"nodes={body2.get('node_count')}, links={body2.get('link_count')}") except Exception as e: runner.record("HTTP POST /graph/seed", False, str(e)) client.close() except ImportError: runner.skip("HTTP endpoints", "httpx not installed") except Exception as e: runner.skip("HTTP endpoints", f"Could not connect: {e}") # ═══════════════════════════════════════════════════════════════════ # CLEANUP # ═══════════════════════════════════════════════════════════════════ print("\n─── Cleanup ───") if neo4j_connected and neo4j_svc and test_doc_id: try: neo4j_svc.run_write( "MATCH (d:Document {doc_id: $doc_id})-[r]->(n) DETACH DELETE d, n", doc_id=test_doc_id, ) runner.log(f"Cleaned up test data for doc_id={test_doc_id}") except Exception as e: runner.log(f"Cleanup warning: {e}", "WARN") if neo4j_svc: try: neo4j_svc.close() except: pass # ═══════════════════════════════════════════════════════════════════ # REPORT # ═══════════════════════════════════════════════════════════════════ all_passed = runner.print_report() return 0 if all_passed else 1 if __name__ == "__main__": exit_code = run_tests() sys.exit(exit_code)