Files
devplacepy/tests/api/votes.py
retoor ebf6bf7f82 feat: add Code Farm cooperative idle game with plot-based crop system and pub/sub notifications
Implement a Farmville-style cooperative idle game mounted at `/game` with member-only play and public farm viewing. The data layer is pure and timestamp-driven with no background tick: plot states are derived from `ready_at` vs current time, never stored. Key features include plantable software projects (shell script through kernel) that build over real time, harvest for coins and XP, CI tier upgrades for faster builds, plot purchasing with doubling cost, watering mechanics for friends' builds, daily quests, and prestige system. All game endpoints negotiate HTML or JSON and return full farm state for single-request client refresh. Pub/sub notifications broadcast farm updates on `public.game.farm.{username}` topics. Database schema adds `game_farms`, `game_plots`, and `game_quests` tables with appropriate indexes.
2026-06-22 16:41:53 +00:00

138 lines
4.5 KiB
Python

# retoor <retoor@molodetz.nl>
import time
import requests
from tests.conftest import BASE_URL
def _session_validation():
s = requests.Session()
name = f"val_{int(time.time() * 1000)}"
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
from datetime import datetime, timezone
from devplacepy.database import get_table
from devplacepy.utils import generate_uid
_counter_votes = [0]
AJAX_votes = {"X-Requested-With": "fetch"}
def _session_votes():
_counter_votes[0] += 1
name = f"vot{int(time.time() * 1000)}{_counter_votes[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_votes(username):
return get_table("users").find_one(username=username)["uid"]
def _make_post_votes(owner_uid):
uid = generate_uid()
get_table("posts").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner_uid,
"slug": f"{uid[:8]}-vote-post",
"title": None,
"content": "vote target content",
"topic": "random",
"project_uid": None,
"image": None,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return uid
def test_vote_bad_value_is_handled_not_500(app_server):
s = _session_validation()
r = s.post(
f"{BASE_URL}/votes/post/nonexistent",
data={"value": "abc"},
allow_redirects=False,
)
assert r.status_code != 500
assert r.status_code in (302, 303, 400)
def test_upvote_returns_ajax_payload(app_server):
s_a, a_name = _session_votes()
s_b, _ = _session_votes()
post_uid = _make_post_votes(_uid_votes(a_name))
r = s_b.post(f"{BASE_URL}/votes/post/{post_uid}", data={"value": "1"}, headers=AJAX_votes)
assert r.json() == {"net": 1, "up": 1, "down": 0, "value": 1}
def test_repeated_vote_toggles_off(app_server):
s_a, a_name = _session_votes()
s_b, _ = _session_votes()
post_uid = _make_post_votes(_uid_votes(a_name))
s_b.post(f"{BASE_URL}/votes/post/{post_uid}", data={"value": "1"}, headers=AJAX_votes)
r = s_b.post(f"{BASE_URL}/votes/post/{post_uid}", data={"value": "1"}, headers=AJAX_votes)
assert r.json()["net"] == 0
assert r.json()["value"] == 0
def test_switch_upvote_to_downvote(app_server):
s_a, a_name = _session_votes()
s_b, _ = _session_votes()
post_uid = _make_post_votes(_uid_votes(a_name))
s_b.post(f"{BASE_URL}/votes/post/{post_uid}", data={"value": "1"}, headers=AJAX_votes)
r = s_b.post(
f"{BASE_URL}/votes/post/{post_uid}", data={"value": "-1"}, headers=AJAX_votes
)
assert r.json()["net"] == -1
assert r.json()["value"] == -1
def test_upvote_notifies_owner_and_awards_xp(app_server):
s_a, a_name = _session_votes()
s_b, _ = _session_votes()
a_uid = _uid_votes(a_name)
post_uid = _make_post_votes(a_uid)
before = get_table("users").find_one(uid=a_uid).get("xp", 0) or 0
s_b.post(f"{BASE_URL}/votes/post/{post_uid}", data={"value": "1"}, headers=AJAX_votes)
assert get_table("notifications").count(user_uid=a_uid, type="vote") == 1
after = get_table("users").find_one(uid=a_uid).get("xp", 0) or 0
assert after - before == 5
def test_self_vote_does_not_notify(app_server):
s_a, a_name = _session_votes()
a_uid = _uid_votes(a_name)
post_uid = _make_post_votes(a_uid)
before = get_table("notifications").count(user_uid=a_uid, type="vote")
s_a.post(f"{BASE_URL}/votes/post/{post_uid}", data={"value": "1"}, headers=AJAX_votes)
after = get_table("notifications").count(user_uid=a_uid, type="vote")
assert after == before
def test_non_ajax_vote_redirects_to_referer(app_server):
s_a, a_name = _session_votes()
s_b, _ = _session_votes()
post_uid = _make_post_votes(_uid_votes(a_name))
r = s_b.post(
f"{BASE_URL}/votes/post/{post_uid}",
data={"value": "1"},
headers={"Referer": f"{BASE_URL}/gists"},
allow_redirects=False,
)
assert r.status_code == 302
assert r.headers["location"] == "/gists"