Files
product-fpol/tests/test_application.py
khodowany fa99c6624a
ci / frontend (push) Successful in 9m11s
ci / backend (push) Failing after 16m6s
Fix
2026-10-06 09:07:41 -05:00

332 lines
20 KiB
Python

"""Real C++ service/SQLite integration without requiring a listening socket."""
import concurrent.futures
import copy
import json
import pathlib
import sqlite3
import subprocess
import sys
import tempfile
import unittest
ADMIN = pathlib.Path(sys.argv.pop(1)).resolve()
INGEST = pathlib.Path(sys.argv.pop(1)).resolve()
ROOT = pathlib.Path(__file__).resolve().parents[1]
NOW = 2000000000
PASSWORD = "a sufficiently long password"
class application_tests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.base = tempfile.TemporaryDirectory(prefix="fpol_accounts_")
cls.base_database = pathlib.Path(cls.base.name) / "base.sqlite"
cls.identities = {}
def command(*args, stdin=None):
run = subprocess.run([str(ADMIN), "--database", str(cls.base_database), "--allow-demo", *args], input=stdin, text=True, capture_output=True, timeout=30)
if run.returncode:
raise RuntimeError(f"{args[0]} exited with code {run.returncode}:\n{run.stderr}")
return json.loads(run.stdout.splitlines()[-1])
command("import", str(ROOT / "data/demo/catalog.json"), str(ROOT / "data/demo/ratings.json"), str(ROOT / "data/real/us-2026-10-04-partial.json"))
for username in ("alice", "bob", "carol"):
command("create-user", username, stdin=PASSWORD + "\n")
cls.identities[username] = command("app-request", "-", stdin=json.dumps({"action": "auth/login", "input": {"username": username, "password": PASSWORD}, "clock": NOW}))
@classmethod
def tearDownClass(cls):
cls.base.cleanup()
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="fpol_application_")
self.addCleanup(self.temp.cleanup)
self.path = pathlib.Path(self.temp.name)
self.database = self.path / "test.sqlite"
with sqlite3.connect(self.base_database) as source, sqlite3.connect(self.database) as destination:
source.backup(destination)
self.identities = copy.deepcopy(type(self).identities)
self.now = NOW
def command(self, executable, *args, stdin=None):
result = subprocess.run([str(executable), "--database", str(self.database), *map(str, args)], input=stdin, capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stderr)
return json.loads(result.stdout.splitlines()[-1])
def request(self, action, data=None, who="alice", status=200, csrf=True, raw=False):
identity = self.identities.get(who, {})
result = self.command(ADMIN, "--allow-demo", "app-request", "-", stdin=json.dumps({"action": action, "input": data or {}, "clock": self.now,
"session": identity.get("session_token", ""), "csrf": identity.get("payload", {}).get("csrf", "") if csrf else ""}))
self.assertEqual(result.get("status", 200), status, result)
return result if raw or not result.get("ok") else result.get("payload", {})
def league(self, players=("alice", "bob"), maximum=2, demo=True):
data = {"name": "Friends", "team_name": "Alice team", "max_teams": maximum}
if demo:
data["catalog_id"] = "demo-2026"
league = self.request("league/create", data)["league_id"]
invite = self.request("league/invite", {"league_id": league})["invite"]
for player in players[1:]:
self.request("league/join", {"invite": invite, "team_name": player + " team"}, player)
return league, invite
def view(self, league, who="alice"):
return self.request("league", {"league_id": league}, who)
def start(self, players=("alice", "bob")):
league, _ = self.league(players=players, maximum=len(players))
self.request("draft/start", {"league_id": league})
return league
def turn(self, league):
view = self.view(league)
who = view["picker"]["username"]
return who, self.view(league, who)
def pick(self, league, candidate=None, request_id=None):
who, view = self.turn(league)
data = {"league_id": league, "candidate_id": candidate or view["legal_candidate_ids"][0], "pick_number": view["draft"]["pick_number"], "request_id": request_id or f"pick-{view['draft']['pick_number']}"}
return who, data, self.request("draft/pick", data, who)
def file(self, name, body):
path = self.path / name
path.write_text(json.dumps(body))
return path
def ingest(self, payload, configure=False):
if configure:
config = {"id": "fixture", "kind": "official-json", "url": "https://example.org/reviewed-test-fixture", "catalog_id": "demo-2026", "races": [{"race_id": "demo-house-01", "role": role} for role in ("counts", "outcome")]}
self.command(INGEST, "configure", self.file("source.json", config))
return self.command(INGEST, "replay", "fixture", self.file("payload.json", payload))
def test_auth_and_csrf_and_logout(self):
self.request("leagues", who="nobody", status=401)
self.request("league/create", {}, csrf=False, status=403)
self.request("auth/login", {"username": "alice", "password": "incorrect password"}, status=401)
self.request("auth/logout")
self.request("auth/me", status=401)
def test_login_rate_limit(self):
for _ in range(10):
self.request("auth/login", {"username": "nonexistent", "password": PASSWORD}, status=401)
self.request("auth/login", {"username": "nonexistent", "password": PASSWORD}, status=429)
self.now += 901
self.request("auth/login", {"username": "nonexistent", "password": PASSWORD}, status=401)
def test_invited_registration_capacity_and_atomicity(self):
league, invite = self.league(players=("alice",), maximum=2)
credentials = {"username": "david", "password": PASSWORD, "team_name": "David", "invite": invite}
registered = self.request("auth/register", credentials, who="nobody", raw=True)
self.assertEqual(registered["payload"]["admin"], 0)
self.assertEqual(len(self.view(league)["teams"]), 2)
credentials["username"] = "edward"
self.request("auth/register", credentials, who="nobody", status=409)
with sqlite3.connect(self.database) as database:
self.assertEqual(database.execute("SELECT COUNT(*) FROM users WHERE username='edward'").fetchone()[0], 0)
self.assertNotIn(invite, str(database.execute("SELECT * FROM invites").fetchall()))
def test_invite_expiry_and_revocation(self):
league, invite = self.league(players=("alice",))
self.request("league/revoke-invites", {"league_id": league})
self.request("league/join", {"invite": invite, "team_name": "Bob"}, "bob", status=404)
invite = self.request("league/invite", {"league_id": league})["invite"]
self.now += 7 * 86400
self.request("league/join", {"invite": invite, "team_name": "Bob"}, "bob", status=404)
def test_isolation_and_commissioner_permissions(self):
league, _ = self.league()
self.request("league", {"league_id": league}, "carol", status=403)
self.request("standings", {"league_id": league}, "carol", status=403)
self.request("candidates", {"league_id": league}, "carol", status=403)
self.request("draft/start", {"league_id": league}, "bob", status=403)
self.request("league/invite", {"league_id": league}, "bob", status=403)
self.assertEqual(self.request("leagues", who="carol"), [])
self.request("draft/queue", {"league_id": league}, status=400)
def test_reset_single_use_expiry_and_session_revocation(self):
reset = self.command(ADMIN, "reset-link", "alice")
# Operator reset timestamps use wall time, not the diagnostic clock.
self.now = reset["expires_at"] - 1
self.request("auth/reset", {"reset_token": reset["reset_token"], "password": PASSWORD + " new"}, who="nobody")
self.now = NOW
self.request("auth/me", status=401)
self.request("auth/reset", {"reset_token": reset["reset_token"], "password": PASSWORD}, who="nobody", status=400)
reset = self.command(ADMIN, "reset-link", "bob")
self.now = reset["expires_at"]
self.request("auth/reset", {"reset_token": reset["reset_token"], "password": PASSWORD}, who="nobody", status=400)
def test_password_change_invalidates_old_session(self):
result = self.request("auth/password", {"current_password": PASSWORD, "password": PASSWORD + " updated"}, raw=True)
self.request("auth/me", status=401)
self.identities["alice"] = result
self.assertEqual(self.request("auth/me")["username"], "alice")
def test_real_leagues_exist_before_data_ready(self):
league, _ = self.league(demo=False)
view = self.view(league)
self.assertFalse(view["readiness"]["ready"])
self.assertEqual(len(view["teams"]), 2)
self.request("draft/start", {"league_id": league}, status=409)
self.assertEqual(self.request("candidates", {"limit": 100})["total"], 115)
self.assertEqual(len(self.request("candidates", {"offset": 100, "limit": 100})["items"]), 15)
def test_private_queue_timeout_and_snake(self):
league = self.start()
who, view = self.turn(league)
candidate = view["legal_candidate_ids"][-1]
self.request("draft/queue", {"league_id": league, "candidates": [candidate]}, who)
other = "alice" if who == "bob" else "bob"
self.assertNotIn(candidate, json.dumps(self.view(league, other)["teams"]))
self.now += 120
self.request("tick")
view = self.view(league)
self.assertEqual(view["picks"][0]["candidate_id"], candidate)
self.assertEqual(view["picks"][0]["automatic"], 1)
second = view["picker"]["id"]
self.pick(league)
self.assertEqual(self.view(league)["picker"]["id"], second)
def test_pause_restart_and_frozen_settings(self):
league = self.start()
self.now += 50
self.request("draft/pause", {"league_id": league})
self.assertEqual(self.view(league)["draft"]["remaining"], 70)
self.now += 1000
self.request("tick")
self.assertEqual(len(self.view(league)["picks"]), 0)
self.request("draft/resume", {"league_id": league})
self.assertEqual(self.view(league)["draft"]["deadline"], self.now + 70)
self.request("restart")
view = self.view(league)
self.assertEqual(view["league"]["status"], "paused")
self.assertEqual(view["draft"]["remaining"], 120)
self.assertEqual(json.loads(view["league"]["rules_json"])["version"], "v2-november-share")
self.request("league/settings", {"league_id": league, "name": "Changed", "catalog_id": "demo-2026"}, status=409)
def test_idempotent_concurrent_picks_and_timeout_boundary(self):
league = self.start()
who, view = self.turn(league)
data = {"league_id": league, "candidate_id": view["legal_candidate_ids"][0], "pick_number": 0, "request_id": "same-request"}
with concurrent.futures.ThreadPoolExecutor(2) as pool:
results = list(pool.map(lambda _: self.request("draft/pick", data, who), range(2)))
self.assertEqual(results[0], results[1])
self.assertEqual(len(self.view(league)["picks"]), 1)
data["candidate_id"] = view["legal_candidate_ids"][1]
self.request("draft/pick", data, who, status=409)
who, view = self.turn(league)
self.now = view["draft"]["deadline"]
self.request("draft/pick", {"league_id": league, "candidate_id": view["legal_candidate_ids"][0], "pick_number": 1, "request_id": "expired"}, who, status=409)
self.request("tick")
self.assertEqual(len(self.view(league)["picks"]), 2)
def test_full_draft_capacity_and_league_exclusivity(self):
league = self.start(players=("alice", "bob", "carol"))
for _ in range(36):
self.now += 120
self.request("tick")
view = self.view(league)
self.assertEqual(view["league"]["status"], "drafted")
self.assertEqual(len({p["candidate_id"] for p in view["picks"]}), 36)
for team in view["teams"]:
self.assertEqual(len({p["slot"] for p in view["picks"] if p["team_id"] == team["id"]}), 12)
second = self.start()
self.pick(second, view["picks"][0]["candidate_id"])
self.assertEqual(len(self.view(second)["picks"]), 1)
def test_ingest_corrections_runoff_retraction_and_outage(self):
league = self.start()
self.pick(league, "demo-house-01-1")
counts = {"race_id": "demo-house-01", "kind": "counts", "round": "november_first", "source_time": "2026-11-04T01:00:00Z", "total_votes": 1000, "votes": [{"candidate_id": "demo-house-01-1", "votes": 400}]}
outcome = {"race_id": "demo-house-01", "kind": "outcome", "round": "general", "source_time": "2026-11-04T01:00:00Z", "status": "pending_round", "winner_id": None}
payload = {"schema_version": 1, "observations": [counts, outcome]}
result = self.ingest(payload, configure=True)
self.assertFalse(result["verified"])
def score():
return next(row["roster"][0]["points"] for row in self.request("standings", {"league_id": league})["teams"] if row["roster"])
self.assertEqual(score(), 40)
outcome.update(round="runoff", source_time="2026-12-06T01:00:00Z", status="called", winner_id="demo-house-01-1")
self.ingest({"schema_version": 1, "observations": [outcome]})
self.assertEqual(score(), 90)
outcome.update(source_time="2026-12-06T02:00:00Z", status="reporting", winner_id=None)
self.ingest({"schema_version": 1, "observations": [outcome]})
self.assertEqual(score(), 40)
counts.update(source_time="2026-11-04T03:00:00Z", votes=[{"candidate_id": "demo-house-01-1", "votes": 450}])
self.ingest({"schema_version": 1, "observations": [counts]})
self.assertEqual(score(), 45)
counts["round"] = "runoff"
self.assertTrue(self.ingest({"schema_version": 1, "observations": [counts]})["quarantined"])
self.assertEqual(score(), 45)
counts["round"] = "november_first"
counts["source_time"] = "2026-11-04T00:00:00Z"
counts["votes"][0]["votes"] = 100
self.ingest({"schema_version": 1, "observations": [counts]})
self.assertEqual(score(), 45)
def test_twelve_teams_preserve_party_capacity_and_finish(self):
league, invite = self.league(players=("alice", "bob", "carol"), maximum=12)
for index in range(9):
name = f"player_{index}"
self.identities[name] = self.request("auth/register", {"username": name, "password": PASSWORD, "team_name": name, "invite": invite}, who="nobody", raw=True)
self.request("draft/start", {"league_id": league})
self.pick(league, "demo-house-01-1")
# Once someone has their D slot, a second D would consume another team's only required supply.
with sqlite3.connect(self.database) as database:
first = database.execute("SELECT team_id FROM picks WHERE number=0 AND league_id=?", (league,)).fetchone()[0]
database.execute("UPDATE drafts SET pick_number=23 WHERE league_id=?", (league,))
who, view = self.turn(league)
self.assertEqual(view["picker"]["id"], first)
self.assertNotIn("demo-house-02-1", view["legal_candidate_ids"])
with sqlite3.connect(self.database) as database:
database.execute("UPDATE drafts SET pick_number=1 WHERE league_id=?", (league,))
for _ in range(143):
self.now += 120
self.request("tick")
view = self.view(league)
self.assertEqual(view["league"]["status"], "drafted")
self.assertEqual(len(view["picks"]), 144)
self.assertEqual(len({row["candidate_id"] for row in view["picks"]}), 144)
def test_openamerica_mapping_and_fusion_lines(self):
config = {"id": "openamerica-test", "kind": "openamerica", "url": "https://openamerica.io/elections/results.json?year=2026&office=House", "office": "House", "catalog_id": "demo-2026", "counts_until": "2099-01-01T00:00:00Z", "races": [{"race_id": "demo-house-01", "role": "counts", "provider_id": "MI-01", "candidates": {"Test Candidate|D": "demo-house-01-1", "Test Candidate|W": "demo-house-01-1", "Write-in|": None}}]}
self.command(INGEST, "configure", self.file("oa-config.json", config))
# Synthetic transport fixture. It cannot verify a real feed.
payload = {"api": 1, "year": 2026, "office": "House", "stale": False, "as_of": "2026-11-04T01:00:00+00:00", "races": {"MI-01": {"total": 100, "cands": [{"n": "Test Candidate", "p": "D", "v": 60}, {"n": "Test Candidate", "p": "W", "v": 10}]}}}
result = self.command(INGEST, "replay", "openamerica-test", self.file("oa-payload.json", payload))
self.assertEqual(result["changed"], 1)
self.assertFalse(result["verified"])
with sqlite3.connect(self.database) as database:
normalized = json.loads(database.execute("SELECT body FROM observations").fetchone()[0])
self.assertEqual(normalized["votes"], [{"candidate_id": "demo-house-01-1", "votes": 70}])
payload["races"]["MI-01"]["cands"].append({"n": "Unknown", "p": "R", "v": 20})
self.assertTrue(self.command(INGEST, "replay", "openamerica-test", self.file("oa-payload.json", payload))["quarantined"])
activation = subprocess.run([str(INGEST), "--database", str(self.database), "activate", "openamerica-test"], capture_output=True, text=True)
self.assertNotEqual(activation.returncode, 0)
def test_ratings_import_permission_immutability_and_freeze(self):
league, _ = self.league()
ratings = json.loads((ROOT / "data/demo/ratings.json").read_text())
ratings["id"] = "commissioner-ratings"
data = {"league_id": league, "document": ratings}
self.request("league/ratings", data, "bob", status=403)
self.request("league/ratings", data)
self.request("league/ratings", data)
self.assertEqual(self.view(league)["league"]["ratings_id"], ratings["id"])
ratings["entries"][0]["rating"] = "toss_up"
self.request("league/ratings", data, status=409)
self.request("draft/start", {"league_id": league})
ratings["id"] = "too-late"
self.request("league/ratings", data, status=409)
self.request("league/name", {"league_id": league, "name": "Renamed after start"})
self.assertEqual(self.view(league)["league"]["name"], "Renamed after start")
def test_one_verified_race_cannot_activate_whole_feed(self):
self.ingest({"schema_version": 1, "observations": []}, configure=True)
# Fixture-only network evidence, to exercise the coverage gate without sockets.
with sqlite3.connect(self.database) as database:
database.execute("UPDATE sources SET verified=1,last_error=NULL WHERE id='fixture'")
database.execute("INSERT INTO source_checks VALUES('fixture','demo-house-01','counts',?)", (self.now,))
result = subprocess.run([str(INGEST), "--database", str(self.database), "activate", "fixture"], capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
with sqlite3.connect(self.database) as database:
self.assertEqual(database.execute("SELECT COUNT(*) FROM coverage").fetchone()[0], 0)
def test_candidate_watch_deduplicates_and_detects_return_to_previous_payload(self):
config = {"id": "watch", "kind": "candidate-watch", "url": "https://example.org/official-candidates", "catalog_id": "demo-2026", "races": []}
self.command(INGEST, "configure", self.file("watch.json", config))
first = self.file("first.json", {"version": 1})
second = self.file("second.json", {"version": 2})
self.command(INGEST, "replay", "watch", first)
with sqlite3.connect(self.database) as database:
database.execute("UPDATE sources SET last_changed=123,etag='external-etag' WHERE id='watch'")
self.command(INGEST, "replay", "watch", first)
with sqlite3.connect(self.database) as database:
self.assertEqual(database.execute("SELECT last_changed FROM sources WHERE id='watch'").fetchone()[0], 123)
self.command(INGEST, "replay", "watch", second)
with sqlite3.connect(self.database) as database:
database.execute("UPDATE sources SET last_changed=456 WHERE id='watch'")
self.command(INGEST, "replay", "watch", first)
with sqlite3.connect(self.database) as database:
self.assertGreater(database.execute("SELECT last_changed FROM sources WHERE id='watch'").fetchone()[0], 456)
self.assertEqual(database.execute("SELECT COUNT(*) FROM source_payloads").fetchone()[0], 2)
self.assertEqual(database.execute("SELECT verified FROM sources WHERE id='watch'").fetchone()[0], 0)
def test_catalog_documents_survive_migration_and_backup(self):
backup = self.path / "backup.sqlite"
self.command(ADMIN, "backup", backup)
with sqlite3.connect(backup) as database:
self.assertEqual(database.execute("PRAGMA integrity_check").fetchone()[0], "ok")
self.assertEqual(database.execute("SELECT COUNT(*) FROM documents").fetchone()[0], 3)
hashes = database.execute("SELECT password_hash FROM users").fetchall()
self.assertEqual(len(set(row[0] for row in hashes)), 3)
self.assertTrue(all(row[0].startswith("scrypt$131072$8$1$") for row in hashes))
if __name__ == "__main__":
unittest.main()