covered some test lacunae; gatekeeper now waits for +6 gamers to commit tokens to unblock game room

This commit is contained in:
Disco DeDisco
2026-03-13 22:51:42 -04:00
parent e0d1f51bf1
commit dddffd22d5
11 changed files with 121 additions and 8 deletions

View File

@@ -3,6 +3,7 @@ source = apps
omit = omit =
*/migrations/* */migrations/*
*/tests/* */tests/*
*/routing.py
[report] [report]
show_missing = true show_missing = true

View File

@@ -96,7 +96,8 @@ def create_gate_slots(sender, instance, created, **kwargs):
def debit_token(user, slot, token): def debit_token(user, slot, token):
if token.token_type == Token.COIN: if token.token_type == Token.COIN:
token.current_room = slot.room token.current_room = slot.room
token.next_ready_at = timezone.now() + slot.room.renewal_period period = slot.room.renewal_period or timedelta(days=7)
token.next_ready_at = timezone.now() + period
token.save() token.save()
else: else:
token.delete() token.delete()
@@ -104,3 +105,8 @@ def debit_token(user, slot, token):
slot.status = GateSlot.FILLED slot.status = GateSlot.FILLED
slot.filled_at = timezone.now() slot.filled_at = timezone.now()
slot.save() slot.save()
room = slot.room
if not room.gate_slots.filter(status=GateSlot.EMPTY).exists():
room.gate_status = Room.OPEN
room.save()

View File

@@ -40,6 +40,18 @@ class DebitTokenTest(TestCase):
self.assertEqual(self.slot.status, GateSlot.FILLED) self.assertEqual(self.slot.status, GateSlot.FILLED)
self.assertEqual(self.slot.gamer, self.owner) self.assertEqual(self.slot.gamer, self.owner)
def test_debit_fills_last_slot_and_opens_gate(self):
for i in range(2, 7):
gamer = User.objects.create(email=f"g{i}@test.io")
slot = self.room.gate_slots.get(slot_number=i)
slot.gamer = gamer
slot.status = GateSlot.FILLED
slot.save()
free_token = Token.objects.get(user=self.owner, token_type=Token.FREE)
debit_token(self.owner, self.slot, free_token)
self.room.refresh_from_db()
self.assertEqual(self.room.gate_status, Room.OPEN)
class CoinTokenInUseTest(TestCase): class CoinTokenInUseTest(TestCase):
def setUp(self): def setUp(self):

View File

@@ -30,6 +30,10 @@ class RoomCreationViewTest(TestCase):
data={"name": "Test Room"}, data={"name": "Test Room"},
) )
def test_create_room_get_redirects_to_gameboard(self):
response = self.client.get(reverse("epic:create_room"))
self.assertRedirects(response, "/gameboard/")
class MyGamesContextTest(TestCase): class MyGamesContextTest(TestCase):
def setUp(self): def setUp(self):
@@ -50,3 +54,23 @@ class MyGamesContextTest(TestCase):
slot.save() slot.save()
response = self.client.get("/gameboard/") response = self.client.get("/gameboard/")
self.assertIn(room, response.context["my_games"]) self.assertIn(room, response.context["my_games"])
class GateStatusViewTest(TestCase):
def setUp(self):
self.owner = User.objects.create(email="founder@test.io")
self.client.force_login(self.owner)
self.room = Room.objects.create(name="Test Room", owner=self.owner)
def test_gate_status_returns_empty_when_open(self):
self.room.gate_status = Room.OPEN
self.room.save()
response = self.client.get(reverse("epic:gate_status", kwargs={"room_id": self.room.id}))
self.assertEqual(response.content, b"")
def test_gate_status_returns_partial_when_gathering(self):
response = self.client.get(
reverse("epic:gate_status", kwargs={"room_id": self.room.id})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "gate-modal")

View File

@@ -9,5 +9,6 @@ urlpatterns = [
path('room/<uuid:room_id>/gate/', views.gatekeeper, name='gatekeeper'), path('room/<uuid:room_id>/gate/', views.gatekeeper, name='gatekeeper'),
path('room/<uuid:room_id>/gate/<int:slot_number>/drop_token', views.drop_token, name='drop_token'), path('room/<uuid:room_id>/gate/<int:slot_number>/drop_token', views.drop_token, name='drop_token'),
path('room/<uuid:room_id>/gate/invite', views.invite_gamer, name='invite_gamer'), path('room/<uuid:room_id>/gate/invite', views.invite_gamer, name='invite_gamer'),
path('room/<uuid:room_id>/gate/status', views.gate_status, name='gate_status'),
] ]

View File

@@ -1,4 +1,5 @@
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from apps.epic.models import Room, RoomInvite, debit_token from apps.epic.models import Room, RoomInvite, debit_token
@@ -12,7 +13,7 @@ def create_room(request):
if name: if name:
room = Room.objects.create(name=name, owner=request.user) room = Room.objects.create(name=name, owner=request.user)
return redirect("epic:gatekeeper", room_id=room.id) return redirect("epic:gatekeeper", room_id=room.id)
return redirect("gameboard:index") return redirect("/gameboard/")
def gatekeeper(request, room_id): def gatekeeper(request, room_id):
room = Room.objects.get(id=room_id) room = Room.objects.get(id=room_id)
@@ -54,3 +55,18 @@ def invite_gamer(request, room_id):
defaults={"status": RoomInvite.PENDING} defaults={"status": RoomInvite.PENDING}
) )
return redirect("epic:gatekeeper", room_id=room_id) return redirect("epic:gatekeeper", room_id=room_id)
def gate_status(request, room_id):
room = Room.objects.get(id=room_id)
if room.gate_status == Room.OPEN:
return HttpResponse("")
slots = room.gate_slots.order_by("slot_number")
user_has_slot = (
request.user.is_authenticated
and slots.filter(gamer=request.user).exists()
)
return render(request, "apps/gameboard/_partials/_gatekeeper.html", {
"room": room,
"slots": slots,
"user_has_slot": user_has_slot,
})

View File

@@ -143,6 +143,10 @@ class TokenTooltipTest(TestCase):
free = Token.objects.get(user=self.user, token_type=Token.FREE) free = Token.objects.get(user=self.user, token_type=Token.FREE)
self.assertIsNone(free.tooltip_shoptalk()) self.assertIsNone(free.tooltip_shoptalk())
def test_tooltip_room_html_returns_empty_when_no_room(self):
token = Token.objects.get(user=self.user, token_type=Token.COIN)
self.assertEqual(token.tooltip_room_html(), "")
class PaymentMethodTest(TestCase): class PaymentMethodTest(TestCase):
def setUp(self): def setUp(self):

View File

@@ -12,7 +12,7 @@ urlpatterns = [
path('dashboard/', include('apps.dashboard.urls')), path('dashboard/', include('apps.dashboard.urls')),
path('lyric/', include('apps.lyric.urls')), path('lyric/', include('apps.lyric.urls')),
path('gameboard/', include('apps.gameboard.urls')), path('gameboard/', include('apps.gameboard.urls')),
path('gameboard/', include('apps.epic.urls', 'epic')), path('gameboard/', include('apps.epic.urls')),
] ]
# Please remove the following urlpattern # Please remove the following urlpattern

View File

@@ -2,6 +2,8 @@ from selenium.webdriver.common.by import By
from .base import FunctionalTest from .base import FunctionalTest
from apps.applets.models import Applet from apps.applets.models import Applet
from apps.epic.models import Room, GateSlot
from apps.lyric.models import User
class GatekeeperTest(FunctionalTest): class GatekeeperTest(FunctionalTest):
@@ -134,3 +136,43 @@ class GatekeeperTest(FunctionalTest):
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-slot.filled")), 2 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)
)
room_url = self.browser.current_url
# 2. Fill all 6 slots directly via ORM (founder + 5 extras)
self.browser.find_element(By.CSS_SELECTOR, ".drop-token-btn").click()
self.wait_for(
lambda: self.browser.find_element(By.CSS_SELECTOR, ".gate-slot.filled")
)
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()
# 3. Gatekeeper disappears via htmx
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(By.CSS_SELECTOR, ".gate-modal")), 0
)
)
# Restore the following once room built
# body = self.browser.find_element(By.TAG_NAME, "body")
# self.assertIn("OPEN", body.text)

View File

@@ -1,4 +1,4 @@
<div class="gate-modal" role"dialog" aria-label="Gatekeeper"> <div class="gate-modal" role="dialog" aria-label="Gatekeeper">
<header class="gate-header"> <header class="gate-header">
<h1>{{ room.name }}</h1> <h1>{{ room.name }}</h1>
<span class="gate-status">{{ room.gate_status }}</span> <span class="gate-status">{{ room.gate_status }}</span>

View File

@@ -7,10 +7,17 @@
<div class="room-table"></div> <div class="room-table"></div>
</div> </div>
<div
id="id_gate_wrapper"
hx-get="{% url 'epic:gate_status' room.id %}"
hx-trigger="every 3s"
hx-swap="outerHTML"
>
{% if room.gate_status == "GATHERING" %} {% if room.gate_status == "GATHERING" %}
<div class="gate-overlay"> <div class="gate-overlay">
{% include "apps/gameboard/_partials/_gatekeeper.html" %} {% include "apps/gameboard/_partials/_gatekeeper.html" %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
{% endblock content %} {% endblock content %}