feat(f3): require player session token for lie submission
All checks were successful
CI / test-and-quality (push) Successful in 1m34s
CI / test-and-quality (pull_request) Successful in 1m34s

This commit is contained in:
2026-02-27 23:11:59 +01:00
parent 86dbd4fabc
commit 37e1d32675
5 changed files with 80 additions and 4 deletions

View File

@@ -55,6 +55,8 @@ class LobbyFlowTests(TestCase):
body = response.json()
self.assertEqual(body["session"]["code"], "ABCD23")
self.assertEqual(body["player"]["nickname"], "Luna")
self.assertIn("session_token", body["player"])
self.assertTrue(body["player"]["session_token"])
self.assertTrue(Player.objects.filter(session=session, nickname="Luna").exists())
def test_join_rejects_duplicate_nickname_case_insensitive(self):
@@ -217,7 +219,7 @@ class LieSubmissionTests(TestCase):
"lobby:submit_lie",
kwargs={"code": self.session.code, "round_question_id": round_question.id},
),
data={"player_id": self.player.id, "text": "Sydney"},
data={"player_id": self.player.id, "session_token": self.player.session_token, "text": "Sydney"},
content_type="application/json",
)
@@ -239,7 +241,7 @@ class LieSubmissionTests(TestCase):
"lobby:submit_lie",
kwargs={"code": self.session.code, "round_question_id": round_question.id},
),
data={"player_id": self.player.id, "text": "Melbourne"},
data={"player_id": self.player.id, "session_token": self.player.session_token, "text": "Melbourne"},
content_type="application/json",
)
@@ -260,13 +262,53 @@ class LieSubmissionTests(TestCase):
"lobby:submit_lie",
kwargs={"code": self.session.code, "round_question_id": round_question.id},
),
data={"player_id": self.player.id, "text": "Brisbane"},
data={"player_id": self.player.id, "session_token": self.player.session_token, "text": "Brisbane"},
content_type="application/json",
)
self.assertEqual(response.status_code, 409)
self.assertEqual(response.json()["error"], "Lie already submitted for this player")
def test_submit_lie_requires_session_token(self):
round_question = RoundQuestion.objects.create(
session=self.session,
round_number=1,
question=self.question,
correct_answer=self.question.correct_answer,
)
response = self.client.post(
reverse(
"lobby:submit_lie",
kwargs={"code": self.session.code, "round_question_id": round_question.id},
),
data={"player_id": self.player.id, "text": "Sydney"},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()["error"], "session_token is required")
def test_submit_lie_rejects_invalid_session_token(self):
round_question = RoundQuestion.objects.create(
session=self.session,
round_number=1,
question=self.question,
correct_answer=self.question.correct_answer,
)
response = self.client.post(
reverse(
"lobby:submit_lie",
kwargs={"code": self.session.code, "round_question_id": round_question.id},
),
data={"player_id": self.player.id, "session_token": "invalid-token", "text": "Sydney"},
content_type="application/json",
)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.json()["error"], "Invalid player session token")
class MixAnswersTests(TestCase):
def setUp(self):
self.host = User.objects.create_user(username="host", password="secret123")

View File

@@ -104,6 +104,7 @@ def join_session(request: HttpRequest) -> JsonResponse:
"player": {
"id": player.id,
"nickname": player.nickname,
"session_token": player.session_token,
"score": player.score,
},
"session": {
@@ -296,11 +297,15 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
session_code = code.strip().upper()
player_id = payload.get("player_id")
session_token = str(payload.get("session_token", "")).strip()
lie_text = str(payload.get("text", "")).strip()
if not player_id:
return JsonResponse({"error": "player_id is required"}, status=400)
if not session_token:
return JsonResponse({"error": "session_token is required"}, status=400)
if not lie_text or len(lie_text) > 255:
return JsonResponse({"error": "text must be between 1 and 255 characters"}, status=400)
@@ -317,6 +322,9 @@ def submit_lie(request: HttpRequest, code: str, round_question_id: int) -> JsonR
except Player.DoesNotExist:
return JsonResponse({"error": "Player not found in session"}, status=404)
if player.session_token != session_token:
return JsonResponse({"error": "Invalid player session token"}, status=403)
try:
round_question = RoundQuestion.objects.get(
pk=round_question_id,