Add Opinion Wars: week-long two-faction battles attached to posts
A new post attachment type beside polls: the composer gains a Start
Opinion War builder (same disabled-inputs opt-in as the poll builder)
that names exactly two factions; the battle runs for exactly 7 days
from post creation. Members join a side, may defect at any time
(damage already dealt stays with the faction it was dealt to), and
fight once per 24 hours per battle. A fight spends 25 Code Farm coins
and deals deterministic level-weighted damage: 100 + 10 * min(level,
20) HP, so a newcomer deals 110 and a veteran caps at 300 - no
randomness anywhere.
The battle renders on the post card as a CSS pixel-art battlefield
(box-shadow sprites: castles, faction flags, marching soldiers, a
flickering campfire; steps() animation, disabled under reduced motion)
with live HP bars, a countdown, the viewer's faction strip, top
contributors and an event ticker. Live frames ride pub/sub on
public.battle.{uid} via a relay on the service-lock owner, with the
durable opinion_war_events trail (per-war atomic seq) as the source of
truth and a 15s incremental poller as fallback. /battles lists battles
with active/ended/mine filters, search and pagination.
Every mutation is a conditional UPDATE via conditional_update_row: the
fight sequence claims the cooldown first, then spends coins, then lands
the damage, compensating earlier steps on any later refusal so a crash
costs a turn, never coins. Resolution is lazy on read (no cron):
an exactly-once CAS computes the winner in the statement, awards XP
(participation, winner bonus, top damage dealer bonus; draws pay
participation only), emits the result event and notifies fighters. The
OpinionWarService backstop resolves unviewed wars and sends
fight-ready notifications, exactly-once via a marker CAS.
Fan-out: battle notification type, four badges, audit keys
(battle.create/join/switch/fight/resolve), Devii actions (join/fight
confirm-gated), API docs group, docs prose page, sitemap and topnav
entries, REPORTABLE_TARGETS registration, post-delete cascades,
README and nested CLAUDE.md documentation.
Verified with the four-layer procedure: property checks over the full
damage domain, 1200-step stateful fuzz (hp-sum invariant, coins never
negative, resolved totals frozen), and real 8-process races proving
exactly-once semantics for concurrent fights, double-spends across two
wars, resolution XP and double-joins. Persisted tests in
tests/unit/services/opinionwar, tests/api/battles, tests/e2e/battles
and tests/api/posts/create.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from devplacepy.database import get_table, refresh_snapshot
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
JSON = {"Accept": "application/json"}
|
||||
_counter_battles = [0]
|
||||
|
||||
|
||||
def _session_battles():
|
||||
_counter_battles[0] += 1
|
||||
name = f"war{int(time.time() * 1000)}{_counter_battles[0]}"
|
||||
s = requests.Session()
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/signup",
|
||||
data={
|
||||
"username": name,
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s, name
|
||||
|
||||
|
||||
def _create_war_post(session, title, faction_a="Tabs", faction_b="Spaces"):
|
||||
r = session.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
data={
|
||||
"content": "Opinion War host post content for tests.",
|
||||
"title": title,
|
||||
"topic": "question",
|
||||
"war_faction_a": faction_a,
|
||||
"war_faction_b": faction_b,
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
slug = r.headers["location"].split("/posts/")[-1]
|
||||
refresh_snapshot()
|
||||
post = get_table("posts").find_one(slug=slug)
|
||||
war = get_table("opinion_wars").find_one(post_uid=post["uid"])
|
||||
return post, war
|
||||
|
||||
|
||||
def _war_row(war_uid):
|
||||
refresh_snapshot()
|
||||
return get_table("opinion_wars").find_one(uid=war_uid)
|
||||
|
||||
|
||||
def _fighter_row(war_uid, user_uid):
|
||||
refresh_snapshot()
|
||||
return get_table("opinion_war_fighters").find_one(
|
||||
war_uid=war_uid, user_uid=user_uid
|
||||
)
|
||||
|
||||
|
||||
def _user_row(username):
|
||||
refresh_snapshot()
|
||||
return get_table("users").find_one(username=username)
|
||||
|
||||
|
||||
def _join(session, war_uid, faction):
|
||||
return session.post(
|
||||
f"{BASE_URL}/battles/{war_uid}/join",
|
||||
data={"faction": faction},
|
||||
headers=JSON,
|
||||
)
|
||||
|
||||
|
||||
def _fight(session, war_uid):
|
||||
return session.post(f"{BASE_URL}/battles/{war_uid}/fight", headers=JSON)
|
||||
@@ -0,0 +1,165 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import requests
|
||||
|
||||
from devplacepy.database import get_table, refresh_snapshot
|
||||
from devplacepy.services.opinionwar import rules
|
||||
from tests.api.battles._helpers import (
|
||||
JSON,
|
||||
_create_war_post,
|
||||
_fight,
|
||||
_fighter_row,
|
||||
_join,
|
||||
_session_battles,
|
||||
_user_row,
|
||||
_war_row,
|
||||
)
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def _warp_cooldown(war_uid, user_uid):
|
||||
fighter = _fighter_row(war_uid, user_uid)
|
||||
warped = (datetime.now(timezone.utc) - timedelta(hours=25)).isoformat()
|
||||
get_table("opinion_war_fighters").update(
|
||||
{"uid": fighter["uid"], "last_fight_at": warped}, ["uid"]
|
||||
)
|
||||
|
||||
|
||||
def test_join_and_switch(app_server):
|
||||
s, name = _session_battles()
|
||||
_, war = _create_war_post(s, f"join-{int(time.time() * 1000)}")
|
||||
r = _join(s, war["uid"], "a")
|
||||
assert r.status_code == 200
|
||||
payload = r.json()
|
||||
assert payload["ok"] is True
|
||||
assert payload["data"]["war"]["viewer"]["faction"] == "a"
|
||||
fighter = _fighter_row(war["uid"], _user_row(name)["uid"])
|
||||
assert fighter["faction"] == "a"
|
||||
r = _join(s, war["uid"], "b")
|
||||
assert r.json()["data"]["war"]["viewer"]["faction"] == "b"
|
||||
fighter = _fighter_row(war["uid"], _user_row(name)["uid"])
|
||||
assert fighter["faction"] == "b"
|
||||
|
||||
|
||||
def test_join_invalid_faction_422(app_server):
|
||||
s, _ = _session_battles()
|
||||
_, war = _create_war_post(s, f"badfaction-{int(time.time() * 1000)}")
|
||||
r = s.post(
|
||||
f"{BASE_URL}/battles/{war['uid']}/join",
|
||||
data={"faction": "c"},
|
||||
headers=JSON,
|
||||
)
|
||||
assert r.status_code in (400, 422)
|
||||
|
||||
|
||||
def test_join_requires_login(app_server):
|
||||
s, _ = _session_battles()
|
||||
_, war = _create_war_post(s, f"anon-{int(time.time() * 1000)}")
|
||||
r = requests.post(
|
||||
f"{BASE_URL}/battles/{war['uid']}/join",
|
||||
data={"faction": "a"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code in (303, 401)
|
||||
|
||||
|
||||
def test_fight_spends_coins_and_deals_level_damage(app_server):
|
||||
s, name = _session_battles()
|
||||
_, war = _create_war_post(s, f"fight-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "a")
|
||||
r = _fight(s, war["uid"])
|
||||
assert r.status_code == 200
|
||||
payload = r.json()
|
||||
user = _user_row(name)
|
||||
expected = rules.damage_for(user.get("level"))
|
||||
assert payload["data"]["damage"] == expected
|
||||
assert payload["data"]["war"]["hp_a"] == expected
|
||||
war_row = _war_row(war["uid"])
|
||||
assert int(war_row["hp_a"]) == expected
|
||||
refresh_snapshot()
|
||||
farm = get_table("game_farms").find_one(user_uid=user["uid"])
|
||||
from devplacepy.services.game import economy
|
||||
|
||||
assert int(farm["coins"]) == economy.STARTING_COINS - rules.FIGHT_COST_COINS
|
||||
|
||||
|
||||
def test_fight_cooldown_refused(app_server):
|
||||
s, _ = _session_battles()
|
||||
_, war = _create_war_post(s, f"cooldown-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "a")
|
||||
assert _fight(s, war["uid"]).status_code == 200
|
||||
r = _fight(s, war["uid"])
|
||||
assert r.status_code == 400
|
||||
assert "24 hours" in r.json()["error"]["message"]
|
||||
|
||||
|
||||
def test_fight_after_warped_cooldown_lands(app_server):
|
||||
s, name = _session_battles()
|
||||
_, war = _create_war_post(s, f"warp-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "b")
|
||||
assert _fight(s, war["uid"]).status_code == 200
|
||||
_warp_cooldown(war["uid"], _user_row(name)["uid"])
|
||||
r = _fight(s, war["uid"])
|
||||
assert r.status_code == 200
|
||||
fighter = _fighter_row(war["uid"], _user_row(name)["uid"])
|
||||
assert int(fighter["fight_count"]) == 2
|
||||
|
||||
|
||||
def test_fight_without_coins_refused_and_cooldown_kept(app_server):
|
||||
s, name = _session_battles()
|
||||
_, war = _create_war_post(s, f"broke-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "a")
|
||||
assert _fight(s, war["uid"]).status_code == 200
|
||||
user = _user_row(name)
|
||||
_warp_cooldown(war["uid"], user["uid"])
|
||||
refresh_snapshot()
|
||||
farm = get_table("game_farms").find_one(user_uid=user["uid"])
|
||||
get_table("game_farms").update({"uid": farm["uid"], "coins": 0}, ["uid"])
|
||||
r = _fight(s, war["uid"])
|
||||
assert r.status_code == 400
|
||||
assert "coins" in r.json()["error"]["message"]
|
||||
fighter = _fighter_row(war["uid"], user["uid"])
|
||||
assert int(fighter["fight_count"]) == 1
|
||||
r = _fight(s, war["uid"])
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_fight_before_join_refused(app_server):
|
||||
s, _ = _session_battles()
|
||||
other, _ = _session_battles()
|
||||
_, war = _create_war_post(s, f"nojoin-{int(time.time() * 1000)}")
|
||||
r = _fight(other, war["uid"])
|
||||
assert r.status_code == 400
|
||||
assert "Join" in r.json()["error"]["message"]
|
||||
|
||||
|
||||
def test_switch_keeps_dealt_damage(app_server):
|
||||
s, name = _session_battles()
|
||||
_, war = _create_war_post(s, f"defect-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "a")
|
||||
assert _fight(s, war["uid"]).status_code == 200
|
||||
r = _join(s, war["uid"], "b")
|
||||
war_state = r.json()["data"]["war"]
|
||||
user = _user_row(name)
|
||||
expected = rules.damage_for(user.get("level"))
|
||||
assert war_state["hp_a"] == expected
|
||||
assert war_state["hp_b"] == 0
|
||||
fighter = _fighter_row(war["uid"], user["uid"])
|
||||
assert fighter["faction"] == "b"
|
||||
assert int(fighter["hp_a"]) == expected
|
||||
|
||||
|
||||
def test_actions_on_resolved_war_refused(app_server):
|
||||
s, _ = _session_battles()
|
||||
other, _ = _session_battles()
|
||||
_, war = _create_war_post(s, f"over-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "a")
|
||||
past = (datetime.now(timezone.utc) - timedelta(minutes=1)).isoformat()
|
||||
get_table("opinion_wars").update({"uid": war["uid"], "ends_at": past}, ["uid"])
|
||||
r = _fight(s, war["uid"])
|
||||
assert r.status_code == 400
|
||||
r = _join(other, war["uid"], "b")
|
||||
assert r.status_code == 400
|
||||
@@ -0,0 +1,43 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from tests.api.battles._helpers import _create_war_post, _fight, _join, _session_battles
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def test_events_ordered_and_incremental(app_server):
|
||||
s, _ = _session_battles()
|
||||
other, _ = _session_battles()
|
||||
_, war = _create_war_post(s, f"events-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "a")
|
||||
_join(other, war["uid"], "b")
|
||||
_fight(s, war["uid"])
|
||||
|
||||
r = requests.get(f"{BASE_URL}/battles/{war['uid']}/events")
|
||||
assert r.status_code == 200
|
||||
payload = r.json()
|
||||
assert payload["status"] == "active"
|
||||
seqs = [event["seq"] for event in payload["events"]]
|
||||
assert seqs == sorted(seqs)
|
||||
kinds = [event["kind"] for event in payload["events"]]
|
||||
assert kinds[:3] == ["join", "join", "fight"]
|
||||
assert "lead_change" in kinds
|
||||
|
||||
fight_event = next(e for e in payload["events"] if e["kind"] == "fight")
|
||||
assert fight_event["faction"] == "a"
|
||||
assert fight_event["hp_a"] > 0
|
||||
|
||||
after = seqs[1]
|
||||
r = requests.get(
|
||||
f"{BASE_URL}/battles/{war['uid']}/events", params={"after": after}
|
||||
)
|
||||
replay = [event["seq"] for event in r.json()["events"]]
|
||||
assert replay == [seq for seq in seqs if seq > after]
|
||||
|
||||
|
||||
def test_events_unknown_battle_404(app_server):
|
||||
r = requests.get(f"{BASE_URL}/battles/nope/events")
|
||||
assert r.status_code == 404
|
||||
@@ -0,0 +1,73 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from tests.api.battles._helpers import JSON, _create_war_post, _session_battles
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def test_listing_renders_battle_card(app_server):
|
||||
s, _ = _session_battles()
|
||||
_create_war_post(s, f"listing-{int(time.time() * 1000)}", "Vim", "Emacs")
|
||||
r = s.get(f"{BASE_URL}/battles")
|
||||
assert r.status_code == 200
|
||||
assert "dp-opinion-war" in r.text
|
||||
assert "Vim" in r.text
|
||||
|
||||
|
||||
def test_listing_json_shape(app_server):
|
||||
s, _ = _session_battles()
|
||||
_, war = _create_war_post(s, f"json-{int(time.time() * 1000)}")
|
||||
r = s.get(f"{BASE_URL}/battles", headers=JSON)
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["current_filter"] == "active"
|
||||
assert set(data["counts"]) == {"active", "ended", "mine"}
|
||||
uids = [battle["uid"] for battle in data["battles"]]
|
||||
assert war["uid"] in uids
|
||||
battle = next(b for b in data["battles"] if b["uid"] == war["uid"])
|
||||
assert battle["faction_a"] == "Tabs"
|
||||
assert battle["pct_a"] + battle["pct_b"] == 100
|
||||
assert battle["fight_cost"] > 0
|
||||
|
||||
|
||||
def test_listing_guest_access(app_server):
|
||||
s, _ = _session_battles()
|
||||
_create_war_post(s, f"guest-{int(time.time() * 1000)}")
|
||||
r = requests.get(f"{BASE_URL}/battles")
|
||||
assert r.status_code == 200
|
||||
assert "dp-opinion-war" in r.text
|
||||
|
||||
|
||||
def test_listing_search_matches_faction(app_server):
|
||||
s, _ = _session_battles()
|
||||
marker = f"Zx{int(time.time() * 1000) % 100000}"
|
||||
_, war = _create_war_post(s, f"search-{marker}", marker, "Others")
|
||||
r = s.get(f"{BASE_URL}/battles", params={"search": marker}, headers=JSON)
|
||||
uids = [battle["uid"] for battle in r.json()["battles"]]
|
||||
assert uids == [war["uid"]]
|
||||
|
||||
|
||||
def test_listing_invalid_filter_falls_back(app_server):
|
||||
s, _ = _session_battles()
|
||||
r = s.get(f"{BASE_URL}/battles", params={"filter": "bogus"}, headers=JSON)
|
||||
assert r.json()["current_filter"] == "active"
|
||||
|
||||
|
||||
def test_battle_state_json(app_server):
|
||||
s, _ = _session_battles()
|
||||
_, war = _create_war_post(s, f"state-{int(time.time() * 1000)}")
|
||||
r = requests.get(f"{BASE_URL}/battles/{war['uid']}")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["uid"] == war["uid"]
|
||||
assert data["status"] == "active"
|
||||
assert data["viewer"] is None
|
||||
assert data["ends_in"]
|
||||
|
||||
|
||||
def test_battle_state_unknown_uid_404(app_server):
|
||||
r = requests.get(f"{BASE_URL}/battles/does-not-exist", headers=JSON)
|
||||
assert r.status_code == 404
|
||||
@@ -0,0 +1,94 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import requests
|
||||
|
||||
from devplacepy.database import get_table, refresh_snapshot
|
||||
from devplacepy.utils.rewards import XP_BATTLE_PART, XP_BATTLE_TOP, XP_BATTLE_WIN
|
||||
from tests.api.battles._helpers import (
|
||||
_create_war_post,
|
||||
_fight,
|
||||
_join,
|
||||
_session_battles,
|
||||
_user_row,
|
||||
_war_row,
|
||||
)
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def _warp_ended(war_uid):
|
||||
past = (datetime.now(timezone.utc) - timedelta(minutes=1)).isoformat()
|
||||
get_table("opinion_wars").update({"uid": war_uid, "ends_at": past}, ["uid"])
|
||||
|
||||
|
||||
def test_resolution_awards_xp_exactly_once(app_server):
|
||||
s, winner_name = _session_battles()
|
||||
other, loser_name = _session_battles()
|
||||
_, war = _create_war_post(s, f"resolve-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "a")
|
||||
_join(other, war["uid"], "b")
|
||||
assert _fight(s, war["uid"]).status_code == 200
|
||||
winner_before = int(_user_row(winner_name)["xp"] or 0)
|
||||
loser_before = int(_user_row(loser_name)["xp"] or 0)
|
||||
|
||||
_warp_ended(war["uid"])
|
||||
r = requests.get(f"{BASE_URL}/battles/{war['uid']}")
|
||||
assert r.status_code == 200
|
||||
state = r.json()
|
||||
assert state["status"] == "resolved"
|
||||
assert state["winner"] == "a"
|
||||
assert state["winner_label"] == "Tabs"
|
||||
|
||||
row = _war_row(war["uid"])
|
||||
assert row["status"] == "resolved"
|
||||
assert row["resolved_at"]
|
||||
|
||||
winner_after = int(_user_row(winner_name)["xp"] or 0)
|
||||
loser_after = int(_user_row(loser_name)["xp"] or 0)
|
||||
assert winner_after == winner_before + XP_BATTLE_PART + XP_BATTLE_WIN + XP_BATTLE_TOP
|
||||
assert loser_after == loser_before
|
||||
|
||||
events = requests.get(f"{BASE_URL}/battles/{war['uid']}/events").json()["events"]
|
||||
results = [event for event in events if event["kind"] == "result"]
|
||||
assert len(results) == 1
|
||||
|
||||
refresh_snapshot()
|
||||
notes = list(
|
||||
get_table("notifications").find(
|
||||
type="battle", user_uid=_user_row(loser_name)["uid"]
|
||||
)
|
||||
)
|
||||
assert any("Battle over" in (n.get("message") or "") for n in notes)
|
||||
|
||||
r = requests.get(f"{BASE_URL}/battles/{war['uid']}")
|
||||
assert r.json()["status"] == "resolved"
|
||||
assert int(_user_row(winner_name)["xp"] or 0) == winner_after
|
||||
events = requests.get(f"{BASE_URL}/battles/{war['uid']}/events").json()["events"]
|
||||
assert len([e for e in events if e["kind"] == "result"]) == 1
|
||||
|
||||
|
||||
def test_draw_awards_participation_only(app_server):
|
||||
s, name = _session_battles()
|
||||
_, war = _create_war_post(s, f"draw-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "a")
|
||||
before = int(_user_row(name)["xp"] or 0)
|
||||
_warp_ended(war["uid"])
|
||||
state = requests.get(f"{BASE_URL}/battles/{war['uid']}").json()
|
||||
assert state["winner"] == "draw"
|
||||
assert int(_user_row(name)["xp"] or 0) == before
|
||||
|
||||
|
||||
def test_post_page_resolves_due_war(app_server):
|
||||
s, _ = _session_battles()
|
||||
post, war = _create_war_post(s, f"lazy-{int(time.time() * 1000)}")
|
||||
_join(s, war["uid"], "b")
|
||||
assert _fight(s, war["uid"]).status_code == 200
|
||||
_warp_ended(war["uid"])
|
||||
slug = post.get("slug") or post["uid"]
|
||||
r = s.get(f"{BASE_URL}/posts/{slug}")
|
||||
assert r.status_code == 200
|
||||
assert "wins the war" in r.text
|
||||
assert _war_row(war["uid"])["status"] == "resolved"
|
||||
assert _war_row(war["uid"])["winner"] == "b"
|
||||
@@ -795,3 +795,109 @@ def test_feed_includes_project_when_linked(app_server):
|
||||
assert "/projects/" in item["project_link"]["url"]
|
||||
break
|
||||
assert found, "feed must include project info for linked posts"
|
||||
|
||||
|
||||
def test_create_post_with_war_creates_battle(app_server):
|
||||
from devplacepy.services.opinionwar import rules
|
||||
|
||||
s, _ = _member()
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
headers=JSON_audit_log,
|
||||
data={
|
||||
"title": _unique("auwar"),
|
||||
"content": "this post carries an opinion war between two factions",
|
||||
"topic": "question",
|
||||
"war_faction_a": "Tabs",
|
||||
"war_faction_b": "Spaces",
|
||||
},
|
||||
)
|
||||
post_uid = r.json()["data"]["uid"]
|
||||
refresh_snapshot()
|
||||
war = get_table("opinion_wars").find_one(post_uid=post_uid)
|
||||
assert war is not None
|
||||
assert war["faction_a"] == "Tabs"
|
||||
assert war["faction_b"] == "Spaces"
|
||||
assert war["status"] == "active"
|
||||
created = datetime.fromisoformat(war["created_at"])
|
||||
ends = datetime.fromisoformat(war["ends_at"])
|
||||
assert abs((ends - created).total_seconds() - rules.WAR_DURATION_DAYS * 86400) < 5
|
||||
|
||||
|
||||
def test_create_post_with_one_faction_creates_no_war(app_server):
|
||||
s, _ = _member()
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
headers=JSON_audit_log,
|
||||
data={
|
||||
"title": _unique("auwarhalf"),
|
||||
"content": "one faction alone starts no war on this post",
|
||||
"topic": "question",
|
||||
"war_faction_a": "Loners",
|
||||
},
|
||||
)
|
||||
post_uid = r.json()["data"]["uid"]
|
||||
refresh_snapshot()
|
||||
assert get_table("opinion_wars").find_one(post_uid=post_uid) is None
|
||||
|
||||
|
||||
def test_create_post_with_equal_factions_creates_no_war(app_server):
|
||||
s, _ = _member()
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
headers=JSON_audit_log,
|
||||
data={
|
||||
"title": _unique("auwareq"),
|
||||
"content": "two identical faction names start no war here",
|
||||
"topic": "question",
|
||||
"war_faction_a": "Same",
|
||||
"war_faction_b": "same",
|
||||
},
|
||||
)
|
||||
post_uid = r.json()["data"]["uid"]
|
||||
refresh_snapshot()
|
||||
assert get_table("opinion_wars").find_one(post_uid=post_uid) is None
|
||||
|
||||
|
||||
def test_create_post_with_poll_and_war_attaches_both(app_server):
|
||||
s, _ = _member()
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
headers=JSON_audit_log,
|
||||
data=[
|
||||
("title", _unique("auwarpoll")),
|
||||
("content", "this post carries a poll and an opinion war together"),
|
||||
("topic", "question"),
|
||||
("poll_question", "Which side?"),
|
||||
("poll_options", "Tabs"),
|
||||
("poll_options", "Spaces"),
|
||||
("war_faction_a", "Tabs"),
|
||||
("war_faction_b", "Spaces"),
|
||||
],
|
||||
)
|
||||
post_uid = r.json()["data"]["uid"]
|
||||
refresh_snapshot()
|
||||
assert get_table("polls").find_one(post_uid=post_uid) is not None
|
||||
assert get_table("opinion_wars").find_one(post_uid=post_uid) is not None
|
||||
|
||||
|
||||
def test_feed_serializes_war_on_post(app_server):
|
||||
s, _ = _member()
|
||||
marker = _unique("auwarfeed")
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
headers=JSON_audit_log,
|
||||
data={
|
||||
"title": marker,
|
||||
"content": "the feed must carry the serialized war for this post",
|
||||
"topic": "question",
|
||||
"war_faction_a": "Cats",
|
||||
"war_faction_b": "Dogs",
|
||||
},
|
||||
)
|
||||
post_uid = r.json()["data"]["uid"]
|
||||
feed = s.get(f"{BASE_URL}/feed", headers=JSON_audit_log).json()
|
||||
item = next(i for i in feed["posts"] if i["post"]["uid"] == post_uid)
|
||||
assert item["war"] is not None
|
||||
assert item["war"]["faction_a"] == "Cats"
|
||||
assert item["war"]["pct_a"] == 50
|
||||
|
||||
Reference in New Issue
Block a user