Files
python-tdd/src/functional_tests/test_gatekeeper.py

586 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import time
from datetime import timedelta
from django.utils import timezone
from selenium.webdriver.common.by import By
from .base import FunctionalTest
from apps.applets.models import Applet
from apps.epic.models import Room, GateSlot, select_token
from apps.lyric.models import Token, User
class GatekeeperTest(FunctionalTest):
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
Applet.objects.get_or_create(
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
)
def test_founder_creates_room_and_sees_gatekeeper(self):
# 1. Log in, navigate to gameboard
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
# 2. New Game applet has room name input, create button
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_new_game")
)
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Test Room")
self.browser.find_element(By.ID, "id_create_game_btn").click()
# 3. User is redirected to Gatekeeper page for new room
self.wait_for(
lambda: self.assertIn("/gameboard/room/", self.browser.current_url)
)
self.wait_for(
lambda: self.assertIn("/gate/", self.browser.current_url)
)
# 4. Page shows room name, GATHERING status
body = self.browser.find_element(By.TAG_NAME, "body")
self.assertIn("TEST ROOM", body.text)
self.assertIn("GATHERING GAMERS", body.text)
# 5. Six token slot circles are visible, all empty
slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot")
self.assertEqual(len(slots), 6)
for slot in slots:
self.assertIn("empty", slot.get_attribute("class"))
# 6. Shared coin slot is present; no individual drop buttons
self.browser.find_element(By.CSS_SELECTOR, ".token-slot")
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".drop-token-btn")), 0
)
def test_founder_drops_token_and_slot_fills(self):
# 1. Set up: log in, create room, arrive at gatekeeper
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_new_game_name")
)
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Dragon's Den")
self.browser.find_element(By.ID, "id_create_game_btn").click()
self.wait_for(
lambda: self.assertIn("/gate/", self.browser.current_url)
)
# 2. Founder clicks Insert Token via the shared coin slot
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
# 3. Slot 1 (lowest) now shows OK button; slot is reserved
ok_btn = self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
)
)
# 4. Founder clicks OK → slot fills
ok_btn.click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
slots = self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot")
self.assertIn("filled", slots[0].get_attribute("class"))
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
)
def test_room_appears_in_my_games_after_creation(self):
# 1. Set up founder, game room, name
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_new_game_name")
)
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Dragon's Den")
self.browser.find_element(By.ID, "id_create_game_btn").click()
self.wait_for(
lambda: self.assertIn("/gate/", self.browser.current_url)
)
# 2. Navigate back to gameboard
self.browser.get(self.live_server_url + "/gameboard/")
my_games = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_my_games")
)
self.assertIn("Dragon's Den", my_games.text)
def test_second_gamer_drops_token_into_open_slot(self):
# 1. Founder creates room, confirms slot 1
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_new_game_name")
)
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Dragon's Den")
self.browser.find_element(By.ID, "id_create_game_btn").click()
self.wait_for(
lambda: self.assertIn("/gate/", self.browser.current_url)
)
room_url = self.browser.current_url
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
# 2. Founder invites friend
invite_input = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_invite_email")
)
invite_input.send_keys("friend@test.io")
self.browser.find_element(By.ID, "id_invite_btn").click()
# 3. Friend logs in, sees invitation in My Games
self.create_pre_authenticated_session("friend@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
my_games = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_applet_my_games")
)
self.assertIn("Dragon's Den", my_games.text)
# 4. Friend follows link to gatekeeper
self.browser.find_element(By.LINK_TEXT, "Dragon's Den").click()
# 5. Friend drops token via coin slot and confirms
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
# 6. Now two slots filled
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot.filled")), 2
)
)
def test_gate_opens_when_all_slots_filled(self):
# 1. Founder creates room
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(
lambda: self.browser.find_element(By.ID, "id_new_game_name")
)
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Dragon's Den")
self.browser.find_element(By.ID, "id_create_game_btn").click()
self.wait_for(
lambda: self.assertIn("/gate/", self.browser.current_url)
)
# 2. Founder confirms slot 1 via coin slot
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
# 3. Fill slots 26 directly via ORM
room = Room.objects.get(name="Dragon's Den")
for i, email in enumerate([
"g2@test.io", "g3@test.io", "g4@test.io", "g5@test.io", "g6@test.io"
], start=2):
gamer = User.objects.create(email=email)
slot = room.gate_slots.get(slot_number=i)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
room.refresh_from_db()
room.gate_status = Room.OPEN
room.save()
# 4. Gate shows launch button when all slots filled
# update this for ASGI after channels sprint!
self.browser.refresh()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".launch-game-btn")
)
def test_owner_can_delete_room_via_gear_menu(self):
self.create_pre_authenticated_session("founder@test.io")
self.browser.get(self.live_server_url + "/gameboard/")
self.wait_for(lambda: self.browser.find_element(By.ID, "id_new_game_name"))
self.browser.find_element(By.ID, "id_new_game_name").send_keys("Doomed Room")
self.browser.find_element(By.ID, "id_create_game_btn").click()
self.wait_for(lambda: self.assertIn("/gate/", self.browser.current_url))
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-danger")
).click()
self.wait_for(lambda: self.assertEqual(
self.browser.current_url, self.live_server_url + "/gameboard/"
))
self.assertFalse(Room.objects.filter(name="Doomed Room").exists())
def test_gatekeeper_overlay_persists_after_htmx_poll(self):
# 1. Create room directly (GATHERING) and navigate to its gate URL
self.create_pre_authenticated_session("founder@test.io")
founder = User.objects.get(email="founder@test.io")
room = Room.objects.create(name="Persistent Room", owner=founder)
self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/")
# 2. Assert overlay visible on initial page load
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-overlay")
)
# 3. Wait for HTMX poll cycle to fire (poll interval is 3s)
time.sleep(4)
# 4. Assert overlay still present and visible after poll
overlays = self.browser.find_elements(By.CSS_SELECTOR, ".gate-overlay")
self.assertEqual(len(overlays), 1)
self.assertTrue(overlays[0].is_displayed())
def test_gamer_can_abandon_room_via_gear_menu(self):
founder = User.objects.create(email="founder@test.io")
room = Room.objects.create(name="Dragon's Den", owner=founder)
slot = room.gate_slots.get(slot_number=2)
self.create_pre_authenticated_session("gamer@test.io")
gamer, _ = User.objects.get_or_create(email="gamer@test.io")
slot.gamer = gamer
slot.status = "FILLED"
slot.save()
self.browser.get(self.live_server_url + f"/gameboard/room/{room.id}/gate/")
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon")
).click()
self.wait_for(lambda: self.assertEqual(
self.browser.current_url, self.live_server_url + "/gameboard/"
))
slot.refresh_from_db()
self.assertEqual(slot.status, "EMPTY")
self.assertIsNone(slot.gamer)
class CoinSlotTest(FunctionalTest):
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
Applet.objects.get_or_create(
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
)
self.create_pre_authenticated_session("founder@test.io")
self.founder = User.objects.get(email="founder@test.io")
self.room = Room.objects.create(name="Coin Room", owner=self.founder)
self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"
def test_coin_slot_active_for_eligible_gamer(self):
# Gamer with no slot arrives at gatekeeper — coin slot is active
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.active")
)
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
def test_drop_token_reserves_lowest_empty_slot(self):
# Gamer drops token; slot 1 (lowest) becomes reserved with OK button
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
)
)
slot = self.room.gate_slots.get(slot_number=1)
slot.refresh_from_db()
self.assertEqual(slot.status, GateSlot.RESERVED)
self.assertEqual(slot.gamer, self.founder)
def test_confirm_fills_slot_and_removes_ok_button(self):
# Drop then confirm → slot 1 FILLED, OK button gone
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
)
slot = self.room.gate_slots.get(slot_number=1)
slot.refresh_from_db()
self.assertEqual(slot.status, GateSlot.FILLED)
def test_gamer_can_return_pending_token(self):
# Drop then return via Push to Return → slot remains empty
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
# Push to Return appears in coin slot
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-return-btn")
).click()
# Slot 1 still empty; coin slot active again
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.active")
)
slot = self.room.gate_slots.get(slot_number=1)
slot.refresh_from_db()
self.assertEqual(slot.status, GateSlot.EMPTY)
def test_coin_slot_locked_while_another_token_is_pending(self):
# Pre-set slot 1 as RESERVED by a different user
other = User.objects.create(email="other@test.io")
slot = self.room.gate_slots.get(slot_number=1)
slot.gamer = other
slot.status = GateSlot.RESERVED
slot.reserved_at = timezone.now()
slot.save()
# Current user (founder) sees coin slot locked
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.locked")
)
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, "button.token-rails")), 0
)
def test_last_gamer_sees_pick_roles_button(self):
# Fill slots 15 via ORM; slot 6 empty
for i, email in enumerate([
"g1@test.io", "g2@test.io", "g3@test.io", "g4@test.io", "g5@test.io"
], start=1):
gamer = User.objects.create(email=email)
slot = self.room.gate_slots.get(slot_number=i)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
# Founder (no slot yet) drops token → gets slot 6
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
# Slot 6 shows PICK ROLES instead of OK
self.wait_for(
lambda: self.assertIn(
"PICK ROLES",
self.browser.find_element(
By.CSS_SELECTOR, ".gate-slot[data-slot='6']"
).text,
)
)
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot .btn-confirm")), 0
)
class TokenPriorityTest(FunctionalTest):
def setUp(self):
super().setUp()
Applet.objects.get_or_create(
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
)
Applet.objects.get_or_create(
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
)
self.create_pre_authenticated_session("gamer@test.io")
self.gamer = User.objects.get(email="gamer@test.io")
self.room = Room.objects.create(name="Token Room", owner=self.gamer)
self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"
self.coin = Token.objects.get(user=self.gamer, token_type=Token.COIN)
def test_coin_is_used_by_default(self):
# 1. COIN token created at signup, not yet leased to a room
self.assertEqual(self.coin.token_type, Token.COIN)
self.assertIsNone(self.coin.current_room)
# 2. Gamer drops token and confirms
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
# 3. Coin is now leased to this room, page not refreshed
self.assertEqual(self.browser.current_url, self.gate_url)
self.coin.refresh_from_db()
self.assertEqual(self.coin.current_room, self.room)
def test_free_token_used_when_coin_in_use(self):
# 1. Coin already leased to another room
other_room = Room.objects.create(name="Other Room", owner=self.gamer)
self.coin.current_room = other_room
self.coin.save()
# 2. Gamer has one unexpired free token (signup gives one; delete it and add fresh)
Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
Token.objects.create(
user=self.gamer,
token_type=Token.FREE,
expires_at=timezone.now() + timedelta(days=7),
)
# 3. Gamer drops token → Free Token consumed
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
self.assertEqual(
Token.objects.filter(user=self.gamer, token_type=Token.FREE).count(), 0
)
# 4. Coin untouched, still leased to other room
self.assertEqual(self.browser.current_url, self.gate_url)
self.coin.refresh_from_db()
self.assertEqual(self.coin.current_room, other_room)
def test_tithe_token_used_when_free_tokens_exhausted(self):
# 1. Coin in use, no Free Tokens, one Tithe Token
other_room = Room.objects.create(name="Other Room", owner=self.gamer)
self.coin.current_room = other_room
self.coin.save()
Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
tithe = Token.objects.create(user=self.gamer, token_type=Token.TITHE)
# 2. Gamer drops token → tithe consumed
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
# Tithe row deleted, page hasn't refreshed
self.assertEqual(self.browser.current_url, self.gate_url)
self.assertFalse(Token.objects.filter(pk=tithe.pk).exists())
def test_slot_blocked_when_no_tokens_available(self):
# Coin in use, no Free Tokens, no Tithe Tokens → depleted state
other_room = Room.objects.create(name="Other Room", owner=self.gamer)
self.coin.current_room = other_room
self.coin.save()
Token.objects.filter(user=self.gamer, token_type=Token.FREE).delete()
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.depleted")
)
self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, "button.token-rails")), 0
)
def test_staff_backstage_pass_bypasses_token_cost(self):
# 1. Staff user has a PASS token
self.gamer.is_staff = True
self.gamer.save()
pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
# 2. Drops token, confirms as normal
self.browser.get(self.gate_url)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, "button.token-rails")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-confirm")
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
# 3. Pass not consumed, coin not leased; no reload
self.assertEqual(self.browser.current_url, self.gate_url)
self.assertTrue(Token.objects.filter(pk=pass_token.pk).exists())
self.coin.refresh_from_db()
self.assertIsNone(self.coin.current_room)
class GameKitInsertTest(FunctionalTest):
"""Token selected from Game Kit, inserted via token-slot click."""
def setUp(self):
super().setUp()
self.create_pre_authenticated_session("gamer@insert.io")
self.gamer = User.objects.get(email="gamer@insert.io")
self.coin = self.gamer.tokens.filter(token_type=Token.COIN).first()
self.room = Room.objects.create(name="Insert Room", owner=self.gamer)
self.gate_url = self.live_server_url + f"/gameboard/room/{self.room.id}/gate/"
def _select_token_from_kit(self, token):
self.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, f"[data-token-id='{token.id}']"
).click()
)
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".token-slot.ready")
)
def test_coin_insert_via_kit_reserves_slot(self):
self.browser.get(self.gate_url)
self._select_token_from_kit(self.coin)
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.reserved")
)
self.assertEqual(self.browser.current_url, self.gate_url)
def test_free_token_insert_via_kit_consumed_on_confirm(self):
self.gamer.tokens.filter(token_type=Token.FREE).delete()
token = Token.objects.create(
user=self.gamer,
token_type=Token.FREE,
expires_at=timezone.now() + timedelta(days=7),
)
self.browser.get(self.gate_url)
self._select_token_from_kit(token)
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
)
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
self.assertFalse(Token.objects.filter(id=token.id).exists())
def test_tithe_token_insert_via_kit_consumed_on_confirm(self):
token = Token.objects.create(user=self.gamer, token_type=Token.TITHE)
self.browser.get(self.gate_url)
self._select_token_from_kit(token)
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR, ".gate-slot[data-slot='1'] .btn-confirm"
)
).click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
self.assertFalse(Token.objects.filter(id=token.id).exists())
def test_pass_token_insert_via_kit_not_consumed(self):
self.gamer.is_staff = True
self.gamer.save()
pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
self.gamer.equipped_trinket = pass_token
self.gamer.save(update_fields=["equipped_trinket"])
self.browser.get(self.gate_url)
self._select_token_from_kit(pass_token)
self.browser.find_element(By.CSS_SELECTOR, "button.token-rails").click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.reserved")
)
self.assertTrue(Token.objects.filter(id=pass_token.id).exists())
self.assertEqual(self.browser.current_url, self.gate_url)