depscreen / tests /test_chat.py
halsabbah's picture
deploy: sync code from GitHub main
36b2bff verified
"""Route tests for /api/chat β€” conversation CRUD.
LLM-calling routes (send message, stream, auto-title) are deliberately
skipped at route level; they're covered at the service level in
test_services.py via mocked OpenAI clients. What we exercise here is
the conversation lifecycle: create β†’ list β†’ rename β†’ get messages β†’
archive. Plus the RBAC guard that patients own their own conversations.
"""
from __future__ import annotations
from uuid import uuid4
# ─────────────────────────────────────────────────────────────────────────────
# Create / list
# ─────────────────────────────────────────────────────────────────────────────
def test_create_conversation_requires_patient_role(client, clinician_headers):
resp = client.post(
"/api/chat/conversations",
headers=clinician_headers,
json={"title": "Test", "context_type": "general"},
)
assert resp.status_code == 403
def test_patient_creates_conversation(client, patient_headers):
resp = client.post(
"/api/chat/conversations",
headers=patient_headers,
json={"title": "Feeling off today", "context_type": "general"},
)
assert resp.status_code == 200
data = resp.json()
assert data["title"] == "Feeling off today"
assert data["context_type"] == "general"
assert data["message_count"] == 0
assert data["is_active"] is True
def test_list_conversations_returns_own_only(client, patient_user, patient_headers, db):
"""Patient A shouldn't see Patient B's conversations."""
from app.models.db import Conversation, User
# Seed a conversation for patient_user
db.add(
Conversation(
id=str(uuid4()),
user_id=patient_user.id,
title="My convo",
context_type="general",
is_active=True,
)
)
# A conversation owned by a different patient
other = User(
id=str(uuid4()),
email="other@test.local",
password_hash="x",
full_name="Other",
role="patient",
is_active=True,
)
db.add(other)
db.flush()
db.add(
Conversation(
id=str(uuid4()),
user_id=other.id,
title="Someone else's convo",
context_type="general",
is_active=True,
)
)
db.commit()
resp = client.get("/api/chat/conversations", headers=patient_headers)
assert resp.status_code == 200
titles = [c["title"] for c in resp.json()]
assert "My convo" in titles
assert "Someone else's convo" not in titles
# ─────────────────────────────────────────────────────────────────────────────
# Rename
# ─────────────────────────────────────────────────────────────────────────────
def test_rename_conversation(client, patient_headers):
create = client.post(
"/api/chat/conversations",
headers=patient_headers,
json={"title": "Old title", "context_type": "general"},
)
conv_id = create.json()["id"]
rename = client.patch(
f"/api/chat/conversations/{conv_id}",
headers=patient_headers,
json={"title": "A quieter week"},
)
assert rename.status_code == 200
assert rename.json()["title"] == "A quieter week"
# Confirm it persisted
lst = client.get("/api/chat/conversations", headers=patient_headers)
matching = [c for c in lst.json() if c["id"] == conv_id]
assert matching[0]["title"] == "A quieter week"
def test_rename_rejects_empty_title(client, patient_headers):
create = client.post(
"/api/chat/conversations",
headers=patient_headers,
json={"title": "Some title", "context_type": "general"},
)
conv_id = create.json()["id"]
resp = client.patch(
f"/api/chat/conversations/{conv_id}",
headers=patient_headers,
json={"title": " "},
)
assert resp.status_code == 400
def test_rename_rejects_overlong_title(client, patient_headers):
create = client.post(
"/api/chat/conversations",
headers=patient_headers,
json={"title": "Some title", "context_type": "general"},
)
conv_id = create.json()["id"]
resp = client.patch(
f"/api/chat/conversations/{conv_id}",
headers=patient_headers,
json={"title": "x" * 150},
)
assert resp.status_code == 400
def test_rename_404_on_other_users_conversation(client, patient_headers, db):
from app.models.db import Conversation, User
other = User(
id=str(uuid4()),
email="stranger@test.local",
password_hash="x",
full_name="Stranger",
role="patient",
is_active=True,
)
db.add(other)
db.flush()
theirs = Conversation(
id=str(uuid4()),
user_id=other.id,
title="Theirs",
context_type="general",
is_active=True,
)
db.add(theirs)
db.commit()
resp = client.patch(
f"/api/chat/conversations/{theirs.id}",
headers=patient_headers,
json={"title": "Hacked"},
)
assert resp.status_code == 404
# ─────────────────────────────────────────────────────────────────────────────
# Archive
# ─────────────────────────────────────────────────────────────────────────────
def test_archive_conversation_soft_deletes(client, patient_headers, db):
from app.models.db import Conversation
create = client.post(
"/api/chat/conversations",
headers=patient_headers,
json={"title": "To archive", "context_type": "general"},
)
conv_id = create.json()["id"]
resp = client.delete(f"/api/chat/conversations/{conv_id}", headers=patient_headers)
assert resp.status_code == 200
# Row still exists but is_active = False
row = db.query(Conversation).filter(Conversation.id == conv_id).first()
assert row is not None
assert row.is_active is False
# List no longer includes it (filters by is_active)
lst = client.get("/api/chat/conversations", headers=patient_headers)
ids = [c["id"] for c in lst.json()]
assert conv_id not in ids
# ─────────────────────────────────────────────────────────────────────────────
# Get messages
# ─────────────────────────────────────────────────────────────────────────────
def test_get_messages_returns_chronological(client, patient_user, patient_headers, db):
from datetime import UTC, datetime, timedelta
from app.models.db import ChatMessage, Conversation
conv_id = str(uuid4())
db.add(
Conversation(
id=conv_id,
user_id=patient_user.id,
title="Test",
context_type="general",
is_active=True,
)
)
now = datetime.now(UTC)
# Seed out-of-order so we can prove the endpoint sorts
db.add(
ChatMessage(
id=str(uuid4()),
conversation_id=conv_id,
role="assistant",
content="Second",
created_at=now,
)
)
db.add(
ChatMessage(
id=str(uuid4()),
conversation_id=conv_id,
role="user",
content="First",
created_at=now - timedelta(seconds=5),
)
)
db.commit()
resp = client.get(f"/api/chat/conversations/{conv_id}/messages", headers=patient_headers)
assert resp.status_code == 200
msgs = resp.json()["messages"]
assert [m["content"] for m in msgs] == ["First", "Second"]
def test_get_messages_404_on_foreign_conversation(client, patient_headers, db):
from app.models.db import Conversation, User
other = User(
id=str(uuid4()),
email="foreign@test.local",
password_hash="x",
full_name="Foreign",
role="patient",
is_active=True,
)
db.add(other)
db.flush()
theirs = Conversation(
id=str(uuid4()),
user_id=other.id,
title="Theirs",
context_type="general",
is_active=True,
)
db.add(theirs)
db.commit()
resp = client.get(f"/api/chat/conversations/{theirs.id}/messages", headers=patient_headers)
assert resp.status_code == 404