Files
boker/tests/test_phase2_auth_leagues.py
T

503 lines
18 KiB
Python

from __future__ import annotations
from datetime import date
import unittest
from boker.db import database_extensions_available
@unittest.skipUnless(
database_extensions_available(),
"Database dependencies are not installed.",
)
class Phase2AuthLeagueRouteTests(unittest.TestCase):
def setUp(self):
from app import create_app
from boker.db import db
self.db = db
self.app = create_app(
{
"TESTING": True,
"SECRET_KEY": "test-secret",
"SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
"WTF_CSRF_ENABLED": False,
}
)
self.ctx = self.app.app_context()
self.ctx.push()
self.db.create_all()
self.client = self.app.test_client()
def tearDown(self):
self.db.session.remove()
self.db.drop_all()
self.db.engine.dispose()
self.ctx.pop()
def league_path(self, league=None) -> str:
if league is None:
from boker.db_models import League
league = League.query.one()
return f"/l/{league.url_ref}"
def create_verified_user(self, email="owner@example.com"):
from boker.db_models import utc_now
from boker.league_repositories import create_user
user = create_user(email, "password123")
self.db.session.flush()
user.email_verified_at = utc_now()
self.db.session.commit()
return user
def login_as(self, user):
with self.client.session_transaction() as flask_session:
flask_session["user_id"] = user.id
def create_logged_in_league(self, email="owner@example.com", name="Friday Poker"):
from boker.league_repositories import create_league
owner = self.create_verified_user(email)
league = create_league(owner, name, name.lower().replace(" ", "-"))
self.db.session.commit()
self.login_as(owner)
return owner, league, self.league_path(league)
def test_registration_starts_email_verification_before_login(self):
from boker.db_models import User
response = self.client.post(
"/account/register",
data={
"email": "Owner@Example.com",
"password": "password123",
"confirm_password": "password123",
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], "/account/verify-email")
user = User.query.one()
self.assertEqual(user.email, "owner@example.com")
self.assertIsNone(user.email_verified_at)
self.assertIsNotNone(user.email_verification_code_hash)
with self.client.session_transaction() as flask_session:
self.assertNotIn("user_id", flask_session)
self.assertEqual(flask_session["pending_verification_user_id"], user.id)
def test_verified_user_can_create_league_and_owner_membership(self):
from boker.db_models import League, LeagueMembership, User
user = self.create_verified_user("Owner@Example.com")
self.login_as(user)
response = self.client.post(
"/leagues/new",
data={
"name": "Friday Poker",
"slug": "",
"description": "Private home league",
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(User.query.count(), 1)
league = League.query.one()
self.assertEqual(response.headers["Location"], self.league_path(league))
membership = LeagueMembership.query.one()
self.assertEqual(league.slug, "friday-poker")
self.assertEqual(membership.user_id, user.id)
self.assertEqual(membership.role, "owner")
dashboard = self.client.get(self.league_path(league))
self.assertEqual(dashboard.status_code, 200)
self.assertIn(b"Friday Poker", dashboard.data)
def test_legacy_admin_routes_are_not_registered_by_default(self):
response = self.client.get("/admin/login")
self.assertEqual(response.status_code, 404)
def test_duplicate_league_slug_is_allowed_because_route_uses_public_key(self):
owner = self.create_verified_user()
self.login_as(owner)
self.client.post("/leagues/new", data={"name": "Friday Poker"})
self.client.post("/leagues/new", data={"name": "Friday Poker"})
from boker.db_models import League
self.assertEqual(
[league.slug for league in League.query.order_by(League.slug.asc()).all()],
["friday-poker", "friday-poker"],
)
def test_legacy_uuid_league_route_redirects_to_public_key_route(self):
from boker.db import db
from boker.league_repositories import create_league, create_user
owner = create_user("owner@example.com", "password123")
db.session.flush()
league = create_league(owner, "Private League", "private-league")
db.session.commit()
with self.client.session_transaction() as flask_session:
flask_session["user_id"] = owner.id
response = self.client.get(f"/l/{league.id}/{league.slug}")
self.assertEqual(response.status_code, 301)
self.assertEqual(response.headers["Location"], self.league_path(league))
def test_league_dashboard_requires_membership(self):
from boker.db import db
from boker.league_repositories import create_league, create_user
owner = create_user("owner@example.com", "password123")
other = create_user("other@example.com", "password123")
db.session.flush()
league = create_league(owner, "Private League", "private-league")
db.session.commit()
with self.client.session_transaction() as flask_session:
flask_session["user_id"] = other.id
response = self.client.get(self.league_path(league))
self.assertEqual(response.status_code, 403)
def test_owner_can_add_archive_and_reactivate_player(self):
from boker.db_models import Player
_, _, league_base = self.create_logged_in_league()
response = self.client.post(
f"{league_base}/players",
data={
"display_name": "Alex",
"slug": "",
"notes": "Usually hosts.",
},
)
self.assertEqual(response.status_code, 302)
player = Player.query.one()
self.assertEqual(player.display_name, "Alex")
self.assertEqual(player.slug, "alex")
self.assertEqual(player.status, "active")
self.assertEqual(player.notes, "Usually hosts.")
duplicate = self.client.post(
f"{league_base}/players",
data={"display_name": " alex "},
)
self.assertEqual(duplicate.status_code, 200)
self.assertIn(b"already exists", duplicate.data)
self.assertEqual(Player.query.count(), 1)
archive = self.client.post(f"{league_base}/players/{player.id}/archive")
self.assertEqual(archive.status_code, 302)
self.db.session.refresh(player)
self.assertEqual(player.status, "archived")
reactivate = self.client.post(f"{league_base}/players/{player.id}/reactivate")
self.assertEqual(reactivate.status_code, 302)
self.db.session.refresh(player)
self.assertEqual(player.status, "active")
def test_viewer_can_view_players_but_cannot_add_or_archive(self):
from boker.db import db
from boker.db_models import LeagueMembership, Player
from boker.league_repositories import create_league, create_player, create_user
owner = create_user("owner@example.com", "password123")
viewer = create_user("viewer@example.com", "password123")
db.session.flush()
league = create_league(owner, "Private League", "private-league")
player = create_player(league.id, "Alex", "alex")
db.session.flush()
db.session.add(
LeagueMembership(
league_id=league.id,
user_id=viewer.id,
role="viewer",
)
)
db.session.commit()
with self.client.session_transaction() as flask_session:
flask_session["user_id"] = viewer.id
league_base = self.league_path(league)
response = self.client.get(f"{league_base}/players")
self.assertEqual(response.status_code, 200)
self.assertIn(b"Alex", response.data)
self.assertNotIn(b"Add player", response.data)
add = self.client.post(
f"{league_base}/players",
data={"display_name": "Blair"},
)
self.assertEqual(add.status_code, 403)
self.assertEqual(Player.query.count(), 1)
archive = self.client.post(f"{league_base}/players/{player.id}/archive")
self.assertEqual(archive.status_code, 403)
self.db.session.refresh(player)
self.assertEqual(player.status, "active")
def test_owner_can_create_same_day_sessions_and_close_them(self):
from boker.db_models import LedgerEvent, PokerSession
_, _, league_base = self.create_logged_in_league()
first = self.client.post(
f"{league_base}/sessions",
data={
"session_date": "2026-10-02",
"label": "",
"status": "open",
"notes": "Main game",
},
)
second = self.client.post(
f"{league_base}/sessions",
data={
"session_date": "2026-10-02",
"label": "Late table",
"status": "open",
},
)
self.assertEqual(first.status_code, 302)
self.assertEqual(second.status_code, 302)
sessions = PokerSession.query.order_by(PokerSession.sequence_on_date.asc()).all()
self.assertEqual([session.sequence_on_date for session in sessions], [1, 2])
self.assertEqual(sessions[0].status, "open")
self.assertEqual(sessions[0].notes, "Main game")
self.assertEqual(sessions[1].display_label, "Late table")
events = LedgerEvent.query.order_by(LedgerEvent.created_at.asc()).all()
self.assertEqual([event.event_type for event in events], ["session_open", "session_open"])
close = self.client.post(f"{league_base}/sessions/{sessions[0].id}/close")
self.assertEqual(close.status_code, 302)
self.db.session.refresh(sessions[0])
self.assertEqual(sessions[0].status, "closed")
self.assertIsNotNone(sessions[0].closed_at)
self.assertEqual(LedgerEvent.query.count(), 3)
self.assertEqual(
LedgerEvent.query.order_by(LedgerEvent.created_at.desc()).first().event_type,
"session_close",
)
def test_closed_session_creation_does_not_append_marker_event_until_opened(self):
from boker.db_models import LedgerEvent, PokerSession
_, _, league_base = self.create_logged_in_league()
self.client.post(
f"{league_base}/sessions",
data={"session_date": "2026-10-02", "status": "closed"},
)
session = PokerSession.query.one()
self.assertEqual(session.status, "closed")
self.assertEqual(LedgerEvent.query.count(), 0)
open_response = self.client.post(f"{league_base}/sessions/{session.id}/open")
self.assertEqual(open_response.status_code, 302)
self.db.session.refresh(session)
self.assertEqual(session.status, "open")
self.assertIsNotNone(session.opened_at)
self.assertEqual(LedgerEvent.query.one().event_type, "session_open")
def test_viewer_can_view_sessions_but_cannot_create_or_close(self):
from boker.db import db
from boker.db_models import LeagueMembership, PokerSession
from boker.league_repositories import create_league, create_poker_session, create_user
owner = create_user("owner@example.com", "password123")
viewer = create_user("viewer@example.com", "password123")
db.session.flush()
league = create_league(owner, "Private League", "private-league")
session = create_poker_session(league.id, date(2026, 10, 2), status="open")
db.session.flush()
db.session.add(
LeagueMembership(
league_id=league.id,
user_id=viewer.id,
role="viewer",
)
)
db.session.commit()
with self.client.session_transaction() as flask_session:
flask_session["user_id"] = viewer.id
league_base = self.league_path(league)
response = self.client.get(f"{league_base}/sessions")
self.assertEqual(response.status_code, 200)
self.assertIn(b"2026-10-02", response.data)
self.assertNotIn(b"Create session", response.data)
create = self.client.post(
f"{league_base}/sessions",
data={"session_date": "2026-10-03"},
)
self.assertEqual(create.status_code, 403)
self.assertEqual(PokerSession.query.count(), 1)
close = self.client.post(f"{league_base}/sessions/{session.id}/close")
self.assertEqual(close.status_code, 403)
self.db.session.refresh(session)
self.assertEqual(session.status, "open")
def test_owner_can_append_events_and_view_db_backed_stats(self):
from boker.db_models import LedgerEvent, Player, PokerSession
_, _, league_base = self.create_logged_in_league()
self.client.post(f"{league_base}/players", data={"display_name": "Alex"})
self.client.post(
f"{league_base}/sessions",
data={"session_date": "2026-10-02", "status": "open"},
)
player = Player.query.one()
session = PokerSession.query.one()
buyin = self.client.post(
f"{league_base}/sessions/{session.id}",
data={
"player_id": player.id,
"event_type": "buyin",
"amount": "20",
},
)
cashout = self.client.post(
f"{league_base}/sessions/{session.id}",
data={
"player_id": player.id,
"event_type": "cashout",
"amount": "35",
},
)
paid = self.client.post(
f"{league_base}/sessions/{session.id}",
data={
"player_id": player.id,
"event_type": "paid_out",
"amount": "15",
},
)
self.assertEqual(buyin.status_code, 302)
self.assertEqual(cashout.status_code, 302)
self.assertEqual(paid.status_code, 302)
self.assertEqual(LedgerEvent.query.filter_by(player_id=player.id).count(), 3)
leaderboard = self.client.get(f"{league_base}/leaderboard")
self.assertEqual(leaderboard.status_code, 200)
self.assertIn(b"Alex", leaderboard.data)
self.assertIn(b"15.00", leaderboard.data)
detail = self.client.get(f"{league_base}/sessions/{session.id}")
self.assertEqual(detail.status_code, 200)
self.assertIn(b"cashout", detail.data)
self.assertIn(b"paid out", detail.data)
player_detail = self.client.get(f"{league_base}/players/{player.id}")
self.assertEqual(player_detail.status_code, 200)
self.assertIn(b"Total net", player_detail.data)
self.assertIn(b"20.00", player_detail.data)
def test_league_ledger_shows_and_resolves_player_debts(self):
from boker.db_models import LedgerEvent, Player, PokerSession
_, _, league_base = self.create_logged_in_league()
self.client.post(f"{league_base}/players", data={"display_name": "Alex"})
self.client.post(
f"{league_base}/sessions",
data={"session_date": "2026-10-02", "status": "open"},
)
player = Player.query.one()
session = PokerSession.query.one()
self.client.post(
f"{league_base}/sessions/{session.id}",
data={"player_id": player.id, "event_type": "front", "amount": "50"},
)
self.client.post(
f"{league_base}/sessions/{session.id}",
data={"player_id": player.id, "event_type": "cashout", "amount": "20"},
)
ledger = self.client.get(f"{league_base}/ledger")
self.assertEqual(ledger.status_code, 200)
self.assertIn(b"Debt resolution", ledger.data)
self.assertIn(b"Front receivables", ledger.data)
self.assertIn(b"30.00", ledger.data)
repayment = self.client.post(
f"{league_base}/ledger",
data={
"session_id": session.id,
"player_id": player.id,
"event_type": "debt_repayment",
"amount": "10",
},
)
self.assertEqual(repayment.status_code, 302)
self.assertEqual(
LedgerEvent.query.order_by(LedgerEvent.created_at.desc()).first().event_type,
"debt_repayment",
)
ledger = self.client.get(f"{league_base}/ledger")
self.assertEqual(ledger.status_code, 200)
self.assertIn(b"20.00", ledger.data)
def test_viewer_cannot_append_ledger_events(self):
from boker.db import db
from boker.db_models import LeagueMembership, LedgerEvent
from boker.league_repositories import (
create_league,
create_player,
create_poker_session,
create_user,
)
owner = create_user("owner@example.com", "password123")
viewer = create_user("viewer@example.com", "password123")
db.session.flush()
league = create_league(owner, "Private League", "private-league")
player = create_player(league.id, "Alex", "alex")
session = create_poker_session(league.id, date(2026, 10, 2), status="open")
db.session.flush()
db.session.add(
LeagueMembership(
league_id=league.id,
user_id=viewer.id,
role="viewer",
)
)
db.session.commit()
with self.client.session_transaction() as flask_session:
flask_session["user_id"] = viewer.id
response = self.client.post(
f"{self.league_path(league)}/sessions/{session.id}",
data={
"player_id": player.id,
"event_type": "buyin",
"amount": "20",
},
)
self.assertEqual(response.status_code, 403)
self.assertEqual(LedgerEvent.query.count(), 0)
if __name__ == "__main__":
unittest.main()