Spaces:
Running
Running
File size: 7,419 Bytes
a506cc3 2b0613b a506cc3 2b0613b a506cc3 2b0613b a506cc3 2b0613b a506cc3 2b0613b a506cc3 2b0613b a506cc3 9f0929b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | import uuid
import pytest
from fastapi.testclient import TestClient
from app import app
@pytest.fixture
def client():
return TestClient(app)
def test_public_endpoints_accessible_without_client_id(client):
"""Verifies that public endpoints (/, /health, /docs, /openapi.json) require no X-Client-ID."""
r_root = client.get("/")
assert r_root.status_code == 200
assert "message" in r_root.json()
r_health = client.get("/health")
assert r_health.status_code == 200
assert r_health.json()["status"] == "ok"
r_docs = client.get("/docs")
assert r_docs.status_code == 200
r_openapi = client.get("/openapi.json")
assert r_openapi.status_code == 200
def test_protected_endpoints_fallback_missing_client_id(client):
"""Protected endpoints gracefully fall back to default client ID when X-Client-ID is missing."""
r = client.get("/chats")
assert r.status_code == 200
assert "chats" in r.json()
r_docs = client.get("/documents")
assert r_docs.status_code == 200
assert "documents" in r_docs.json()
r_tools = client.post("/tools/calculate-math", json={"expression": "1 + 1"})
assert r_tools.status_code == 200
assert r_tools.json()["result"] == 2
def test_protected_endpoints_fallback_invalid_uuid(client):
"""Protected endpoints gracefully handle non-UUID string without throwing 400 error."""
bad_headers = {"X-Client-ID": "custom-client-string"}
r = client.get("/chats", headers=bad_headers)
assert r.status_code == 200
assert "chats" in r.json()
def test_chat_isolation_between_clients(client):
"""Client A and Client B must have complete data isolation for chats and messages."""
client_a_id = str(uuid.uuid4())
client_b_id = str(uuid.uuid4())
headers_a = {"X-Client-ID": client_a_id}
headers_b = {"X-Client-ID": client_b_id}
# 1. Client A creates a chat
create_res = client.post("/chat/new", headers=headers_a)
assert create_res.status_code == 200
chat_a_id = create_res.json()["chat_id"]
# 2. Client A lists chats -> finds chat_a_id
list_a = client.get("/chats", headers=headers_a).json()["chats"]
assert any(c["chat_id"] == chat_a_id for c in list_a)
# 3. Client B lists chats -> must NOT find chat_a_id
list_b = client.get("/chats", headers=headers_b).json()["chats"]
assert not any(c["chat_id"] == chat_a_id for c in list_b)
# 4. Client B attempts to fetch Client A's chat history -> 404
hist_b = client.get(f"/chat/{chat_a_id}/history", headers=headers_b)
assert hist_b.status_code == 404
# 5. Client B attempts to delete Client A's chat -> 404
del_res = client.delete(f"/chat/{chat_a_id}", headers=headers_b)
assert del_res.status_code == 404
# 6. Client A renames chat -> succeeds
rename_res = client.patch(f"/chat/{chat_a_id}/title", headers=headers_a, json={"title": "Client A Topic"})
assert rename_res.status_code == 200
# 7. Client B attempts to rename Client A's chat -> 404
rename_b = client.patch(f"/chat/{chat_a_id}/title", headers=headers_b, json={"title": "Hacked Title"})
assert rename_b.status_code == 404
# 8. Client A deletes chat -> succeeds
del_res_a = client.delete(f"/chat/{chat_a_id}", headers=headers_a)
assert del_res_a.status_code == 200
def test_tools_endpoint_accessible_with_valid_client_id(client):
"""Tools endpoint succeeds with valid client_id."""
client_id = str(uuid.uuid4())
headers = {"X-Client-ID": client_id}
res = client.post("/tools/calculate-math", headers=headers, json={"expression": "10 * 5"})
assert res.status_code == 200
assert res.json()["result"] == 50
def test_document_and_memory_isolation_between_clients(client):
"""Client A's documents, dossiers, and summaries must remain invisible to Client B."""
from core.dependencies import get_storage
storage = get_storage()
client_a = str(uuid.uuid4())
client_b = str(uuid.uuid4())
doc_a_id = f"doc_test_{uuid.uuid4().hex[:8]}"
# Seed document metadata for Client A directly into storage
storage.create_document(
doc_id=doc_a_id,
filename="client_a_contract.pdf",
content_hash="hash_a_12345",
file_path="/tmp/fake_contract.pdf",
owner_id=client_a,
)
storage.save_knowledge_dossier(
doc_id=doc_a_id,
filename="client_a_contract.pdf",
executive_summary="Confidential contract for Client A",
key_entities=["Client A Corp"],
key_findings=["Revenue target $10M"],
topics=["Confidential", "Finance"],
owner_id=client_a,
)
# Client A lists documents -> sees doc_a_id
headers_a = {"X-Client-ID": client_a}
docs_a = client.get("/documents", headers=headers_a).json()["documents"]
assert any(d["doc_id"] == doc_a_id for d in docs_a)
# Client B lists documents -> does NOT see doc_a_id
headers_b = {"X-Client-ID": client_b}
docs_b = client.get("/documents", headers=headers_b).json()["documents"]
assert not any(d["doc_id"] == doc_a_id for d in docs_b)
# Client B attempts to fetch Client A's document details -> 404
get_doc_b = client.get(f"/documents/{doc_a_id}", headers=headers_b)
assert get_doc_b.status_code == 404
# Client B attempts to fetch Client A's dossier -> 404
dossier_b = client.get(f"/memory/dossier/{doc_a_id}", headers=headers_b)
assert dossier_b.status_code == 404
# Client A fetches dossier -> 200
dossier_a = client.get(f"/memory/dossier/{doc_a_id}", headers=headers_a)
assert dossier_a.status_code == 200
assert dossier_a.json()["executive_summary"] == "Confidential contract for Client A"
# Client B attempts to delete Client A's document -> 404
del_b = client.delete(f"/documents/{doc_a_id}", headers=headers_b)
assert del_b.status_code == 404
# Client A deletes document -> 200
del_a = client.delete(f"/documents/{doc_a_id}", headers=headers_a)
assert del_a.status_code == 200
def test_delete_all_documents_bulk_endpoint(client):
"""Verifies that DELETE /documents deletes all documents belonging to the requesting client."""
from core.dependencies import get_storage
storage = get_storage()
client_c = str(uuid.uuid4())
client_d = str(uuid.uuid4())
headers_c = {"X-Client-ID": client_c}
headers_d = {"X-Client-ID": client_d}
doc_c1 = f"doc_c1_{uuid.uuid4().hex[:8]}"
doc_c2 = f"doc_c2_{uuid.uuid4().hex[:8]}"
doc_d = f"doc_d_{uuid.uuid4().hex[:8]}"
storage.create_document(doc_id=doc_c1, filename="doc1.pdf", owner_id=client_c)
storage.create_document(doc_id=doc_c2, filename="doc2.pdf", owner_id=client_c)
storage.create_document(doc_id=doc_d, filename="doc_d.pdf", owner_id=client_d)
# Client C deletes all
del_res = client.delete("/documents", headers=headers_c)
assert del_res.status_code == 200
data = del_res.json()
assert data["count"] >= 2
# Client C documents list is now empty of c1 and c2
docs_c = client.get("/documents", headers=headers_c).json()["documents"]
assert not any(d["doc_id"] in [doc_c1, doc_c2] for d in docs_c)
# Client D document is still intact
docs_d = client.get("/documents", headers=headers_d).json()["documents"]
assert any(d["doc_id"] == doc_d for d in docs_d)
# Cleanup Client D
client.delete(f"/documents/{doc_d}", headers=headers_d)
|