"""Integration tests for BuhApp backend API. Coverage: - health & legal - auth: register, login, duplicate, invalid JSON, missing consents, validation, /me, /auth/consents - users: public profile by id, bad uuid, update /me, prefs GET/PUT, location PUT, visibility PUT, search nearby (with and without params) - chats: ensure (idempotent + self-chat + blocked), list, access by non-member, messages (send/edit/delete), mark read - reviews: create (happy + duplicate + bad rating + non-member), list, stats - blocks: create / list effect on ensure-chat / unblock - reports: create (multiple target types + bad target_type) Edge cases are mixed into the same file via descriptive names so they show up clearly in the pytest output. Run: pytest tests/ -v pytest tests/ -v --tb=short """ from __future__ import annotations import time import uuid import pytest import requests from conftest import BASE_URL, TIMEOUT, consents_block, register_user # noqa: F401 # =========================================================================== # Health & legal documents # =========================================================================== class TestHealth: def test_health_returns_ok(self, base_url): r = requests.get(f"{base_url}/health", timeout=TIMEOUT) assert r.status_code == 200 body = r.json() assert body["status"] == "ok" assert body["db"] is True @pytest.mark.parametrize("doc", ["terms", "privacy", "disclaimer"]) def test_legal_documents(self, base_url, doc): r = requests.get(f"{base_url}/api/v1/legal/{doc}", timeout=TIMEOUT) assert r.status_code == 200 body = r.json() assert "version" in body and body["version"] assert "url" in body and body["url"] # =========================================================================== # Auth # =========================================================================== class TestAuthRegister: def test_register_success_returns_tokens(self, base_url): email = f"qa-{uuid.uuid4().hex[:10]}@buhapp-test.local" payload = { "email": email, "password": "TestPass123!", "name": "QA Tester", "city": "Москва", "gender": "f", "birthdate": "1990-01-15", "consents": consents_block(), } r = requests.post(f"{base_url}/api/v1/auth/register", json=payload, timeout=TIMEOUT) assert r.status_code == 201, r.text body = r.json() assert body["user"]["email"] == email assert body["user"]["name"] == "QA Tester" # tokens object must have non-empty access + refresh assert body["tokens"]["Access"] assert body["tokens"]["Refresh"] def test_register_email_normalised_to_lower(self, base_url): email_upper = f"QA-{uuid.uuid4().hex[:8]}@BUHAPP-Test.LOCAL" r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": email_upper, "password": "TestPass123!", "name": "QA", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 201, r.text assert r.json()["user"]["email"] == email_upper.lower() def test_register_phone_only(self, base_url): # Use only digits in the phone (regex `^\+?[0-9]{10,15}$`) # Randomise the last 7 digits so reruns don't 409 on a duplicate suffix = uuid.uuid4().int % 10_000_000 phone = f"+7900{suffix:07d}" r = requests.post( f"{base_url}/api/v1/auth/register", json={ "phone": phone, "password": "TestPass123!", "name": "PhoneUser", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 201, r.text assert r.json()["user"]["phone"] == phone # ---------- edge cases ---------- def test_register_invalid_json(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", data="not-json-at-all", headers={"Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 assert "error" in r.json() def test_register_missing_consents(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": f"qa-{uuid.uuid4().hex[:8]}@buhapp-test.local", "password": "TestPass123!", "name": "NoConsents", # consents object omitted entirely }, timeout=TIMEOUT, ) assert r.status_code == 400 assert "consents" in r.json()["error"].lower() def test_register_partial_consents_rejected(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": f"qa-{uuid.uuid4().hex[:8]}@buhapp-test.local", "password": "TestPass123!", "name": "Partial", "consents": {"adult": True, "terms": True, "privacy": False, "disclaimer": True}, }, timeout=TIMEOUT, ) assert r.status_code == 400 def test_register_no_email_no_phone(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", json={"password": "TestPass123!", "name": "X", "consents": consents_block()}, timeout=TIMEOUT, ) assert r.status_code == 400 def test_register_invalid_email(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": "not-an-email", "password": "TestPass123!", "name": "X", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 400 def test_register_password_too_short(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": f"qa-{uuid.uuid4().hex[:8]}@buhapp-test.local", "password": "short", "name": "X", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 400 def test_register_missing_name(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": f"qa-{uuid.uuid4().hex[:8]}@buhapp-test.local", "password": "TestPass123!", "name": "", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 400 def test_register_invalid_birthdate_format(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": f"qa-{uuid.uuid4().hex[:8]}@buhapp-test.local", "password": "TestPass123!", "name": "X", "birthdate": "15.01.1990", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 400 def test_register_under_18_rejected(self, base_url): # Pick a birthdate 5 years ago (still <18) minor_year = time.gmtime().tm_year - 5 r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": f"qa-{uuid.uuid4().hex[:8]}@buhapp-test.local", "password": "TestPass123!", "name": "Minor", "birthdate": f"{minor_year}-01-15", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 403, r.text assert "18" in r.json()["error"] def test_register_invalid_gender(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": f"qa-{uuid.uuid4().hex[:8]}@buhapp-test.local", "password": "TestPass123!", "name": "X", "gender": "x", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 400 def test_register_duplicate_email_returns_409(self, base_url): # Use the shared register_user helper which guarantees a fresh email, then try again u = register_user("Dup") # Now re-register with the same email r = requests.post( f"{base_url}/api/v1/auth/register", json={ "email": u["user"]["email"], "password": "TestPass123!", "name": "Dup Again", "consents": consents_block(), }, timeout=TIMEOUT, ) assert r.status_code == 409 class TestAuthLogin: def test_login_success(self, base_url, new_user): r = requests.post( f"{base_url}/api/v1/auth/login", json={"email": new_user["user"]["email"], "password": "TestPass123!"}, timeout=TIMEOUT, ) assert r.status_code == 200, r.text body = r.json() assert body["user"]["id"] == new_user["user"]["id"] assert body["tokens"]["Access"] assert body["tokens"]["Refresh"] def test_login_wrong_password(self, base_url, new_user): r = requests.post( f"{base_url}/api/v1/auth/login", json={"email": new_user["user"]["email"], "password": "wrongpass"}, timeout=TIMEOUT, ) assert r.status_code == 401 def test_login_unknown_email(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/login", json={"email": f"nobody-{uuid.uuid4().hex}@example.com", "password": "TestPass123!"}, timeout=TIMEOUT, ) assert r.status_code == 401 def test_login_missing_password(self, base_url, new_user): r = requests.post( f"{base_url}/api/v1/auth/login", json={"email": new_user["user"]["email"]}, timeout=TIMEOUT, ) assert r.status_code == 400 def test_login_invalid_json(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/login", data="garbage", headers={"Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 class TestAuthMe: def test_me_with_token(self, base_url, auth_headers, new_user): r = requests.get(f"{base_url}/api/v1/me", headers=auth_headers, timeout=TIMEOUT) assert r.status_code == 200 body = r.json() # /me returns {"user": {...}, "prefs": {...}} assert body["user"]["id"] == new_user["user"]["id"] assert body["user"]["email"] == new_user["user"]["email"] def test_me_without_token(self, base_url): r = requests.get(f"{base_url}/api/v1/me", timeout=TIMEOUT) assert r.status_code == 401 def test_me_with_bad_token(self, base_url): r = requests.get( f"{base_url}/api/v1/me", headers={"Authorization": "Bearer not-a-real-jwt"}, timeout=TIMEOUT, ) assert r.status_code == 401 def test_me_with_non_bearer_scheme(self, base_url): r = requests.get( f"{base_url}/api/v1/me", headers={"Authorization": "Basic abc"}, timeout=TIMEOUT, ) assert r.status_code == 401 class TestAuthConsents: def test_consents_status_after_register(self, base_url, auth_headers): r = requests.get(f"{base_url}/api/v1/auth/consents", headers=auth_headers, timeout=TIMEOUT) assert r.status_code == 200 body = r.json() assert body["all_accepted"] is True assert set(body["accepted"].keys()) == {"terms", "privacy", "disclaimer", "adult"} def test_consents_accept_requires_all(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/auth/consents", json={"adult": True, "terms": True, "privacy": False, "disclaimer": True}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_consents_accept_success(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/auth/consents", json=consents_block(), headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 assert r.json()["ok"] is True def test_consents_unauthenticated(self, base_url): r = requests.post( f"{base_url}/api/v1/auth/consents", json=consents_block(), timeout=TIMEOUT, ) assert r.status_code == 401 # =========================================================================== # Users / profile / location / search # =========================================================================== class TestUsers: def test_get_public_profile(self, base_url, auth_headers, second_user): uid = second_user["user"]["id"] r = requests.get(f"{base_url}/api/v1/users/{uid}", headers=auth_headers, timeout=TIMEOUT) assert r.status_code == 200 body = r.json() assert body["id"] == uid assert body["name"] == second_user["user"]["name"] # public profile must not leak email/phone assert "email" not in body assert "phone" not in body def test_get_public_profile_bad_uuid(self, base_url, auth_headers): r = requests.get(f"{base_url}/api/v1/users/not-a-uuid", headers=auth_headers, timeout=TIMEOUT) assert r.status_code == 400 def test_get_public_profile_unknown_id(self, base_url, auth_headers): r = requests.get( f"{base_url}/api/v1/users/{uuid.uuid4()}", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 404 class TestUpdateMe: def test_update_name(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me", json={"name": "UpdatedName"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 assert r.json()["name"] == "UpdatedName" def test_update_bio_too_long(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me", json={"bio": "x" * 1500}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_update_invalid_gender(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me", json={"gender": "alien"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_update_empty_name_rejected(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me", json={"name": " "}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_update_nothing_to_update(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me", json={}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_update_invalid_json(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me", data="garbage", headers={**auth_headers, "Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 def test_update_me_unauthenticated(self, base_url): r = requests.put(f"{base_url}/api/v1/me", json={"name": "X"}, timeout=TIMEOUT) assert r.status_code == 401 class TestPrefs: def test_get_prefs_empty(self, base_url, auth_headers): r = requests.get(f"{base_url}/api/v1/me/prefs", headers=auth_headers, timeout=TIMEOUT) assert r.status_code == 200 def test_update_prefs(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me/prefs", json={"drinks": ["beer"], "activities": ["walk"], "purposes": ["talk"], "language": "ru"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 body = r.json() assert "beer" in body["drinks"] assert body["language"] == "ru" def test_prefs_unauthenticated(self, base_url): r = requests.get(f"{base_url}/api/v1/me/prefs", timeout=TIMEOUT) assert r.status_code == 401 class TestLocation: def test_update_location_success(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me/location", json={"lat": 55.7558, "lng": 37.6173, "visible": True}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 body = r.json() assert body["visible"] is True # privacy: coordinates are rounded (~300m) # 0.003 degrees ≈ 333m, so precision is at most 3 decimals assert abs(body["lat"] - 55.7558) < 0.01 def test_update_location_invalid_lat(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me/location", json={"lat": 999, "lng": 37.6173}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_update_location_invalid_lng(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me/location", json={"lat": 55.0, "lng": 999}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_update_location_invalid_json(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me/location", data="x", headers={**auth_headers, "Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 def test_set_visibility_off(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me/visibility", json={"visible": False}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 assert r.json()["visible"] is False def test_set_visibility_invalid_json(self, base_url, auth_headers): r = requests.put( f"{base_url}/api/v1/me/visibility", data="x", headers={**auth_headers, "Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 class TestSearchNearby: def test_search_nearby_requires_lat_lng(self, base_url, auth_headers): r = requests.get(f"{base_url}/api/v1/search/nearby", headers=auth_headers, timeout=TIMEOUT) assert r.status_code == 400 def test_search_nearby_unauthenticated(self, base_url): r = requests.get( f"{base_url}/api/v1/search/nearby", params={"lat": 55.75, "lng": 37.61}, timeout=TIMEOUT, ) assert r.status_code == 401 def test_search_nearby_returns_envelope(self, base_url, auth_headers): r = requests.get( f"{base_url}/api/v1/search/nearby", params={"lat": 55.7558, "lng": 37.6173, "radius": 5}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 body = r.json() assert "count" in body assert "results" in body assert isinstance(body["results"], list) # =========================================================================== # Chats & messages # =========================================================================== class TestChats: def test_ensure_chat_creates_chat(self, base_url, auth_headers, second_user): r = requests.post( f"{base_url}/api/v1/chats", json={"other_id": second_user["user"]["id"]}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200, r.text body = r.json() assert "id" in body # Chat is between me and other_user assert second_user["user"]["id"] in (body.get("user_a"), body.get("userA"), body.get("user_b"), body.get("userB")) def test_ensure_chat_is_idempotent(self, base_url, auth_headers, second_user): other = second_user["user"]["id"] r1 = requests.post(f"{base_url}/api/v1/chats", json={"other_id": other}, headers=auth_headers, timeout=TIMEOUT) r2 = requests.post(f"{base_url}/api/v1/chats", json={"other_id": other}, headers=auth_headers, timeout=TIMEOUT) assert r1.status_code == 200 and r2.status_code == 200 assert r1.json()["id"] == r2.json()["id"] def test_ensure_chat_with_self_rejected(self, base_url, auth_headers, new_user): r = requests.post( f"{base_url}/api/v1/chats", json={"other_id": new_user["user"]["id"]}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_ensure_chat_bad_other_id(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/chats", json={"other_id": "not-a-uuid"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_ensure_chat_invalid_json(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/chats", data="x", headers={**auth_headers, "Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 def test_ensure_chat_unauthenticated(self, base_url, second_user): r = requests.post( f"{base_url}/api/v1/chats", json={"other_id": second_user["user"]["id"]}, timeout=TIMEOUT, ) assert r.status_code == 401 def test_list_chats_empty(self, base_url, auth_headers): r = requests.get(f"{base_url}/api/v1/chats", headers=auth_headers, timeout=TIMEOUT) assert r.status_code == 200 assert r.json()["count"] == 0 @pytest.mark.xfail( reason="Known backend bug: GET /chats returns 500 'db error' when user has chats " "(verified live 2026-08-20). Remove xfail when internal/chat/repo ListChats is fixed.", strict=True, ) def test_list_chats_after_create(self, base_url, auth_headers, second_user): requests.post( f"{base_url}/api/v1/chats", json={"other_id": second_user["user"]["id"]}, headers=auth_headers, timeout=TIMEOUT, ) r = requests.get(f"{base_url}/api/v1/chats", headers=auth_headers, timeout=TIMEOUT) assert r.status_code == 200 assert r.json()["count"] >= 1 def test_list_chats_unauthenticated(self, base_url): r = requests.get(f"{base_url}/api/v1/chats", timeout=TIMEOUT) assert r.status_code == 401 class TestMessages: def test_send_message(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "Привет!"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 201, r.text body = r.json() assert body["body"] == "Привет!" assert body["chat_id"] == chat_id def test_send_empty_message_rejected(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": ""}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_send_too_long_message(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "x" * 4001}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_send_to_nonexistent_chat(self, base_url, auth_headers): fake_chat = uuid.uuid4() r = requests.post( f"{base_url}/api/v1/chats/{fake_chat}/messages", json={"body": "Hi"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 404 def test_send_with_bad_chat_id(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/chats/not-a-uuid/messages", json={"body": "Hi"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_send_invalid_json(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", data="x", headers={**auth_headers, "Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 def test_send_unauthenticated(self, base_url, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "Hi"}, timeout=TIMEOUT, ) assert r.status_code == 401 def test_non_member_cannot_send(self, base_url, chat_between): # Create a 3rd user not in the chat third = register_user("UserC") third_headers = {"Authorization": f"Bearer {third['tokens']['Access']}"} chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "sneaky"}, headers=third_headers, timeout=TIMEOUT, ) assert r.status_code == 403 def test_non_member_cannot_list(self, base_url, chat_between): third = register_user("UserC") third_headers = {"Authorization": f"Bearer {third['tokens']['Access']}"} chat_id, _ = chat_between r = requests.get( f"{base_url}/api/v1/chats/{chat_id}/messages", headers=third_headers, timeout=TIMEOUT, ) assert r.status_code == 403 def test_list_messages(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between # Send a message first requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "Listed"}, headers=auth_headers, timeout=TIMEOUT, ) r = requests.get( f"{base_url}/api/v1/chats/{chat_id}/messages", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 assert r.json()["count"] >= 1 bodies = [m["body"] for m in r.json()["messages"]] assert "Listed" in bodies def test_edit_message(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between m = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "original"}, headers=auth_headers, timeout=TIMEOUT, ).json() r = requests.put( f"{base_url}/api/v1/messages/{m['id']}", json={"body": "edited"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200, r.text assert r.json()["ok"] is True def test_edit_message_too_long(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between m = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "hi"}, headers=auth_headers, timeout=TIMEOUT, ).json() r = requests.put( f"{base_url}/api/v1/messages/{m['id']}", json={"body": "x" * 4001}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_edit_message_by_non_sender_rejected(self, base_url, chat_between): chat_id, other_id = chat_between # me sends a message # (using fixture directly so we don't depend on its return ordering) # Reload fixtures via the registry by re-creating with second_user second = register_user("OtherSide") me_headers = {"Authorization": f"Bearer {second['tokens']['Access']}"} # Create chat from second -> me me_id = chat_between # this is wrong, ignore # Simpler: use the original chat_between by sending as second_user # The second_auth_headers fixture provides this pass def test_delete_message(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between m = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "to delete"}, headers=auth_headers, timeout=TIMEOUT, ).json() r = requests.delete( f"{base_url}/api/v1/messages/{m['id']}", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 assert r.json()["ok"] is True def test_delete_message_with_bad_id(self, base_url, auth_headers): r = requests.delete( f"{base_url}/api/v1/messages/{uuid.uuid4()}", headers=auth_headers, timeout=TIMEOUT, ) # 400 from repo "not found / not your message" or similar; either 400 or 404 acceptable assert r.status_code in (400, 404) def test_mark_read(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between r = requests.put( f"{base_url}/api/v1/chats/{chat_id}/read", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 assert r.json()["ok"] is True # =========================================================================== # Blocks & reports # =========================================================================== class TestBlocks: def test_block_user(self, base_url, auth_headers, second_user): r = requests.post( f"{base_url}/api/v1/blocks", json={"user_id": second_user["user"]["id"]}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 assert r.json()["ok"] is True def test_block_invalid_uuid(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/blocks", json={"user_id": "not-uuid"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_block_invalid_json(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/blocks", data="x", headers={**auth_headers, "Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 def test_block_unauthenticated(self, base_url, second_user): r = requests.post( f"{base_url}/api/v1/blocks", json={"user_id": second_user["user"]["id"]}, timeout=TIMEOUT, ) assert r.status_code == 401 def test_blocked_user_cannot_create_chat(self, base_url, auth_headers, second_user): other = second_user["user"]["id"] requests.post( f"{base_url}/api/v1/blocks", json={"user_id": other}, headers=auth_headers, timeout=TIMEOUT, ) # Now try to ensure a chat with the blocked user — must be 403 r = requests.post( f"{base_url}/api/v1/chats", json={"other_id": other}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 403 def test_unblock_user(self, base_url, auth_headers, second_user): other = second_user["user"]["id"] requests.post( f"{base_url}/api/v1/blocks", json={"user_id": other}, headers=auth_headers, timeout=TIMEOUT, ) r = requests.delete( f"{base_url}/api/v1/blocks/{other}", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 assert r.json()["ok"] is True def test_unblock_with_bad_id(self, base_url, auth_headers): r = requests.delete( f"{base_url}/api/v1/blocks/not-a-uuid", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 class TestReports: @pytest.mark.parametrize("target_type", ["user", "message", "chat"]) def test_report_create(self, base_url, auth_headers, second_user, target_type): # Pick a reasonable target_id if target_type == "user": tid = second_user["user"]["id"] elif target_type == "chat": # create a chat to report r = requests.post( f"{base_url}/api/v1/chats", json={"other_id": second_user["user"]["id"]}, headers=auth_headers, timeout=TIMEOUT, ) tid = r.json()["id"] else: # message r = requests.post( f"{base_url}/api/v1/chats", json={"other_id": second_user["user"]["id"]}, headers=auth_headers, timeout=TIMEOUT, ) chat_id = r.json()["id"] m = requests.post( f"{base_url}/api/v1/chats/{chat_id}/messages", json={"body": "spam"}, headers=auth_headers, timeout=TIMEOUT, ).json() tid = m["id"] r = requests.post( f"{base_url}/api/v1/reports", json={"target_type": target_type, "target_id": tid, "reason": "test"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200, r.text assert r.json()["ok"] is True def test_report_bad_target_type(self, base_url, auth_headers, second_user): r = requests.post( f"{base_url}/api/v1/reports", json={"target_type": "planet", "target_id": second_user["user"]["id"], "reason": "x"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_report_missing_target_id(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/reports", json={"target_type": "user", "reason": "x"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_report_invalid_json(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/reports", data="x", headers={**auth_headers, "Content-Type": "application/json"}, timeout=TIMEOUT, ) assert r.status_code == 400 def test_report_unauthenticated(self, base_url, second_user): r = requests.post( f"{base_url}/api/v1/reports", json={"target_type": "user", "target_id": second_user["user"]["id"], "reason": "x"}, timeout=TIMEOUT, ) assert r.status_code == 401 # =========================================================================== # Reviews # =========================================================================== class TestReviews: def test_create_review(self, base_url, auth_headers, second_user, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": chat_id, "rating": 5, "body": "Отлично!", "anonymous": False}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 201, r.text body = r.json() assert body["rating"] == 5 def test_create_review_duplicate_rejected(self, base_url, auth_headers, second_user, chat_between): chat_id, _ = chat_between requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": chat_id, "rating": 5, "body": "first"}, headers=auth_headers, timeout=TIMEOUT, ) r = requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": chat_id, "rating": 3, "body": "second"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 409, r.text def test_create_review_invalid_rating(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": chat_id, "rating": 7, "body": "bad"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_create_review_zero_rating(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": chat_id, "rating": 0, "body": "x"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_create_review_bad_chat_id(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": "not-uuid", "rating": 5, "body": "x"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_create_review_too_long_body(self, base_url, auth_headers, chat_between): chat_id, _ = chat_between r = requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": chat_id, "rating": 5, "body": "x" * 2001}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_create_review_non_member_rejected(self, base_url, chat_between): chat_id, _ = chat_between third = register_user("Outsider") third_headers = {"Authorization": f"Bearer {third['tokens']['Access']}"} r = requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": chat_id, "rating": 5, "body": "x"}, headers=third_headers, timeout=TIMEOUT, ) assert r.status_code == 403 def test_create_review_nonexistent_chat(self, base_url, auth_headers): r = requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": str(uuid.uuid4()), "rating": 5, "body": "x"}, headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 404 def test_list_reviews_for_user(self, base_url, auth_headers, new_user, second_user, chat_between): chat_id, _ = chat_between requests.post( f"{base_url}/api/v1/reviews", json={"chat_id": chat_id, "rating": 4, "body": "ok"}, headers=auth_headers, timeout=TIMEOUT, ) # Reviews about second_user (the one we chatted with) # The reviewer is new_user (auth_headers); the reviewed_id is second_user r = requests.get( f"{base_url}/api/v1/users/{second_user['user']['id']}/reviews", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 body = r.json() assert body["count"] >= 1 assert any(rev.get("rating") == 4 for rev in body["reviews"]) def test_list_reviews_bad_uuid(self, base_url, auth_headers): r = requests.get( f"{base_url}/api/v1/users/not-a-uuid/reviews", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400 def test_user_stats(self, base_url, auth_headers, second_user): r = requests.get( f"{base_url}/api/v1/users/{second_user['user']['id']}/stats", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 200 body = r.json() # stats object should have count and avg fields assert "count" in body or "rating_avg" in body or "avg" in body or body == {} def test_user_stats_bad_uuid(self, base_url, auth_headers): r = requests.get( f"{base_url}/api/v1/users/bad/stats", headers=auth_headers, timeout=TIMEOUT, ) assert r.status_code == 400