import sqlite3 import time from datetime import datetime, timezone import requests from tests.conftest import BASE_URL from devplacepy.config import DATABASE_URL from devplacepy.database import get_table from devplacepy.utils import generate_uid from devplacepy.constants import REACTION_EMOJI _counter = [0] AJAX = {"X-Requested-With": "fetch"} ROCKET = REACTION_EMOJI[2] HEART = REACTION_EMOJI[1] def _session(): _counter[0] += 1 name = f"rxn{int(time.time() * 1000)}{_counter[0]}" s = requests.Session() s.post(f"{BASE_URL}/auth/signup", data={ "username": name, "email": f"{name}@t.dev", "password": "secret123", "confirm_password": "secret123", }, allow_redirects=True) return s, name def _uid(username): return get_table("users").find_one(username=username)["uid"] def _live_query(sql, params): conn = sqlite3.connect(DATABASE_URL.replace("sqlite:///", "", 1)) try: return conn.execute(sql, params).fetchone() finally: conn.close() def _live_reaction_count(target_type, target_uid): return _live_query( "SELECT COUNT(*) FROM reactions WHERE target_type=? AND target_uid=?", (target_type, target_uid), )[0] def _live_post_uid(slug): row = _live_query("SELECT uid FROM posts WHERE slug=?", (slug,)) return row[0] if row else None def _make_post(owner_uid): uid = generate_uid() get_table("posts").insert({ "uid": uid, "user_uid": owner_uid, "slug": f"{uid[:8]}-reaction-post", "title": None, "content": "reaction target content", "topic": "random", "project_uid": None, "image": None, "stars": 0, "created_at": datetime.now(timezone.utc).isoformat(), }) return uid def _make_comment(post_uid, owner_uid): uid = generate_uid() get_table("comments").insert({ "uid": uid, "target_type": "post", "target_uid": post_uid, "post_uid": post_uid, "user_uid": owner_uid, "content": "reaction target comment", "parent_uid": None, "created_at": datetime.now(timezone.utc).isoformat(), }) return uid def test_add_reaction_returns_counts(app_server): s, name = _session() post_uid = _make_post(_uid(name)) r = s.post(f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX) payload = r.json() assert payload["counts"][ROCKET] == 1 assert ROCKET in payload["mine"] assert get_table("reactions").count(target_type="post", target_uid=post_uid) == 1 def test_same_emoji_toggles_off(app_server): s, name = _session() post_uid = _make_post(_uid(name)) s.post(f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX) r = s.post(f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX) payload = r.json() assert payload["counts"].get(ROCKET, 0) == 0 assert payload["mine"] == [] assert get_table("reactions").count(target_type="post", target_uid=post_uid) == 0 def test_two_distinct_emoji_both_counted(app_server): s, name = _session() post_uid = _make_post(_uid(name)) s.post(f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX) r = s.post(f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": HEART}, headers=AJAX) payload = r.json() assert payload["counts"][ROCKET] == 1 assert payload["counts"][HEART] == 1 assert set(payload["mine"]) == {ROCKET, HEART} def test_emoji_outside_palette_rejected(app_server): s, name = _session() post_uid = _make_post(_uid(name)) r = s.post(f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": "notanemoji"}, headers=AJAX, allow_redirects=False) assert r.status_code == 303 assert get_table("reactions").count(target_type="post", target_uid=post_uid) == 0 def test_invalid_target_type_returns_400(app_server): s, name = _session() post_uid = _make_post(_uid(name)) r = s.post(f"{BASE_URL}/reactions/news/{post_uid}", data={"emoji": ROCKET}, headers=AJAX) assert r.status_code == 400 def test_reaction_requires_login(app_server): owner_s, owner_name = _session() post_uid = _make_post(_uid(owner_name)) anon = requests.Session() r = anon.post(f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX, allow_redirects=False) assert r.status_code == 303 assert get_table("reactions").count(target_type="post", target_uid=post_uid) == 0 def test_react_on_comment_target(app_server): s, name = _session() owner_uid = _uid(name) post_uid = _make_post(owner_uid) comment_uid = _make_comment(post_uid, owner_uid) r = s.post(f"{BASE_URL}/reactions/comment/{comment_uid}", data={"emoji": HEART}, headers=AJAX) assert r.json()["counts"][HEART] == 1 assert get_table("reactions").count(target_type="comment", target_uid=comment_uid) == 1 def test_delete_post_cascades_reactions(app_server): s, name = _session() title = f"cascade-{int(time.time() * 1000)}" r = s.post(f"{BASE_URL}/posts/create", data={ "content": "Post created via the server for the reaction cascade test.", "title": title, "topic": "random", }, allow_redirects=False) slug = r.headers["location"].split("/posts/")[-1] post_uid = _live_post_uid(slug) s.post(f"{BASE_URL}/reactions/post/{post_uid}", data={"emoji": ROCKET}, headers=AJAX) assert _live_reaction_count("post", post_uid) == 1 s.post(f"{BASE_URL}/posts/delete/{slug}", allow_redirects=False) assert s.get(f"{BASE_URL}/posts/{slug}").status_code == 404 assert _live_reaction_count("post", post_uid) == 0