Spaces:
Running
Running
| """Émo backend integration tests.""" | |
| import os | |
| import time | |
| import json | |
| import uuid | |
| import requests | |
| import pytest | |
| BASE_URL = os.environ.get('REACT_APP_BACKEND_URL', 'https://emo-personal.preview.emergentagent.com').rstrip('/') | |
| API = f"{BASE_URL}/api" | |
| UNIQUE = uuid.uuid4().hex[:8] | |
| EMAIL = f"test_{UNIQUE}@example.com" | |
| PASSWORD = "emo-test-2026" | |
| NAME = f"Tester {UNIQUE}" | |
| def session(): | |
| s = requests.Session() | |
| s.headers.update({"Content-Type": "application/json"}) | |
| return s | |
| def auth_session(session): | |
| r = session.post(f"{API}/auth/signup", json={"email": EMAIL, "password": PASSWORD, "name": NAME}) | |
| assert r.status_code == 200, f"signup failed: {r.status_code} {r.text}" | |
| data = r.json() | |
| assert data["email"] == EMAIL | |
| # Extract token from Set-Cookie (cookie is HttpOnly/secure/samesite=none — requests session has it) | |
| return session | |
| # ---------------- AUTH ---------------- # | |
| def test_health(session): | |
| r = session.get(f"{API}/") | |
| assert r.status_code == 200 | |
| assert r.json().get("status") == "ok" | |
| def test_signup_duplicate(auth_session): | |
| r = auth_session.post(f"{API}/auth/signup", json={"email": EMAIL, "password": PASSWORD, "name": NAME}) | |
| assert r.status_code == 409 | |
| def test_me(auth_session): | |
| r = auth_session.get(f"{API}/auth/me") | |
| assert r.status_code == 200 | |
| data = r.json() | |
| assert data["email"] == EMAIL | |
| assert data["name"] == NAME | |
| assert data["auth_provider"] == "password" | |
| def test_login_wrong_password(session): | |
| r = session.post(f"{API}/auth/login", json={"email": EMAIL, "password": "wrongpw"}) | |
| assert r.status_code == 401 | |
| def test_login_success_and_bearer_auth(): | |
| # Use new session, capture session_token cookie | |
| s = requests.Session() | |
| s.headers.update({"Content-Type": "application/json"}) | |
| r = s.post(f"{API}/auth/login", json={"email": EMAIL, "password": PASSWORD}) | |
| assert r.status_code == 200, r.text | |
| token = s.cookies.get("session_token") | |
| assert token, "session_token cookie not set" | |
| # Verify Bearer auth works on a fresh session (no cookies) | |
| s2 = requests.Session() | |
| r2 = s2.get(f"{API}/auth/me", headers={"Authorization": f"Bearer {token}"}) | |
| assert r2.status_code == 200 | |
| assert r2.json()["email"] == EMAIL | |
| def test_me_unauthenticated(session): | |
| s = requests.Session() | |
| r = s.get(f"{API}/auth/me") | |
| assert r.status_code == 401 | |
| # ---------------- CONVERSATIONS ---------------- # | |
| def conversation_id(auth_session): | |
| r = auth_session.post(f"{API}/conversations", json={"title": "TEST_conv", "mode": "normal"}) | |
| assert r.status_code == 200, r.text | |
| data = r.json() | |
| assert data["title"] == "TEST_conv" | |
| assert data["mode"] == "normal" | |
| assert "conversation_id" in data | |
| assert "_id" not in data | |
| return data["conversation_id"] | |
| def test_list_conversations(auth_session, conversation_id): | |
| r = auth_session.get(f"{API}/conversations") | |
| assert r.status_code == 200 | |
| ids = [c["conversation_id"] for c in r.json()] | |
| assert conversation_id in ids | |
| def test_rename_conversation(auth_session, conversation_id): | |
| r = auth_session.patch(f"{API}/conversations/{conversation_id}", json={"title": "TEST_renamed"}) | |
| assert r.status_code == 200 | |
| r2 = auth_session.get(f"{API}/conversations") | |
| titles = {c["conversation_id"]: c["title"] for c in r2.json()} | |
| assert titles[conversation_id] == "TEST_renamed" | |
| def test_messages_empty(auth_session, conversation_id): | |
| r = auth_session.get(f"{API}/conversations/{conversation_id}/messages") | |
| assert r.status_code == 200 | |
| assert isinstance(r.json(), list) | |
| def test_rename_not_found(auth_session): | |
| r = auth_session.patch(f"{API}/conversations/conv_nonexistent", json={"title": "x"}) | |
| assert r.status_code == 404 | |
| # ---------------- CHAT STREAM ---------------- # | |
| def _stream_chat(auth_session, conv_id, content, mode="normal"): | |
| r = auth_session.post( | |
| f"{API}/chat/stream", | |
| json={"conversation_id": conv_id, "content": content, "mode": mode}, | |
| stream=True, | |
| timeout=90, | |
| ) | |
| assert r.status_code == 200, r.text | |
| deltas = [] | |
| done = None | |
| err = None | |
| for line in r.iter_lines(decode_unicode=True): | |
| if not line: | |
| continue | |
| if line.startswith("data: "): | |
| payload = json.loads(line[6:]) | |
| if payload["type"] == "delta": | |
| deltas.append(payload["content"]) | |
| elif payload["type"] == "done": | |
| done = payload | |
| elif payload["type"] == "error": | |
| err = payload | |
| return deltas, done, err | |
| def test_chat_stream_normal_and_autotitle(auth_session): | |
| # Fresh conversation to test auto-title | |
| rc = auth_session.post(f"{API}/conversations", json={}) | |
| cid = rc.json()["conversation_id"] | |
| deltas, done, err = _stream_chat(auth_session, cid, "Dis bonjour en une phrase.", "normal") | |
| assert err is None, f"stream error: {err}" | |
| assert len(deltas) > 0, "no deltas received" | |
| full = "".join(deltas) | |
| assert done is not None, "no done event" | |
| assert done.get("mood"), "mood missing in done event" | |
| # Persona: must not claim to be Claude | |
| assert "claude" not in full.lower(), f"Persona leak: {full}" | |
| # Auto-title set from first user message | |
| convs = auth_session.get(f"{API}/conversations").json() | |
| titles = {c["conversation_id"]: c["title"] for c in convs} | |
| assert titles[cid] != "Nouvelle conversation", f"auto-title not set: {titles[cid]}" | |
| # Messages persisted | |
| msgs = auth_session.get(f"{API}/conversations/{cid}/messages").json() | |
| assert len(msgs) >= 2 | |
| roles = [m["role"] for m in msgs] | |
| assert "user" in roles and "emo" in roles | |
| emo_msg = next(m for m in msgs if m["role"] == "emo") | |
| assert "[MOOD:" not in emo_msg["content"], "MOOD tag not stripped from stored content" | |
| assert emo_msg.get("mood") in ( | |
| "neutre", "amusee", "concentree", "sarcastique", "enthousiaste", "agacee", "curieuse", "pensive" | |
| ) | |
| def test_chat_stream_tech_mode_persona(auth_session): | |
| rc = auth_session.post(f"{API}/conversations", json={"mode": "tech"}) | |
| cid = rc.json()["conversation_id"] | |
| deltas, done, err = _stream_chat(auth_session, cid, "Qui es-tu ?", "tech") | |
| assert err is None | |
| full = "".join(deltas).lower() | |
| # Emo may say "pas Claude" (denying Claude identity) — that's fine. | |
| # The real failure mode is claiming to BE Claude. | |
| forbidden = ["je suis claude", "i am claude", "i'm claude", "modèle anthropic", "model anthropic"] | |
| for phrase in forbidden: | |
| assert phrase not in full, f"Emo claimed Claude identity: {full}" | |
| assert "émo" in full or "emo" in full, f"Emo identity missing: {full}" | |
| # ---------------- DELETE ---------------- # | |
| def test_delete_conversation(auth_session, conversation_id): | |
| r = auth_session.delete(f"{API}/conversations/{conversation_id}") | |
| assert r.status_code == 200 | |
| # Verify gone | |
| r2 = auth_session.get(f"{API}/conversations/{conversation_id}/messages") | |
| assert r2.status_code == 404 | |
| def test_logout(auth_session): | |
| r = auth_session.post(f"{API}/auth/logout") | |
| assert r.status_code == 200 | |
| r2 = auth_session.get(f"{API}/auth/me") | |
| assert r2.status_code == 401 | |