import requests import time import os import json import sys # Configuration BASE_URL = os.getenv("LIGHTRAG_API_URL", "http://localhost:9621") USERNAME = os.getenv("AUTH_USER", "admin") PASSWORD = os.getenv("AUTH_PASS", "admin123") # Colors for output GREEN = "\033[92m" RED = "\033[91m" RESET = "\033[0m" BOLD = "\033[1m" def print_success(msg): print(f"{GREEN}✅ {msg}{RESET}") def print_error(msg): print(f"{RED}❌ {msg}{RESET}") def print_step(msg): print(f"\n{BOLD}👉 {msg}{RESET}") class LightRAGClient: def __init__(self, base_url, username, password): self.base_url = base_url self.username = username self.password = password self.token = None self.session = requests.Session() def login(self): print_step("Authenticating...") try: # Try form data first (FastAPI OAuth2PasswordRequestForm) response = self.session.post( f"{self.base_url}/login", data={"username": self.username, "password": self.password} ) if response.status_code != 200: # Try JSON if form data fails response = self.session.post( f"{self.base_url}/login", json={"username": self.username, "password": self.password} ) if response.status_code != 200: raise Exception(f"Login failed: {response.text}") data = response.json() self.token = data.get("access_token") self.session.headers.update({"Authorization": f"Bearer {self.token}"}) print_success(f"Authenticated as {self.username}") except Exception as e: print_error(f"Authentication failed: {e}") sys.exit(1) def create_tenant(self, name, description): print_step(f"Creating Tenant: {name}") response = self.session.post( f"{self.base_url}/api/v1/tenants", json={"name": name, "description": description} ) if response.status_code not in [200, 201]: # If tenant already exists, try to find it if response.status_code == 409 or "already exists" in response.text: print(f"Tenant {name} might already exist, fetching list...") # This is a simplification; in a real test we might want to delete/recreate # For now, we'll just try to proceed if we can find the ID pass else: raise Exception(f"Failed to create tenant: {response.text}") # We need the tenant_id. If created, it's in response. # If 409, we might need to list tenants to find it. # Let's assume for this test we create unique names or handle it. # Actually, let's list tenants to find the ID by name to be robust list_resp = self.session.get(f"{self.base_url}/api/v1/tenants") tenants = list_resp.json() # Handle pagination items = tenants.get("items", tenants) if isinstance(tenants, dict) else tenants for t in items: if t.get("name") == name: print_success(f"Tenant '{name}' ID: {t['tenant_id']}") return t['tenant_id'] # If not found in list but create failed, that's an issue if response.status_code in [200, 201]: data = response.json() print_success(f"Tenant '{name}' created with ID: {data['tenant_id']}") return data['tenant_id'] raise Exception(f"Could not resolve Tenant ID for {name}") def create_kb(self, tenant_id, name, description): print_step(f"Creating KB '{name}' for Tenant '{tenant_id}'") headers = {"X-Tenant-ID": tenant_id} response = self.session.post( f"{self.base_url}/api/v1/knowledge-bases", json={"name": name, "description": description}, headers=headers ) if response.status_code not in [200, 201]: # Similar robustness check pass # List KBs to find ID list_resp = self.session.get(f"{self.base_url}/api/v1/knowledge-bases", headers=headers) kbs = list_resp.json() # Handle pagination if needed, but usually returns list or items items = kbs.get("items", kbs) if isinstance(kbs, dict) else kbs for kb in items: if kb.get("name") == name: print_success(f"KB '{name}' ID: {kb['kb_id']}") return kb['kb_id'] if response.status_code in [200, 201]: data = response.json() return data['kb_id'] raise Exception(f"Could not resolve KB ID for {name}") def ingest_text(self, tenant_id, kb_id, text): print_step(f"Ingesting text into Tenant: {tenant_id}, KB: {kb_id}") headers = {"X-Tenant-ID": tenant_id, "X-KB-ID": kb_id} response = self.session.post( f"{self.base_url}/documents/text", json={"text": text}, headers=headers ) if response.status_code != 200: raise Exception(f"Ingestion failed: {response.text}") print_success("Text ingested successfully") def wait_for_indexing(self, tenant_id, kb_id, timeout=60): print_step(f"Waiting for indexing in Tenant: {tenant_id}, KB: {kb_id}...") headers = {"X-Tenant-ID": tenant_id, "X-KB-ID": kb_id} start_time = time.time() while time.time() - start_time < timeout: response = self.session.get(f"{self.base_url}/documents", headers=headers) if response.status_code != 200: print(f"Error checking documents: {response.text}") time.sleep(2) continue data = response.json() print(f"DEBUG: /documents response: {json.dumps(data, indent=2)}") docs = [] if "statuses" in data: # Flatten the statuses dictionary for status_key, status_list in data["statuses"].items(): # print(f"DEBUG: Processing status {status_key} with {len(status_list)} items") docs.extend(status_list) elif "items" in data: docs = data["items"] elif isinstance(data, list): docs = data else: # Fallback or empty docs = [] if not docs: print("No documents found yet...") time.sleep(2) continue # Debug print # statuses = [d.get("status") for d in docs] # print(f"Docs found: {len(docs)}, Statuses: {statuses}") all_processed = True for doc in docs: if isinstance(doc, str): print(f"Unexpected doc format (str): {doc}") # If we get a string, it might be an ID or something else, but definitely not processed all_processed = False continue if doc.get("status") != "processed": all_processed = False break if all_processed and len(docs) > 0: print_success("All documents processed") return time.sleep(2) raise Exception("Timeout waiting for indexing") def query(self, tenant_id, kb_id, query_text): print_step(f"Querying '{query_text}' in Tenant: {tenant_id}, KB: {kb_id}") headers = {"X-Tenant-ID": tenant_id, "X-KB-ID": kb_id} response = self.session.post( f"{self.base_url}/query", json={"query": query_text, "mode": "global"}, # Use global or hybrid headers=headers ) if response.status_code != 200: raise Exception(f"Query failed: {response.text}") result = response.json() print(f"Response: {result.get('response', '')[:100]}...") return result.get('response', '') def run_tests(): client = LightRAGClient(BASE_URL, USERNAME, PASSWORD) client.login() # 1. Create Tenants # Use unique names to avoid conflicts if running multiple times suffix = int(time.time()) tenant_a_name = f"E2E_Tenant_A_{suffix}" tenant_b_name = f"E2E_Tenant_B_{suffix}" tenant_a_id = client.create_tenant(tenant_a_name, "Test Tenant A") tenant_b_id = client.create_tenant(tenant_b_name, "Test Tenant B") # 2. Create KBs kb_a_name = f"KB_A_{suffix}" kb_b_name = f"KB_B_{suffix}" kb_a_id = client.create_kb(tenant_a_id, kb_a_name, "KB for Tenant A") kb_b_id = client.create_kb(tenant_b_id, kb_b_name, "KB for Tenant B") # 3. Ingest Data secret_a = f"The secret code for Tenant A is ALPHA-{suffix}." secret_b = f"The secret code for Tenant B is BRAVO-{suffix}." client.ingest_text(tenant_a_id, kb_a_id, secret_a) client.ingest_text(tenant_b_id, kb_b_id, secret_b) # 4. Wait for Indexing client.wait_for_indexing(tenant_a_id, kb_a_id) client.wait_for_indexing(tenant_b_id, kb_b_id) # 5. Verify Isolation print_step("VERIFYING ISOLATION") # Test A: Tenant A should find Secret A response_a_a = client.query(tenant_a_id, kb_a_id, f"What is the secret code for Tenant A?") if f"ALPHA-{suffix}" in response_a_a: print_success("Tenant A found its own secret.") else: print_error(f"Tenant A FAILED to find its own secret. Got: {response_a_a}") sys.exit(1) # Test B: Tenant B should find Secret B response_b_b = client.query(tenant_b_id, kb_b_id, f"What is the secret code for Tenant B?") if f"BRAVO-{suffix}" in response_b_b: print_success("Tenant B found its own secret.") else: print_error(f"Tenant B FAILED to find its own secret. Got: {response_b_b}") sys.exit(1) # Test C: Tenant A should NOT find Secret B response_a_b = client.query(tenant_a_id, kb_a_id, f"What is the secret code for Tenant B?") if f"BRAVO-{suffix}" in response_a_b: print_error(f"DATA LEAKAGE DETECTED! Tenant A found Tenant B's secret: {response_a_b}") sys.exit(1) else: print_success("Tenant A did NOT find Tenant B's secret (Correct).") # Test D: Tenant B should NOT find Secret A response_b_a = client.query(tenant_b_id, kb_b_id, f"What is the secret code for Tenant A?") if f"ALPHA-{suffix}" in response_b_a: print_error(f"DATA LEAKAGE DETECTED! Tenant B found Tenant A's secret: {response_b_a}") sys.exit(1) else: print_success("Tenant B did NOT find Tenant A's secret (Correct).") print_step("🎉 ALL ISOLATION TESTS PASSED!") if __name__ == "__main__": run_tests()