You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1540 lines
68 KiB
1540 lines
68 KiB
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
from tempfile import mkdtemp
|
|
from uuid import uuid4
|
|
|
|
test_db_dir = Path(mkdtemp(prefix="dating-app-tests-"))
|
|
test_db_path = test_db_dir / "test.db"
|
|
os.environ["DATABASE_URL"] = f"sqlite:///{test_db_path.as_posix()}"
|
|
from fastapi.testclient import TestClient
|
|
import pytest
|
|
|
|
from app.database import SessionLocal, engine
|
|
from app.main import app
|
|
from app.models import Match, Swipe, User, UserStatus
|
|
from app.storage import StorageService
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def cleanup_test_database():
|
|
yield
|
|
engine.dispose()
|
|
shutil.rmtree(test_db_dir, ignore_errors=True)
|
|
|
|
|
|
def register_user(
|
|
client,
|
|
*,
|
|
full_name=None,
|
|
bio=None,
|
|
city=None,
|
|
country=None,
|
|
interests=None,
|
|
relationship_goal="serious_relationship",
|
|
):
|
|
email = f"tester-{uuid4().hex[:10]}@example.com"
|
|
password = "password123"
|
|
response = client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": email,
|
|
"password": password,
|
|
"confirm_password": password,
|
|
"relationship_goal": relationship_goal,
|
|
"full_name": full_name,
|
|
"bio": bio,
|
|
"city": city,
|
|
"country": country,
|
|
"interests": interests,
|
|
})
|
|
assert response.status_code == 201
|
|
token = response.json()["access_token"]
|
|
me_response = client.get("/api/v1/auth/me", headers={"Authorization": f"Bearer {token}"})
|
|
assert me_response.status_code == 200
|
|
return me_response.json()["id"], token, email, password
|
|
|
|
|
|
def test_private_oss_media_uses_browser_ready_signed_url():
|
|
calls = []
|
|
|
|
class FakeBucket:
|
|
def sign_url(self, method, key, expires):
|
|
calls.append((method, key, expires))
|
|
return f"https://oss.example.test/{key}?signed=1"
|
|
|
|
service = StorageService.__new__(StorageService)
|
|
service.enabled = True
|
|
service.bucket = FakeBucket()
|
|
|
|
assert service.access_url("oss://melove/users/2/avatar.jpg", "/api/v1/media/20") == (
|
|
"https://oss.example.test/melove/users/2/avatar.jpg?signed=1"
|
|
)
|
|
assert calls == [("GET", "melove/users/2/avatar.jpg", 3600)]
|
|
|
|
|
|
def test_register_login_and_profile_without_otp(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
email = f"tester-{uuid4().hex[:8]}@example.com"
|
|
with TestClient(app) as client:
|
|
response = client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": email,
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "serious_relationship",
|
|
"full_name": "测试用户",
|
|
})
|
|
assert response.status_code == 201
|
|
registration_response = response.json()
|
|
assert set(registration_response) == {"access_token", "refresh_token", "token_type"}
|
|
assert registration_response["refresh_token"]
|
|
token = registration_response["access_token"]
|
|
login = client.post("/api/v1/auth/login", json={"email_or_phone": email, "password": "password123"})
|
|
assert login.status_code == 200
|
|
assert login.json()["user"]["email_or_phone"] == email
|
|
updated = client.patch(
|
|
"/api/v1/users/me",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
json={"bio": "喜欢徒步", "relationship_goal": "casual_dates"},
|
|
)
|
|
assert updated.status_code == 200
|
|
assert updated.json()["profile"]["bio"] == "喜欢徒步"
|
|
assert updated.json()["profile"]["relationship_goal"] == "casual_dates"
|
|
assert client.patch(
|
|
"/api/v1/users/me",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
json={"relationship_goal": None},
|
|
).status_code == 422
|
|
|
|
|
|
def test_update_profile_details_and_return_in_get_me():
|
|
with TestClient(app) as client:
|
|
_, token, _, _ = register_user(client, full_name="Detail User", city="Shanghai")
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
details = {
|
|
"occupation": "Creative Director at Studio Drift",
|
|
"education": "NYU Gallatin School",
|
|
"height_cm": 175,
|
|
"beliefs": "spiritual_agnostic",
|
|
"smoking": "non_smoker",
|
|
"drinking": "socially",
|
|
"pets": "golden_retriever",
|
|
"exercise": "active_4x_week",
|
|
}
|
|
updated = client.patch("/api/v1/users/me", headers=headers, json=details)
|
|
assert updated.status_code == 200
|
|
profile = updated.json()["profile"]
|
|
for key, value in details.items():
|
|
assert profile[key] == value
|
|
|
|
me = client.get("/api/v1/auth/me", headers=headers)
|
|
assert me.status_code == 200
|
|
me_profile = me.json()["profile"]
|
|
for key, value in details.items():
|
|
assert me_profile[key] == value
|
|
assert me_profile["age"] is None
|
|
|
|
assert client.patch(
|
|
"/api/v1/users/me", headers=headers, json={"birth_date": "2000-01-01"},
|
|
).status_code == 200
|
|
me_profile = client.get("/api/v1/auth/me", headers=headers).json()["profile"]
|
|
assert me_profile["age"] == 26
|
|
|
|
assert client.patch(
|
|
"/api/v1/users/me", headers=headers, json={"height_cm": 300},
|
|
).status_code == 422
|
|
|
|
|
|
def test_registration_supports_bio_or_followup_video(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
_, bio_token, _, _ = register_user(client, bio=" I prefer a written introduction. ")
|
|
bio_headers = {"Authorization": f"Bearer {bio_token}"}
|
|
bio_profile = client.get("/api/v1/auth/me", headers=bio_headers).json()["profile"]
|
|
assert bio_profile["bio"] == "I prefer a written introduction."
|
|
assert bio_profile["video_url"] is None
|
|
|
|
bio_to_video = client.post(
|
|
"/api/v1/users/me/video",
|
|
headers=bio_headers,
|
|
files={"video": ("bio-replacement.mp4", b"bio-replacement-video", "video/mp4")},
|
|
)
|
|
assert bio_to_video.status_code == 200
|
|
replaced_bio_profile = client.get("/api/v1/auth/me", headers=bio_headers).json()["profile"]
|
|
assert replaced_bio_profile["bio"] is None
|
|
assert replaced_bio_profile["video_url"] == f"/api/v1/media/{bio_to_video.json()['id']}"
|
|
|
|
_, video_token, _, _ = register_user(client)
|
|
video_headers = {"Authorization": f"Bearer {video_token}"}
|
|
before_upload = client.get("/api/v1/auth/me", headers=video_headers).json()["profile"]
|
|
assert before_upload["bio"] is None
|
|
assert before_upload["video_url"] is None
|
|
|
|
uploaded = client.post(
|
|
"/api/v1/users/me/video",
|
|
headers=video_headers,
|
|
files={"video": ("introduction.mp4", b"intro-video", "video/mp4")},
|
|
)
|
|
assert uploaded.status_code == 200
|
|
after_upload = client.get("/api/v1/auth/me", headers=video_headers).json()["profile"]
|
|
assert after_upload["bio"] is None
|
|
assert after_upload["video_url"] == f"/api/v1/media/{uploaded.json()['id']}"
|
|
|
|
video_to_bio = client.patch(
|
|
"/api/v1/users/me",
|
|
headers=video_headers,
|
|
json={"bio": "I switched to a written introduction."},
|
|
)
|
|
assert video_to_bio.status_code == 200
|
|
assert video_to_bio.json()["profile"]["bio"] == "I switched to a written introduction."
|
|
assert video_to_bio.json()["profile"]["video_url"] is None
|
|
assert client.get(f"/api/v1/media/{uploaded.json()['id']}", headers=video_headers).status_code == 404
|
|
|
|
blank_bio = client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": f"blank-bio-{uuid4().hex[:8]}@example.com",
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "serious_relationship",
|
|
"bio": " ",
|
|
})
|
|
assert blank_bio.status_code == 422
|
|
|
|
|
|
def test_profile_image_and_video_uploads_replace_existing(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
_, token, _, _ = register_user(client)
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
first_image = client.post(
|
|
"/api/v1/users/me/image",
|
|
headers=headers,
|
|
files={"image": ("first.png", b"first-image", "image/png")},
|
|
)
|
|
second_image = client.post(
|
|
"/api/v1/users/me/image",
|
|
headers=headers,
|
|
files={"image": ("second.webp", b"second-image", "image/webp")},
|
|
)
|
|
assert first_image.status_code == second_image.status_code == 200
|
|
assert first_image.json()["id"] != second_image.json()["id"]
|
|
assert client.get(f"/api/v1/media/{first_image.json()['id']}", headers=headers).status_code == 404
|
|
current_image = client.get(f"/api/v1/media/{second_image.json()['id']}", headers=headers)
|
|
assert current_image.status_code == 200
|
|
assert current_image.content == b"second-image"
|
|
|
|
first_video = client.post(
|
|
"/api/v1/users/me/video",
|
|
headers=headers,
|
|
files={"video": ("first.mp4", b"first-video", "video/mp4")},
|
|
)
|
|
second_video = client.post(
|
|
"/api/v1/users/me/video",
|
|
headers=headers,
|
|
files={"video": ("second.webm", b"second-video", "video/webm")},
|
|
)
|
|
assert first_video.status_code == second_video.status_code == 200
|
|
assert first_video.json()["id"] != second_video.json()["id"]
|
|
assert client.get(f"/api/v1/media/{first_video.json()['id']}", headers=headers).status_code == 404
|
|
current_video = client.get(f"/api/v1/media/{second_video.json()['id']}", headers=headers)
|
|
assert current_video.status_code == 200
|
|
assert current_video.content == b"second-video"
|
|
profile = client.get("/api/v1/auth/me", headers=headers).json()["profile"]
|
|
assert profile["profile_image_url"] == f"/api/v1/media/{second_image.json()['id']}"
|
|
assert profile["video_url"] == f"/api/v1/media/{second_video.json()['id']}"
|
|
|
|
stored_files = [path for path in tmp_path.rglob("*") if path.is_file()]
|
|
assert sorted(path.read_bytes() for path in stored_files) == [b"second-image", b"second-video"]
|
|
|
|
|
|
def test_profile_media_replacement_keeps_existing_file_when_storage_fails(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
_, token, _, _ = register_user(client)
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
existing = client.post(
|
|
"/api/v1/users/me/image",
|
|
headers=headers,
|
|
files={"image": ("existing.png", b"existing-image", "image/png")},
|
|
)
|
|
assert existing.status_code == 200
|
|
|
|
def fail_storage(_local_path, _relative_key):
|
|
raise RuntimeError("storage unavailable")
|
|
|
|
monkeypatch.setattr("app.main.storage.finalize", fail_storage)
|
|
replacement = client.post(
|
|
"/api/v1/users/me/image",
|
|
headers=headers,
|
|
files={"image": ("replacement.png", b"replacement-image", "image/png")},
|
|
)
|
|
assert replacement.status_code == 503
|
|
|
|
current = client.get(f"/api/v1/media/{existing.json()['id']}", headers=headers)
|
|
assert current.status_code == 200
|
|
assert current.content == b"existing-image"
|
|
stored_files = [path for path in tmp_path.rglob("*") if path.is_file()]
|
|
assert len(stored_files) == 1
|
|
assert stored_files[0].read_bytes() == b"existing-image"
|
|
|
|
|
|
def test_duplicate_registration():
|
|
payload = {
|
|
"email_or_phone": f"138{uuid4().int % 10**8:08d}",
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "serious_relationship",
|
|
}
|
|
with TestClient(app) as client:
|
|
assert client.post("/api/v1/auth/register", json=payload).status_code == 201
|
|
assert client.post("/api/v1/auth/register", json=payload).status_code == 409
|
|
|
|
|
|
def test_email_is_case_insensitive_and_new_user_is_active():
|
|
email = f"ACTIVE-{uuid4().hex[:10]}@EXAMPLE.COM"
|
|
payload = {
|
|
"email_or_phone": email,
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "serious_relationship",
|
|
}
|
|
with TestClient(app) as client:
|
|
registered = client.post("/api/v1/auth/register", json=payload)
|
|
assert registered.status_code == 201
|
|
response = client.post("/api/v1/auth/login", json={
|
|
"email_or_phone": email.lower(),
|
|
"password": payload["password"],
|
|
})
|
|
assert response.status_code == 200
|
|
active_headers = {"Authorization": f"Bearer {registered.json()['access_token']}"}
|
|
assert client.get("/api/v1/meet", headers=active_headers).status_code == 200
|
|
assert client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": email.lower(),
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "serious_relationship",
|
|
}).status_code == 409
|
|
|
|
|
|
def test_phone_login_and_invalid_password():
|
|
phone = f"139{uuid4().int % 10**8:08d}"
|
|
with TestClient(app) as client:
|
|
assert client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": phone,
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "serious_relationship",
|
|
}).status_code == 201
|
|
assert client.post("/api/v1/auth/login", json={
|
|
"email_or_phone": phone,
|
|
"password": "password123",
|
|
}).status_code == 200
|
|
assert client.post("/api/v1/auth/login", json={
|
|
"email_or_phone": phone,
|
|
"password": "wrong-password",
|
|
}).status_code == 401
|
|
|
|
|
|
def test_auth_validation_and_otp_endpoints_are_removed():
|
|
with TestClient(app) as client:
|
|
assert client.post("/api/v1/auth/register", json={"password": "password123"}).status_code == 422
|
|
assert client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": "missing-confirm@example.com",
|
|
"password": "password123",
|
|
}).status_code == 422
|
|
assert client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": "mismatch-confirm@example.com",
|
|
"password": "password123",
|
|
"confirm_password": "password124",
|
|
"relationship_goal": "serious_relationship",
|
|
}).status_code == 422
|
|
assert client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": "missing-goal@example.com",
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
}).status_code == 422
|
|
assert client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": "invalid-goal@example.com",
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "long_term",
|
|
}).status_code == 422
|
|
assert client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": " ",
|
|
"password": "password123",
|
|
}).status_code == 422
|
|
assert client.post("/api/v1/auth/login", json={
|
|
"email_or_phone": "user@example.com",
|
|
}).status_code == 422
|
|
assert client.post("/api/v1/auth/request-code", json={}).status_code == 404
|
|
assert client.post("/api/v1/auth/verify", json={}).status_code == 404
|
|
|
|
|
|
def test_interests_support_registration_custom_values_and_unlimited_count():
|
|
email = f"interests-{uuid4().hex[:10]}@example.com"
|
|
interests = ["Photography", "Dogs", "Travel", "Music", "Gaming", "Fitness", "Cooking", "我的自定义兴趣"]
|
|
with TestClient(app) as client:
|
|
registered = client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": email,
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "new_friends",
|
|
"gender": "Man",
|
|
"interests": interests,
|
|
})
|
|
assert registered.status_code == 201
|
|
headers = {"Authorization": f"Bearer {registered.json()['access_token']}"}
|
|
me = client.get("/api/v1/users/me", headers=headers)
|
|
assert me.status_code == 200
|
|
assert me.json()["profile"]["relationship_goal"] == "new_friends"
|
|
assert me.json()["profile"]["interests"] == interests
|
|
|
|
updated_interests = interests + ["Board Games", "Hiking", "Coffee", "Art", "Reading"]
|
|
updated = client.put("/api/v1/users/me/interests", headers=headers, json={"interests": updated_interests})
|
|
assert updated.status_code == 200
|
|
assert updated.json()["interests"] == updated_interests
|
|
listed = client.get("/api/v1/users/me/interests", headers=headers)
|
|
assert listed.json()["interests"] == updated_interests
|
|
|
|
|
|
def test_interests_require_two_non_empty_unique_values():
|
|
email = f"interest-validation-{uuid4().hex[:10]}@example.com"
|
|
with TestClient(app) as client:
|
|
registered = client.post("/api/v1/auth/register", json={
|
|
"email_or_phone": email,
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "casual_dates",
|
|
})
|
|
headers = {"Authorization": f"Bearer {registered.json()['access_token']}"}
|
|
assert client.put("/api/v1/users/me/interests", headers=headers, json={"interests": ["Only one"]}).status_code == 422
|
|
assert client.put("/api/v1/users/me/interests", headers=headers, json={"interests": ["Travel", " "]}).status_code == 422
|
|
duplicate = client.put("/api/v1/users/me/interests", headers=headers, json={"interests": ["Travel", "travel", "Music"]})
|
|
assert duplicate.status_code == 200
|
|
assert duplicate.json()["interests"] == ["Travel", "Music"]
|
|
|
|
|
|
def test_refresh_me_and_logout_session():
|
|
email = f"session-{uuid4().hex[:10]}@example.com"
|
|
with TestClient(app) as client:
|
|
registered = client.post(
|
|
"/api/v1/auth/register",
|
|
json={
|
|
"email_or_phone": email,
|
|
"password": "password123",
|
|
"confirm_password": "password123",
|
|
"relationship_goal": "marriage",
|
|
"full_name": "Session User",
|
|
},
|
|
)
|
|
assert registered.status_code == 201
|
|
initial = registered.json()
|
|
assert initial["refresh_token"]
|
|
refreshed = client.post(
|
|
"/api/v1/auth/refresh",
|
|
json={"refresh_token": initial["refresh_token"]},
|
|
)
|
|
assert refreshed.status_code == 200
|
|
rotated = refreshed.json()
|
|
assert rotated["refresh_token"] != initial["refresh_token"]
|
|
assert client.post(
|
|
"/api/v1/auth/refresh",
|
|
json={"refresh_token": initial["refresh_token"]},
|
|
).status_code == 401
|
|
|
|
me_headers = {"Authorization": f"Bearer {rotated['access_token']}"}
|
|
me = client.get("/api/v1/auth/me", headers=me_headers)
|
|
assert me.status_code == 200
|
|
assert me.json()["email_or_phone"] == email
|
|
assert client.get("/api/v1/users/me", headers=me_headers).status_code == 200
|
|
|
|
assert client.post(
|
|
"/api/v1/auth/logout",
|
|
json={"refresh_token": rotated["refresh_token"]},
|
|
).status_code == 200
|
|
assert client.post(
|
|
"/api/v1/auth/refresh",
|
|
json={"refresh_token": rotated["refresh_token"]},
|
|
).status_code == 401
|
|
|
|
|
|
def test_meet_match_cancel_and_search(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
first_id, first_token, _, _ = register_user(client, full_name="Alice", city="上海")
|
|
second_id, second_token, _, _ = register_user(
|
|
client,
|
|
full_name="Bob Zhang",
|
|
bio="喜欢徒步和安静的周末",
|
|
city="上海",
|
|
)
|
|
first_headers = {"Authorization": f"Bearer {first_token}"}
|
|
second_headers = {"Authorization": f"Bearer {second_token}"}
|
|
|
|
assert client.put(
|
|
"/api/v1/users/me/location",
|
|
headers=first_headers,
|
|
json={"latitude": 31.2304, "longitude": 121.4737, "city": "上海", "county": "黄浦区"},
|
|
).status_code == 200
|
|
assert client.put(
|
|
"/api/v1/users/me/location",
|
|
headers=second_headers,
|
|
json={"latitude": 31.2310, "longitude": 121.4740, "city": "上海", "county": "静安区"},
|
|
).status_code == 200
|
|
|
|
upload = client.post(
|
|
"/api/v1/users/me/image",
|
|
headers=second_headers,
|
|
files={"image": ("avatar.png", b"fake-image", "image/png")},
|
|
)
|
|
assert upload.status_code == 200
|
|
image_id = upload.json()["id"]
|
|
|
|
identity = client.post(
|
|
"/api/v1/users/me/identity",
|
|
headers=second_headers,
|
|
files={"document": ("passport.png", b"private", "image/png")},
|
|
)
|
|
assert identity.status_code == 200
|
|
identity_id = identity.json()["id"]
|
|
assert client.get(f"/api/v1/media/{image_id}", headers=first_headers).status_code == 200
|
|
assert client.get(f"/api/v1/media/{identity_id}", headers=first_headers).status_code == 403
|
|
|
|
monkeypatch.setattr(
|
|
"app.main.storage.access_url",
|
|
lambda _storage_path, fallback_url: f"https://media.example.test{fallback_url}?signed=1",
|
|
)
|
|
|
|
search = client.get(
|
|
"/api/v1/search",
|
|
headers=first_headers,
|
|
params={"q": "Bob 徒步", "max_distance_km": 5},
|
|
)
|
|
assert search.status_code == 200
|
|
bob = next(item for item in search.json() if item["id"] == second_id)
|
|
assert bob["distance_km"] < 1
|
|
assert [item["id"] for item in bob["media"]] == [image_id]
|
|
expected_image_url = f"https://media.example.test/api/v1/media/{image_id}?signed=1"
|
|
assert bob["profile_image_url"] == expected_image_url
|
|
assert bob["media"][0]["url"] == expected_image_url
|
|
|
|
candidates = client.get("/api/v1/meet", headers=first_headers)
|
|
assert any(item["id"] == second_id for item in candidates.json())
|
|
first_swipe = client.post(
|
|
"/api/v1/meet/swipes",
|
|
headers=first_headers,
|
|
json={"target_user_id": second_id, "direction": "like"},
|
|
)
|
|
assert first_swipe.status_code == 200
|
|
assert first_swipe.json()["matched"] is False
|
|
|
|
second_swipe = client.post(
|
|
"/api/v1/meet/swipes",
|
|
headers=second_headers,
|
|
json={"target_user_id": first_id, "direction": "like"},
|
|
)
|
|
assert second_swipe.status_code == 200
|
|
assert second_swipe.json()["matched"] is True
|
|
assert len(client.get("/api/v1/matches", headers=first_headers).json()) == 1
|
|
|
|
cancel = client.delete(f"/api/v1/meet/likes/{second_id}", headers=first_headers)
|
|
assert cancel.status_code == 200
|
|
assert client.get("/api/v1/matches", headers=first_headers).json() == []
|
|
|
|
|
|
def test_profile_activity_posts_like_lists_matches_and_swipe_undo(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
alice_id, alice_token, _, _ = register_user(client, full_name="Activity Alice", city="Shanghai")
|
|
bob_id, bob_token, _, _ = register_user(client, full_name="Activity Bob", city="Shanghai")
|
|
carol_id, _, _, _ = register_user(client, full_name="Activity Carol", city="Beijing")
|
|
alice_headers = {"Authorization": f"Bearer {alice_token}"}
|
|
bob_headers = {"Authorization": f"Bearer {bob_token}"}
|
|
|
|
text_post = client.post(
|
|
"/api/v1/explore/posts",
|
|
headers=alice_headers,
|
|
json={"content": "My text post", "post_type": "text"},
|
|
)
|
|
assert text_post.status_code == 201
|
|
image_post = client.post(
|
|
"/api/v1/explore/posts",
|
|
headers=alice_headers,
|
|
json={"content": "My image post", "post_type": "image"},
|
|
)
|
|
assert image_post.status_code == 201
|
|
uploaded = client.post(
|
|
f"/api/v1/explore/posts/{image_post.json()['id']}/media",
|
|
headers=alice_headers,
|
|
files={"media": ("post.png", b"post-image", "image/png")},
|
|
)
|
|
assert uploaded.status_code == 201
|
|
monkeypatch.setattr(
|
|
"app.main.storage.access_url",
|
|
lambda _storage_path, fallback_url: f"https://media.example.test{fallback_url}?signed=1",
|
|
)
|
|
|
|
assert client.post(
|
|
"/api/v1/meet/swipes",
|
|
headers=bob_headers,
|
|
json={"target_user_id": alice_id, "direction": "like"},
|
|
).status_code == 200
|
|
matched = client.post(
|
|
"/api/v1/meet/swipes",
|
|
headers=alice_headers,
|
|
json={"target_user_id": bob_id, "direction": "like"},
|
|
)
|
|
assert matched.status_code == 200
|
|
assert matched.json()["matched"] is True
|
|
|
|
assert client.post(
|
|
"/api/v1/meet/swipes",
|
|
headers=alice_headers,
|
|
json={"target_user_id": carol_id, "direction": "like"},
|
|
).status_code == 200
|
|
|
|
me = client.get("/api/v1/auth/me", headers=alice_headers)
|
|
assert me.status_code == 200
|
|
me_body = me.json()
|
|
assert [post["id"] for post in me_body["posts"]] == [image_post.json()["id"], text_post.json()["id"]]
|
|
assert me_body["posts"][0]["media"][0]["url"] == (
|
|
f"https://media.example.test/api/v1/explore/media/{uploaded.json()['id']}?signed=1"
|
|
)
|
|
assert [item["id"] for item in me_body["liked_users"]] == [carol_id]
|
|
assert me_body["liked_by_users"] == []
|
|
assert [item["user"]["id"] for item in me_body["matches"]] == [bob_id]
|
|
assert me_body["liked_users_count"] == 1
|
|
assert me_body["liked_by_users_count"] == 0
|
|
assert me_body["matches_count"] == 1
|
|
|
|
my_posts = client.get("/api/v1/users/me/posts?limit=1", headers=alice_headers)
|
|
assert my_posts.status_code == 200
|
|
assert [item["id"] for item in my_posts.json()] == [image_post.json()["id"]]
|
|
public_posts = client.get(f"/api/v1/users/{alice_id}/posts", headers=bob_headers)
|
|
assert public_posts.status_code == 200
|
|
assert [item["id"] for item in public_posts.json()] == [image_post.json()["id"], text_post.json()["id"]]
|
|
public_profile = client.get(f"/api/v1/users/{alice_id}", headers=bob_headers)
|
|
assert public_profile.status_code == 200
|
|
assert public_profile.json()["full_name"] == "Activity Alice"
|
|
assert public_profile.json()["relationship_goal"] == "serious_relationship"
|
|
|
|
assert [item["id"] for item in client.get(
|
|
"/api/v1/users/me/liked", headers=alice_headers
|
|
).json()] == [carol_id]
|
|
assert client.get("/api/v1/users/me/likes", headers=alice_headers).json() == []
|
|
assert [item["user"]["id"] for item in client.get(
|
|
"/api/v1/users/me/matches", headers=alice_headers
|
|
).json()] == [bob_id]
|
|
assert [item["user"]["id"] for item in client.get(
|
|
"/api/v1/users/me/matches?limit=1&offset=20", headers=alice_headers
|
|
).json()] == [bob_id]
|
|
assert [item["user"]["id"] for item in client.get(
|
|
"/api/v1/matches?limit=1&offset=20", headers=alice_headers
|
|
).json()] == [bob_id]
|
|
|
|
undo_like = client.delete(f"/api/v1/meet/swipes/{carol_id}", headers=alice_headers)
|
|
assert undo_like.status_code == 200
|
|
assert undo_like.json()["previous_direction"] == "like"
|
|
assert carol_id in [item["id"] for item in client.get(
|
|
"/api/v1/meet?limit=50", headers=alice_headers
|
|
).json()]
|
|
|
|
passed = client.post(
|
|
"/api/v1/meet/swipes",
|
|
headers=alice_headers,
|
|
json={"target_user_id": carol_id, "direction": "pass"},
|
|
)
|
|
assert passed.status_code == 200
|
|
assert carol_id not in [item["id"] for item in client.get(
|
|
"/api/v1/meet?limit=50", headers=alice_headers
|
|
).json()]
|
|
undo_pass = client.delete(f"/api/v1/meet/swipes/{carol_id}", headers=alice_headers)
|
|
assert undo_pass.status_code == 200
|
|
assert undo_pass.json()["previous_direction"] == "pass"
|
|
|
|
undo_match_like = client.delete(f"/api/v1/meet/swipes/{bob_id}", headers=alice_headers)
|
|
assert undo_match_like.status_code == 200
|
|
assert client.get("/api/v1/matches", headers=alice_headers).json() == []
|
|
after_undo = client.get("/api/v1/auth/me", headers=alice_headers).json()
|
|
assert after_undo["liked_users_count"] == 0
|
|
assert after_undo["liked_by_users_count"] == 1
|
|
assert after_undo["matches_count"] == 0
|
|
assert [item["id"] for item in client.get(
|
|
"/api/v1/users/me/likes", headers=alice_headers
|
|
).json()] == [bob_id]
|
|
|
|
|
|
def test_profile_activity_counts_exceed_list_limit_and_exclude_suspended_users():
|
|
with TestClient(app) as client:
|
|
owner_id, token, _, _ = register_user(client)
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
with SessionLocal() as db:
|
|
targets = [User(status=UserStatus.active) for _ in range(22)]
|
|
targets[-1].status = UserStatus.suspended
|
|
db.add_all(targets)
|
|
db.flush()
|
|
db.add_all([
|
|
Swipe(actor_id=owner_id, target_id=target.id, direction="like")
|
|
for target in targets
|
|
])
|
|
db.add_all([
|
|
Swipe(actor_id=target.id, target_id=owner_id, direction="super_like")
|
|
for target in targets[:-1]
|
|
])
|
|
db.add(Match(user1_id=min(owner_id, targets[0].id),
|
|
user2_id=max(owner_id, targets[0].id)))
|
|
db.commit()
|
|
|
|
for path in ("/api/v1/auth/me", "/api/v1/users/me"):
|
|
response = client.get(path, headers=headers)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert len(body["liked_users"]) == 20
|
|
assert body["liked_users_count"] == 20
|
|
assert body["liked_by_users_count"] == 20
|
|
assert body["matches_count"] == 1
|
|
|
|
for path in ("/api/v1/users/me/liked", "/api/v1/users/me/likes"):
|
|
response = client.get(path, headers=headers)
|
|
assert response.status_code == 200
|
|
assert len(response.json()) == 20
|
|
legacy_params = client.get(f"{path}?limit=1&offset=20", headers=headers)
|
|
assert legacy_params.status_code == 200
|
|
assert [item["id"] for item in legacy_params.json()] == [
|
|
item["id"] for item in response.json()
|
|
]
|
|
|
|
|
|
def test_location_updates_city_county_and_discovery_order():
|
|
suffix = uuid4().hex[:8]
|
|
old_city = f"Old-{suffix}"
|
|
new_city = f"New-{suffix}"
|
|
with TestClient(app) as client:
|
|
viewer_id, viewer_token, _, _ = register_user(client, city=old_city, country="China")
|
|
old_id, _, _, _ = register_user(client, city=old_city, country="China")
|
|
new_id, _, _, _ = register_user(client, city=new_city, country="China")
|
|
headers = {"Authorization": f"Bearer {viewer_token}"}
|
|
|
|
assert client.get("/api/v1/meet?limit=1", headers=headers).json()[0]["id"] == old_id
|
|
assert client.put("/api/v1/users/me/location", headers=headers, json={
|
|
"latitude": 31.2304, "longitude": 121.4737, "county": "Huangpu",
|
|
}).status_code == 422
|
|
assert client.put("/api/v1/users/me/location", headers=headers, json={
|
|
"latitude": 31.2304, "longitude": 121.4737,
|
|
"city": new_city, "county": " ",
|
|
}).status_code == 422
|
|
|
|
updated = client.put("/api/v1/users/me/location", headers=headers, json={
|
|
"latitude": 31.2304, "longitude": 121.4737,
|
|
"city": f" {new_city} ", "county": " Huangpu ",
|
|
})
|
|
assert updated.status_code == 200
|
|
assert updated.json()["city"] == new_city
|
|
assert updated.json()["county"] == "Huangpu"
|
|
assert updated.json()["latitude"] == 31.2304
|
|
me = client.get("/api/v1/auth/me", headers=headers).json()
|
|
assert me["profile"]["city"] == new_city
|
|
assert me["profile"]["county"] == "Huangpu"
|
|
assert me["profile"]["country"] == "China"
|
|
assert client.get(f"/api/v1/users/{viewer_id}", headers=headers).json()["county"] == "Huangpu"
|
|
assert client.get("/api/v1/meet?limit=1", headers=headers).json()[0]["id"] == new_id
|
|
|
|
changed_city = client.patch("/api/v1/users/me", headers=headers, json={"city": old_city})
|
|
assert changed_city.status_code == 200
|
|
assert changed_city.json()["profile"]["county"] is None
|
|
|
|
|
|
def test_super_like_priority_matching_and_cancel():
|
|
city = f"Super-like-city-{uuid4().hex[:8]}"
|
|
with TestClient(app) as client:
|
|
viewer_id, viewer_token, _, _ = register_user(client, city=city, country="China")
|
|
local_id, _, _, _ = register_user(client, city=city, country="China")
|
|
distant_id, distant_token, _, _ = register_user(client, city="Remote", country="Other")
|
|
viewer_headers = {"Authorization": f"Bearer {viewer_token}"}
|
|
distant_headers = {"Authorization": f"Bearer {distant_token}"}
|
|
viewer_meet = lambda: client.get("/api/v1/meet?limit=1", headers=viewer_headers).json()[0]["id"]
|
|
|
|
assert viewer_meet() == local_id
|
|
assert client.post("/api/v1/meet/swipes", headers=distant_headers, json={
|
|
"target_user_id": viewer_id, "direction": "super_liked",
|
|
}).status_code == 422
|
|
|
|
super_like = client.post("/api/v1/meet/swipes", headers=distant_headers, json={
|
|
"target_user_id": viewer_id, "direction": "super_like",
|
|
})
|
|
assert super_like.status_code == 200
|
|
assert super_like.json() == {"direction": "super_like", "matched": False, "match_id": None}
|
|
assert viewer_meet() == distant_id
|
|
assert [item["id"] for item in client.get(
|
|
"/api/v1/users/me/likes", headers=viewer_headers
|
|
).json()] == [distant_id]
|
|
assert [item["id"] for item in client.get(
|
|
"/api/v1/users/me/liked", headers=distant_headers
|
|
).json()] == [viewer_id]
|
|
|
|
matched = client.post("/api/v1/meet/swipes", headers=viewer_headers, json={
|
|
"target_user_id": distant_id, "direction": "like",
|
|
})
|
|
assert matched.status_code == 200
|
|
assert matched.json()["matched"] is True
|
|
assert len(client.get("/api/v1/matches", headers=viewer_headers).json()) == 1
|
|
|
|
canceled = client.delete(f"/api/v1/meet/swipes/{viewer_id}", headers=distant_headers)
|
|
assert canceled.status_code == 200
|
|
assert canceled.json()["previous_direction"] == "super_like"
|
|
assert client.get("/api/v1/matches", headers=viewer_headers).json() == []
|
|
assert client.delete(f"/api/v1/meet/swipes/{distant_id}", headers=viewer_headers).status_code == 200
|
|
assert viewer_meet() == local_id
|
|
|
|
assert client.post("/api/v1/meet/swipes", headers=distant_headers, json={
|
|
"target_user_id": viewer_id, "direction": "super_like",
|
|
}).status_code == 200
|
|
assert viewer_meet() == distant_id
|
|
assert client.post("/api/v1/meet/swipes", headers=distant_headers, json={
|
|
"target_user_id": viewer_id, "direction": "pass",
|
|
}).status_code == 200
|
|
assert viewer_meet() == local_id
|
|
assert client.delete(f"/api/v1/meet/swipes/{viewer_id}", headers=distant_headers).json()[
|
|
"previous_direction"
|
|
] == "pass"
|
|
|
|
assert client.post("/api/v1/meet/swipes", headers=distant_headers, json={
|
|
"target_user_id": viewer_id, "direction": "super_like",
|
|
}).status_code == 200
|
|
assert viewer_meet() == distant_id
|
|
assert client.post("/api/v1/meet/swipes", headers=distant_headers, json={
|
|
"target_user_id": viewer_id, "direction": "like",
|
|
}).status_code == 200
|
|
assert viewer_meet() == local_id
|
|
assert client.post("/api/v1/meet/swipes", headers=distant_headers, json={
|
|
"target_user_id": viewer_id, "direction": "super_like",
|
|
}).status_code == 200
|
|
assert client.delete(f"/api/v1/meet/likes/{viewer_id}", headers=distant_headers).status_code == 200
|
|
assert viewer_meet() == local_id
|
|
|
|
|
|
def test_discovery_country_compatibility_search_and_explore_order():
|
|
suffix = uuid4().hex[:8]
|
|
country = f"Sortland-{suffix}"
|
|
home_city = f"Home-{suffix}"
|
|
other_city = f"Other-{suffix}"
|
|
foreign_country = f"Elsewhere-{suffix}"
|
|
|
|
with TestClient(app) as client:
|
|
_, viewer_token, _, _ = register_user(
|
|
client,
|
|
full_name="Viewer",
|
|
city=home_city,
|
|
country=country,
|
|
interests=["Travel", "Music"],
|
|
)
|
|
high_id, high_token, _, _ = register_user(
|
|
client,
|
|
full_name="Same City High",
|
|
city=home_city,
|
|
country=country,
|
|
interests=["Travel", "Music"],
|
|
)
|
|
low_id, low_token, _, _ = register_user(
|
|
client,
|
|
full_name="Same City Low",
|
|
city=home_city,
|
|
country=country,
|
|
interests=["Cooking", "Art"],
|
|
)
|
|
country_id, country_token, _, _ = register_user(
|
|
client,
|
|
full_name="Same Country",
|
|
city=other_city,
|
|
country=country,
|
|
interests=["Travel", "Music"],
|
|
)
|
|
foreign_id, foreign_token, _, _ = register_user(
|
|
client,
|
|
full_name="Foreign",
|
|
city=other_city,
|
|
country=foreign_country,
|
|
interests=["Travel", "Music"],
|
|
)
|
|
viewer_headers = {"Authorization": f"Bearer {viewer_token}"}
|
|
|
|
meet = client.get("/api/v1/meet", headers=viewer_headers, params={"limit": 50})
|
|
assert meet.status_code == 200
|
|
meet_items = {item["id"]: item for item in meet.json()}
|
|
assert meet_items[high_id]["country"] == country
|
|
assert meet_items[high_id]["compatibility_percentage"] == 100
|
|
assert meet_items[low_id]["compatibility_percentage"] == 0
|
|
ordered_ids = [item["id"] for item in meet.json()]
|
|
relevant_ids = [item for item in ordered_ids if item in {high_id, low_id, country_id, foreign_id}]
|
|
assert relevant_ids == [high_id, low_id, country_id, foreign_id]
|
|
|
|
|
|
city_and_country = client.get(
|
|
"/api/v1/search",
|
|
headers=viewer_headers,
|
|
params={"city": f" {home_city.lower()} ", "country": country.lower(), "limit": 50},
|
|
)
|
|
assert city_and_country.status_code == 200
|
|
assert [item["id"] for item in city_and_country.json()] == [high_id, low_id]
|
|
|
|
country_only = client.get(
|
|
"/api/v1/search",
|
|
headers=viewer_headers,
|
|
params={"country": country, "limit": 50},
|
|
)
|
|
assert country_only.status_code == 200
|
|
assert [item["id"] for item in country_only.json()] == [high_id, low_id, country_id]
|
|
|
|
post_ids = {}
|
|
for name, token in (
|
|
("high", high_token),
|
|
("low", low_token),
|
|
("country", country_token),
|
|
("foreign", foreign_token),
|
|
):
|
|
response = client.post(
|
|
"/api/v1/explore/posts",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
json={"content": f"Ordering post {name}", "post_type": "text"},
|
|
)
|
|
assert response.status_code == 201
|
|
post_ids[name] = response.json()["id"]
|
|
|
|
feed = client.get("/api/v1/explore/feed", headers=viewer_headers, params={"limit": 50})
|
|
assert feed.status_code == 200
|
|
relevant_posts = [
|
|
item["id"] for item in feed.json()
|
|
if item["id"] in set(post_ids.values())
|
|
]
|
|
assert relevant_posts == [
|
|
post_ids["high"],
|
|
post_ids["low"],
|
|
post_ids["country"],
|
|
post_ids["foreign"],
|
|
]
|
|
high_post = next(item for item in feed.json() if item["id"] == post_ids["high"])
|
|
assert high_post["author"]["compatibility_percentage"] == 100
|
|
assert high_post["author"]["country"] == country
|
|
assert client.get(
|
|
"/api/v1/explore/feed",
|
|
headers=viewer_headers,
|
|
params={"before_id": high_post["id"], "offset": 1},
|
|
).status_code == 422
|
|
|
|
|
|
def test_explore_post_like_comment_and_permissions():
|
|
with TestClient(app) as client:
|
|
author_id, author_token, _, _ = register_user(client, full_name="Post Author", city="上海")
|
|
reader_id, reader_token, _, _ = register_user(client, full_name="Post Reader", city="北京")
|
|
author_headers = {"Authorization": f"Bearer {author_token}"}
|
|
reader_headers = {"Authorization": f"Bearer {reader_token}"}
|
|
|
|
created = client.post(
|
|
"/api/v1/explore/posts",
|
|
headers=author_headers,
|
|
json={"content": "今天去公园散步,天气很好", "post_type": "text"},
|
|
)
|
|
assert created.status_code == 201
|
|
post = created.json()
|
|
assert post["author"]["id"] == author_id
|
|
assert post["like_count"] == 0
|
|
assert post["comment_count"] == 0
|
|
|
|
feed = client.get("/api/v1/explore/feed", headers=reader_headers)
|
|
assert feed.status_code == 200
|
|
assert any(item["id"] == post["id"] for item in feed.json())
|
|
|
|
liked = client.post(f"/api/v1/explore/posts/{post['id']}/like", headers=reader_headers)
|
|
assert liked.status_code == 200
|
|
assert liked.json()["like_count"] == 1
|
|
assert liked.json()["liked_by_me"] is True
|
|
repeated = client.post(f"/api/v1/explore/posts/{post['id']}/like", headers=reader_headers)
|
|
assert repeated.status_code == 200
|
|
assert repeated.json()["like_count"] == 1
|
|
|
|
comment = client.post(
|
|
f"/api/v1/explore/posts/{post['id']}/comments",
|
|
headers=reader_headers,
|
|
json={"content": "看起来很不错"},
|
|
)
|
|
assert comment.status_code == 201
|
|
assert comment.json()["author"]["id"] == reader_id
|
|
comments = client.get(f"/api/v1/explore/posts/{post['id']}/comments", headers=author_headers)
|
|
assert comments.status_code == 200
|
|
assert comments.json()[0]["content"] == "看起来很不错"
|
|
|
|
forbidden = client.delete(f"/api/v1/explore/posts/{post['id']}", headers=reader_headers)
|
|
assert forbidden.status_code == 403
|
|
assert client.delete(f"/api/v1/explore/posts/{post['id']}", headers=author_headers).status_code == 200
|
|
assert client.get(f"/api/v1/explore/posts/{post['id']}", headers=reader_headers).status_code == 404
|
|
|
|
|
|
def test_post_and_comment_author_avatars(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
monkeypatch.setattr(
|
|
"app.main.storage.access_url",
|
|
lambda _storage_path, fallback_url: f"https://media.example.test{fallback_url}?signed=1",
|
|
)
|
|
with TestClient(app) as client:
|
|
author_id, author_token, _, _ = register_user(client, full_name="Avatar Author")
|
|
_, reader_token, _, _ = register_user(client, full_name="Avatar Reader")
|
|
author_headers = {"Authorization": f"Bearer {author_token}"}
|
|
reader_headers = {"Authorization": f"Bearer {reader_token}"}
|
|
expected_urls = []
|
|
for headers in (author_headers, reader_headers):
|
|
uploaded = client.post(
|
|
"/api/v1/users/me/image", headers=headers,
|
|
files={"image": ("avatar.png", b"avatar", "image/png")},
|
|
)
|
|
assert uploaded.status_code == 200
|
|
expected_urls.append(f"https://media.example.test/api/v1/media/{uploaded.json()['id']}?signed=1")
|
|
|
|
post = client.post(
|
|
"/api/v1/explore/posts", headers=author_headers, json={"content": "Avatar test"},
|
|
).json()
|
|
post_id = post["id"]
|
|
assert post["author"]["profile_image_url"] == expected_urls[0]
|
|
for path in (
|
|
f"/api/v1/explore/posts/{post_id}", "/api/v1/explore/feed?limit=50",
|
|
f"/api/v1/users/{author_id}/posts", f"/api/v1/users/{author_id}", "/api/v1/auth/me",
|
|
):
|
|
headers = author_headers if path == "/api/v1/auth/me" else reader_headers
|
|
response = client.get(path, headers=headers)
|
|
assert response.status_code == 200
|
|
result = response.json()
|
|
if isinstance(result, dict) and "posts" in result:
|
|
result = result["posts"]
|
|
item = next(value for value in result if value["id"] == post_id) if isinstance(result, list) else result
|
|
assert item["author"]["profile_image_url"] == expected_urls[0]
|
|
|
|
comment = client.post(
|
|
f"/api/v1/explore/posts/{post_id}/comments", headers=reader_headers, json={"content": "Hello"},
|
|
)
|
|
assert comment.status_code == 201
|
|
assert comment.json()["author"]["profile_image_url"] == expected_urls[1]
|
|
comments = client.get(f"/api/v1/explore/posts/{post_id}/comments", headers=author_headers)
|
|
assert comments.json()[0]["author"]["profile_image_url"] == expected_urls[1]
|
|
|
|
|
|
def test_compact_post_likes_and_liker_list():
|
|
with TestClient(app) as client:
|
|
_, author_token, _, _ = register_user(client, full_name="Liker Author")
|
|
first_id, first_token, _, _ = register_user(client, full_name="First Liker")
|
|
second_id, second_token, _, _ = register_user(client)
|
|
author_headers = {"Authorization": f"Bearer {author_token}"}
|
|
first_headers = {"Authorization": f"Bearer {first_token}"}
|
|
second_headers = {"Authorization": f"Bearer {second_token}"}
|
|
post = client.post(
|
|
"/api/v1/explore/posts", headers=author_headers, json={"content": "Likers test"},
|
|
).json()
|
|
post_id = post["id"]
|
|
assert post["author"]["profile_image_url"] is None
|
|
assert post["liked_by_users"] == []
|
|
path = f"/api/v1/explore/posts/{post_id}/like"
|
|
assert client.get(path).status_code == 401
|
|
assert client.get("/api/v1/explore/posts/2147483647/like", headers=author_headers).status_code == 404
|
|
assert client.get(path, headers=author_headers).json() == []
|
|
|
|
first_liker = {"id": first_id, "full_name": "First Liker"}
|
|
second_liker = {"id": second_id, "full_name": None}
|
|
liked = client.post(path, headers=first_headers)
|
|
assert liked.status_code == 200
|
|
assert liked.json() == {
|
|
"message": "like successful", "like_count": 1, "liked_by_me": True,
|
|
"liked_by_users": [first_liker],
|
|
}
|
|
assert client.post(path, headers=first_headers).json() == liked.json()
|
|
second_like = client.post(path, headers=second_headers).json()
|
|
assert second_like["like_count"] == 2
|
|
assert second_like["liked_by_users"] == [second_liker, first_liker]
|
|
assert client.get(path, headers=author_headers).json() == [second_liker, first_liker]
|
|
detail = client.get(f"/api/v1/explore/posts/{post_id}", headers=first_headers).json()
|
|
assert detail["liked_by_users"] == [second_liker, first_liker]
|
|
assert detail["like_count"] == 2
|
|
assert detail["liked_by_me"] is True
|
|
|
|
unliked = client.delete(path, headers=first_headers).json()
|
|
assert unliked["message"] == "unlike successful"
|
|
assert "content" not in unliked
|
|
assert "media" not in unliked
|
|
assert unliked["liked_by_users"] == [second_liker]
|
|
assert unliked["like_count"] == 1
|
|
assert unliked["liked_by_me"] is False
|
|
assert client.delete(path, headers=first_headers).json()["message"] == "unlike successful"
|
|
client.post(path, headers=first_headers)
|
|
with SessionLocal() as db:
|
|
db.get(User, second_id).status = UserStatus.suspended
|
|
db.commit()
|
|
assert client.get(path, headers=author_headers).json() == [first_liker]
|
|
detail = client.get(f"/api/v1/explore/posts/{post_id}", headers=author_headers).json()
|
|
assert detail["liked_by_users"] == [first_liker]
|
|
assert detail["like_count"] == 1
|
|
assert detail["liked_by_me"] is False
|
|
feed = client.get("/api/v1/explore/feed?limit=50", headers=author_headers).json()
|
|
assert next(item for item in feed if item["id"] == post_id)["liked_by_users"] == [first_liker]
|
|
|
|
|
|
def test_comment_replies_and_likes():
|
|
with TestClient(app) as client:
|
|
_, author_token, _, _ = register_user(client, full_name="Comment Author")
|
|
_, reader_token, _, _ = register_user(client, full_name="Comment Reader")
|
|
author_headers = {"Authorization": f"Bearer {author_token}"}
|
|
reader_headers = {"Authorization": f"Bearer {reader_token}"}
|
|
post = client.post(
|
|
"/api/v1/explore/posts", headers=author_headers, json={"content": "Comment test"},
|
|
).json()
|
|
post_id = post["id"]
|
|
|
|
parent = client.post(
|
|
f"/api/v1/explore/posts/{post_id}/comments",
|
|
headers=reader_headers, json={"content": "Parent comment"},
|
|
)
|
|
assert parent.status_code == 201
|
|
parent_id = parent.json()["id"]
|
|
assert parent.json()["parent_comment_id"] is None
|
|
assert parent.json()["reply_count"] == 0
|
|
|
|
reply = client.post(
|
|
f"/api/v1/explore/posts/{post_id}/comments",
|
|
headers=author_headers,
|
|
json={"content": "Reply comment", "parent_comment_id": parent_id},
|
|
)
|
|
assert reply.status_code == 201
|
|
assert reply.json()["parent_comment_id"] == parent_id
|
|
|
|
assert client.post(
|
|
f"/api/v1/explore/posts/{post_id}/comments",
|
|
headers=author_headers,
|
|
json={"content": "Bad reply", "parent_comment_id": 2147483647},
|
|
).status_code == 404
|
|
|
|
comments = client.get(
|
|
f"/api/v1/explore/posts/{post_id}/comments", headers=reader_headers,
|
|
).json()
|
|
parent_view = next(item for item in comments if item["id"] == parent_id)
|
|
assert parent_view["reply_count"] == 1
|
|
assert parent_view["like_count"] == 0
|
|
|
|
like_path = f"/api/v1/explore/comments/{parent_id}/like"
|
|
liked = client.post(like_path, headers=author_headers)
|
|
assert liked.status_code == 200
|
|
assert liked.json() == {"message": "like successful", "like_count": 1, "liked_by_me": True}
|
|
assert client.post(like_path, headers=author_headers).json()["like_count"] == 1
|
|
|
|
parent_view = next(item for item in client.get(
|
|
f"/api/v1/explore/posts/{post_id}/comments", headers=reader_headers,
|
|
).json() if item["id"] == parent_id)
|
|
assert parent_view["like_count"] == 1
|
|
assert parent_view["liked_by_me"] is False
|
|
|
|
unliked = client.delete(like_path, headers=author_headers)
|
|
assert unliked.status_code == 200
|
|
assert unliked.json() == {"message": "unlike successful", "like_count": 0, "liked_by_me": False}
|
|
assert client.delete(like_path, headers=author_headers).json()["like_count"] == 0
|
|
|
|
|
|
def test_edit_post_and_comment(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
author_id, author_token, _, _ = register_user(client, full_name="Editor")
|
|
_, reader_token, _, _ = register_user(client, full_name="Reader")
|
|
author_headers = {"Authorization": f"Bearer {author_token}"}
|
|
reader_headers = {"Authorization": f"Bearer {reader_token}"}
|
|
|
|
post = client.post(
|
|
"/api/v1/explore/posts", headers=author_headers,
|
|
json={"content": "Old text", "post_type": "text"},
|
|
).json()
|
|
post_id = post["id"]
|
|
|
|
assert client.patch(
|
|
f"/api/v1/explore/posts/{post_id}", headers=reader_headers, json={"content": "Hacked"},
|
|
).status_code == 403
|
|
|
|
updated = client.patch(
|
|
f"/api/v1/explore/posts/{post_id}", headers=author_headers,
|
|
json={"content": "New text", "feeling": "happy", "location": {"name": "Park"}},
|
|
)
|
|
assert updated.status_code == 200
|
|
body = updated.json()
|
|
assert body["content"] == "New text"
|
|
assert body["feeling"] == "happy"
|
|
assert body["location"] == {"name": "Park", "latitude": None, "longitude": None}
|
|
assert body["content_format"] == "plain"
|
|
|
|
assert client.patch(
|
|
f"/api/v1/explore/posts/{post_id}", headers=author_headers,
|
|
json={"content": None},
|
|
).status_code == 422
|
|
assert client.patch(
|
|
f"/api/v1/explore/posts/{post_id}", headers=author_headers,
|
|
json={"content_format": None},
|
|
).status_code == 422
|
|
assert client.patch(
|
|
f"/api/v1/explore/posts/{post_id}", headers=author_headers,
|
|
json={"content": "", "feeling": None, "location": None},
|
|
).status_code == 422
|
|
|
|
comment = client.post(
|
|
f"/api/v1/explore/posts/{post_id}/comments", headers=reader_headers, json={"content": "Nice"},
|
|
).json()
|
|
comment_id = comment["id"]
|
|
assert client.patch(
|
|
f"/api/v1/explore/comments/{comment_id}", headers=author_headers, json={"content": "Stolen"},
|
|
).status_code == 403
|
|
assert client.patch(
|
|
"/api/v1/explore/comments/2147483647", headers=reader_headers, json={"content": "x"},
|
|
).status_code == 404
|
|
edited = client.patch(
|
|
f"/api/v1/explore/comments/{comment_id}", headers=reader_headers, json={"content": " Better "},
|
|
)
|
|
assert edited.status_code == 200
|
|
assert edited.json()["content"] == "Better"
|
|
assert client.patch(
|
|
f"/api/v1/explore/comments/{comment_id}", headers=reader_headers, json={"content": " "},
|
|
).status_code == 422
|
|
|
|
|
|
def test_post_supports_image_and_video_together(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
_, token, _, _ = register_user(client, full_name="Mixed Media")
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
post = client.post(
|
|
"/api/v1/explore/posts", headers=headers, json={"content": "Mixed post", "post_type": "text"},
|
|
).json()
|
|
post_id = post["id"]
|
|
|
|
image = client.post(
|
|
f"/api/v1/explore/posts/{post_id}/media", headers=headers,
|
|
files={"media": ("a.png", b"img1", "image/png")},
|
|
)
|
|
assert image.status_code == 201
|
|
video = client.post(
|
|
f"/api/v1/explore/posts/{post_id}/media", headers=headers,
|
|
files={"media": ("b.mp4", b"vid1", "video/mp4")},
|
|
)
|
|
assert video.status_code == 201
|
|
|
|
detail = client.get(f"/api/v1/explore/posts/{post_id}", headers=headers).json()
|
|
assert detail["post_type"] == "mixed"
|
|
assert {item["media_type"] for item in detail["media"]} == {"image", "video"}
|
|
|
|
second_video = client.post(
|
|
f"/api/v1/explore/posts/{post_id}/media", headers=headers,
|
|
files={"media": ("c.mp4", b"vid2", "video/mp4")},
|
|
)
|
|
assert second_video.status_code == 409
|
|
|
|
assert client.delete(
|
|
f"/api/v1/explore/media/{video.json()['id']}", headers=headers,
|
|
).status_code == 200
|
|
detail = client.get(f"/api/v1/explore/posts/{post_id}", headers=headers).json()
|
|
assert detail["post_type"] == "image"
|
|
assert {item["media_type"] for item in detail["media"]} == {"image"}
|
|
|
|
assert client.delete(
|
|
f"/api/v1/explore/media/{image.json()['id']}", headers=headers,
|
|
).status_code == 200
|
|
detail = client.get(f"/api/v1/explore/posts/{post_id}", headers=headers).json()
|
|
assert detail["post_type"] == "text"
|
|
assert detail["media"] == []
|
|
|
|
|
|
def test_post_composer_metadata_in_feed_and_public_posts(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
author_id, author_token, _, _ = register_user(client, city="Shanghai")
|
|
_, reader_token, _, _ = register_user(client, city="Shanghai")
|
|
author_headers = {"Authorization": f"Bearer {author_token}"}
|
|
reader_headers = {"Authorization": f"Bearer {reader_token}"}
|
|
|
|
for payload in (
|
|
{"content": " "},
|
|
{"content": "Hello", "feeling": " "},
|
|
{"content": "Hello", "location": {"name": " "}},
|
|
{"content": "Hello", "location": {"name": "Soho", "latitude": 40.72}},
|
|
{"content": "Hello", "location": {"name": "Soho", "latitude": 91, "longitude": 0}},
|
|
{"content": "Hello", "content_format": "html"},
|
|
):
|
|
assert client.post("/api/v1/explore/posts", headers=author_headers, json=payload).status_code == 422
|
|
|
|
created = client.post("/api/v1/explore/posts", headers=author_headers, json={
|
|
"content": "Feeling **happy** and *relaxed* today",
|
|
"content_format": "markdown",
|
|
"feeling": " happy ",
|
|
"location": {"name": " Blue Bottle Coffee, Soho NYC ", "latitude": 40.723, "longitude": -73.998},
|
|
})
|
|
assert created.status_code == 201
|
|
post = created.json()
|
|
assert post["content_format"] == "markdown"
|
|
assert post["feeling"] == "happy"
|
|
assert post["location"] == {
|
|
"name": "Blue Bottle Coffee, Soho NYC", "latitude": 40.723, "longitude": -73.998,
|
|
}
|
|
|
|
post_id = post["id"]
|
|
read_paths = (
|
|
f"/api/v1/explore/posts/{post_id}",
|
|
"/api/v1/explore/feed?limit=50",
|
|
f"/api/v1/users/{author_id}/posts",
|
|
"/api/v1/users/me/posts",
|
|
)
|
|
for path in read_paths:
|
|
headers = author_headers if path == "/api/v1/users/me/posts" else reader_headers
|
|
response = client.get(path, headers=headers)
|
|
assert response.status_code == 200
|
|
result = response.json()
|
|
item = result if isinstance(result, dict) else next(value for value in result if value["id"] == post_id)
|
|
assert item["content_format"] == "markdown"
|
|
assert item["feeling"] == "happy"
|
|
assert item["location"] == post["location"]
|
|
|
|
me = client.get("/api/v1/auth/me", headers=author_headers)
|
|
assert me.status_code == 200
|
|
assert next(item for item in me.json()["posts"] if item["id"] == post_id)["location"] == post["location"]
|
|
|
|
legacy = client.post("/api/v1/explore/posts", headers=author_headers, json={"content": "Plain post"})
|
|
assert legacy.status_code == 201
|
|
assert legacy.json()["content_format"] == "plain"
|
|
assert legacy.json()["feeling"] is None
|
|
assert legacy.json()["location"] is None
|
|
|
|
feeling_only = client.post("/api/v1/explore/posts", headers=author_headers, json={"feeling": "blessed"})
|
|
assert feeling_only.status_code == 201
|
|
assert feeling_only.json()["content"] == ""
|
|
|
|
image_post = client.post("/api/v1/explore/posts", headers=author_headers, json={
|
|
"post_type": "image", "feeling": "excited", "location": {"name": "Soho NYC"},
|
|
})
|
|
assert image_post.status_code == 201
|
|
uploaded = client.post(
|
|
f"/api/v1/explore/posts/{image_post.json()['id']}/media",
|
|
headers=author_headers,
|
|
files={"media": ("photo.png", b"image", "image/png")},
|
|
)
|
|
assert uploaded.status_code == 201
|
|
public_posts = client.get(f"/api/v1/users/{author_id}/posts", headers=reader_headers).json()
|
|
image_result = next(item for item in public_posts if item["id"] == image_post.json()["id"])
|
|
assert image_result["feeling"] == "excited"
|
|
assert image_result["location"] == {"name": "Soho NYC", "latitude": None, "longitude": None}
|
|
assert len(image_result["media"]) == 1
|
|
|
|
video_post = client.post("/api/v1/explore/posts", headers=author_headers, json={
|
|
"post_type": "video", "content": "A short introduction", "feeling": "happy",
|
|
})
|
|
assert video_post.status_code == 201
|
|
video_upload = client.post(
|
|
f"/api/v1/explore/posts/{video_post.json()['id']}/media",
|
|
headers=author_headers,
|
|
files={"media": ("clip.mp4", b"video", "video/mp4")},
|
|
)
|
|
assert video_upload.status_code == 201
|
|
public_posts = client.get(f"/api/v1/users/{author_id}/posts", headers=reader_headers).json()
|
|
video_result = next(item for item in public_posts if item["id"] == video_post.json()["id"])
|
|
assert video_result["post_type"] == "video"
|
|
assert video_result["feeling"] == "happy"
|
|
assert video_result["media"][0]["media_type"] == "video"
|
|
|
|
|
|
def test_chat_requires_match_and_supports_messages():
|
|
with TestClient(app) as client:
|
|
first_id, first_token, _, _ = register_user(client, full_name="Chat Alice")
|
|
second_id, second_token, _, _ = register_user(client, full_name="Chat Bob")
|
|
outsider_id, outsider_token, _, _ = register_user(client, full_name="Chat Outsider")
|
|
first_headers = {"Authorization": f"Bearer {first_token}"}
|
|
second_headers = {"Authorization": f"Bearer {second_token}"}
|
|
outsider_headers = {"Authorization": f"Bearer {outsider_token}"}
|
|
|
|
assert client.post(
|
|
"/api/v1/chat/conversations/999999",
|
|
headers=first_headers,
|
|
).status_code == 404
|
|
|
|
first_swipe = client.post(
|
|
"/api/v1/meet/swipes",
|
|
headers=first_headers,
|
|
json={"target_user_id": second_id, "direction": "like"},
|
|
)
|
|
assert first_swipe.status_code == 200
|
|
second_swipe = client.post(
|
|
"/api/v1/meet/swipes",
|
|
headers=second_headers,
|
|
json={"target_user_id": first_id, "direction": "like"},
|
|
)
|
|
match_id = second_swipe.json()["match_id"]
|
|
assert second_swipe.json()["matched"] is True
|
|
|
|
blocked = client.post(
|
|
f"/api/v1/chat/conversations/{match_id}",
|
|
headers=outsider_headers,
|
|
)
|
|
assert blocked.status_code == 404
|
|
|
|
conversation = client.post(
|
|
f"/api/v1/chat/conversations/{match_id}",
|
|
headers=first_headers,
|
|
)
|
|
assert conversation.status_code == 201
|
|
conversation_id = conversation.json()["id"]
|
|
again = client.post(
|
|
f"/api/v1/chat/conversations/{match_id}",
|
|
headers=second_headers,
|
|
)
|
|
assert again.status_code == 201
|
|
assert again.json()["id"] == conversation_id
|
|
|
|
sent = client.post(
|
|
f"/api/v1/chat/conversations/{conversation_id}/messages",
|
|
headers=first_headers,
|
|
json={"content": "你好,很高兴认识你"},
|
|
)
|
|
assert sent.status_code == 201
|
|
message_id = sent.json()["id"]
|
|
reply = client.post(
|
|
f"/api/v1/chat/conversations/{conversation_id}/messages",
|
|
headers=second_headers,
|
|
json={"content": "你好!"},
|
|
)
|
|
assert reply.status_code == 201
|
|
|
|
messages = client.get(
|
|
f"/api/v1/chat/conversations/{conversation_id}/messages",
|
|
headers=second_headers,
|
|
)
|
|
assert [item["content"] for item in messages.json()] == ["你好,很高兴认识你", "你好!"]
|
|
|
|
forbidden = client.delete(f"/api/v1/chat/messages/{message_id}", headers=second_headers)
|
|
assert forbidden.status_code == 403
|
|
recalled = client.delete(f"/api/v1/chat/messages/{message_id}", headers=first_headers)
|
|
assert recalled.status_code == 200
|
|
assert recalled.json()["is_deleted"] is True
|
|
assert recalled.json()["content"] is None
|
|
|
|
listed = client.get("/api/v1/chat/conversations", headers=first_headers)
|
|
assert listed.status_code == 200
|
|
assert listed.json()[0]["id"] == conversation_id
|
|
|
|
|
|
def test_explore_post_media_upload_access_and_cleanup(tmp_path, monkeypatch):
|
|
monkeypatch.setattr("app.main.UPLOAD_ROOT", tmp_path)
|
|
with TestClient(app) as client:
|
|
_, author_token, _, _ = register_user(client, full_name="Media Author")
|
|
_, reader_token, _, _ = register_user(client, full_name="Media Reader")
|
|
author_headers = {"Authorization": f"Bearer {author_token}"}
|
|
reader_headers = {"Authorization": f"Bearer {reader_token}"}
|
|
|
|
created = client.post(
|
|
"/api/v1/explore/posts",
|
|
headers=author_headers,
|
|
json={"content": "今天拍到的风景", "post_type": "image"},
|
|
)
|
|
assert created.status_code == 201
|
|
post_id = created.json()["id"]
|
|
|
|
upload = client.post(
|
|
f"/api/v1/explore/posts/{post_id}/media",
|
|
headers=author_headers,
|
|
params={"position": 0},
|
|
files={"media": ("view.png", b"image-content", "image/png")},
|
|
)
|
|
assert upload.status_code == 201
|
|
media_id = upload.json()["id"]
|
|
assert upload.json()["media_type"] == "image"
|
|
assert upload.json()["size_bytes"] == len(b"image-content")
|
|
|
|
detail = client.get(f"/api/v1/explore/posts/{post_id}", headers=reader_headers)
|
|
assert detail.status_code == 200
|
|
assert detail.json()["media"][0]["id"] == media_id
|
|
downloaded = client.get(f"/api/v1/explore/media/{media_id}", headers=reader_headers)
|
|
assert downloaded.status_code == 200
|
|
assert downloaded.content == b"image-content"
|
|
|
|
assert client.delete(f"/api/v1/explore/media/{media_id}", headers=reader_headers).status_code == 403
|
|
assert client.delete(f"/api/v1/explore/posts/{post_id}", headers=author_headers).status_code == 200
|
|
assert client.get(f"/api/v1/explore/media/{media_id}", headers=reader_headers).status_code == 404
|
|
assert not list(tmp_path.rglob("*.png"))
|
|
|
|
|
|
def test_feed_questions_answer_update_filter_and_delete():
|
|
with TestClient(app) as client:
|
|
_, token, _, _ = register_user(client, full_name="Question User")
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
questions = client.get("/api/v1/feed/questions", headers=headers)
|
|
assert questions.status_code == 200
|
|
assert len(questions.json()) >= 5
|
|
boolean_question = next(item for item in questions.json() if item["answer_type"] == "boolean")
|
|
text_question = next(item for item in questions.json() if item["answer_type"] == "text")
|
|
|
|
invalid = client.put(
|
|
f"/api/v1/feed/questions/{boolean_question['id']}/answer",
|
|
headers=headers,
|
|
json={"answer": "yes"},
|
|
)
|
|
assert invalid.status_code == 422
|
|
boolean_answer = client.put(
|
|
f"/api/v1/feed/questions/{boolean_question['id']}/answer",
|
|
headers=headers,
|
|
json={"answer": True},
|
|
)
|
|
assert boolean_answer.status_code == 200
|
|
assert boolean_answer.json()["answer"] is True
|
|
updated = client.put(
|
|
f"/api/v1/feed/questions/{boolean_question['id']}/answer",
|
|
headers=headers,
|
|
json={"answer": False},
|
|
)
|
|
assert updated.status_code == 200
|
|
assert updated.json()["answer"] is False
|
|
|
|
text_answer = client.put(
|
|
f"/api/v1/feed/questions/{text_question['id']}/answer",
|
|
headers=headers,
|
|
json={"answer": "一起喝咖啡并散步"},
|
|
)
|
|
assert text_answer.status_code == 200
|
|
assert text_answer.json()["answer"] == "一起喝咖啡并散步"
|
|
|
|
answered = client.get("/api/v1/feed/answers/me", headers=headers)
|
|
assert answered.status_code == 200
|
|
assert len(answered.json()) == 2
|
|
unanswered = client.get("/api/v1/feed/questions", headers=headers, params={"unanswered_only": True})
|
|
answered_ids = {boolean_question["id"], text_question["id"]}
|
|
assert answered_ids.isdisjoint({item["id"] for item in unanswered.json()})
|
|
|
|
deleted = client.delete(
|
|
f"/api/v1/feed/questions/{boolean_question['id']}/answer",
|
|
headers=headers,
|
|
)
|
|
assert deleted.status_code == 200
|
|
remaining = client.get("/api/v1/feed/answers/me", headers=headers)
|
|
assert len(remaining.json()) == 1
|
|
|
|
|
|
def test_public_profile_includes_interests_and_recent_posts_without_private_activity():
|
|
with TestClient(app) as client:
|
|
_, viewer_token, _, _ = register_user(client, full_name="Public Viewer")
|
|
target_id, target_token, _, _ = register_user(
|
|
client,
|
|
full_name="Public Target",
|
|
interests=["Travel", "Photography"],
|
|
bio="A public profile bio",
|
|
)
|
|
viewer_headers = {"Authorization": f"Bearer {viewer_token}"}
|
|
target_headers = {"Authorization": f"Bearer {target_token}"}
|
|
created = client.post(
|
|
"/api/v1/explore/posts",
|
|
headers=target_headers,
|
|
json={"content": "Target profile post", "post_type": "text"},
|
|
)
|
|
assert created.status_code == 201
|
|
|
|
profile = client.get(f"/api/v1/users/{target_id}", headers=viewer_headers)
|
|
assert profile.status_code == 200
|
|
payload = profile.json()
|
|
assert payload["interests"] == ["Travel", "Photography"]
|
|
assert len(payload["posts"]) == 1
|
|
assert payload["posts"][0]["content"] == "Target profile post"
|
|
assert "email_or_phone" not in payload
|
|
assert "status" not in payload
|
|
assert "liked_users" not in payload
|
|
assert "matches" not in payload
|
|
|