diff --git a/infra/staging/smoke_suite.sh b/infra/staging/smoke_suite.sh new file mode 100755 index 0000000..11d7c36 --- /dev/null +++ b/infra/staging/smoke_suite.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${BASE_URL:-http://127.0.0.1:8000}" +APP_DIR="${APP_DIR:-/opt/wpp-staging/app}" +ISSUE_ON_FAIL="${ISSUE_ON_FAIL:-1}" + +fail() { + local message="$1" + echo "[smoke] FAIL: ${message}" >&2 + + if [[ "${ISSUE_ON_FAIL}" == "1" ]] && [[ -n "${GITEA_BASE:-}" ]] && [[ -n "${GITEA_REPO:-}" ]] && [[ -n "${GITEA_USER:-}" ]] && [[ -n "${GITEA_TOKEN:-}" ]]; then + python3 - </dev/null || { SMOKE_FAIL_MESSAGE="healthz check failed" fail "healthz check failed"; } + +echo "[smoke] gameplay flow via management command" +( + cd "${APP_DIR}" + .venv/bin/python manage.py smoke_staging +) || { SMOKE_FAIL_MESSAGE="manage.py smoke_staging failed" fail "manage.py smoke_staging failed"; } + +echo "[smoke] OK" diff --git a/lobby/management/__init__.py b/lobby/management/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lobby/management/__init__.py @@ -0,0 +1 @@ + diff --git a/lobby/management/commands/__init__.py b/lobby/management/commands/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lobby/management/commands/__init__.py @@ -0,0 +1 @@ + diff --git a/lobby/management/commands/smoke_staging.py b/lobby/management/commands/smoke_staging.py new file mode 100644 index 0000000..c672028 --- /dev/null +++ b/lobby/management/commands/smoke_staging.py @@ -0,0 +1,118 @@ +import json + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand, CommandError +from django.test import Client + +from fupogfakta.models import Category, GameSession, Player, Question, RoundQuestion + + +class Command(BaseCommand): + help = "Run minimal staging smoke flow for lobby gameplay" + + def handle(self, *args, **options): + GameSession.objects.all().delete() + Player.objects.all().delete() + RoundQuestion.objects.all().delete() + + category, _ = Category.objects.get_or_create( + slug="smoke", + defaults={"name": "Smoke", "is_active": True}, + ) + category.is_active = True + category.save(update_fields=["is_active"]) + + Question.objects.get_or_create( + category=category, + prompt="Smoke prompt?", + defaults={"correct_answer": "Correct", "is_active": True}, + ) + + User = get_user_model() + host, _ = User.objects.get_or_create(username="smoke-host") + host.set_password("smoke-pass") + host.is_staff = True + host.save() + + host_client = Client() + host_client.force_login(host) + + create_res = host_client.post("/lobby/sessions/create", content_type="application/json") + if create_res.status_code != 201: + raise CommandError(f"create_session failed: {create_res.status_code} {create_res.content!r}") + + code = create_res.json()["session"]["code"] + + players = [] + for nickname in ["P1", "P2", "P3"]: + join_res = Client().post( + "/lobby/sessions/join", + data=json.dumps({"code": code, "nickname": nickname}), + content_type="application/json", + ) + if join_res.status_code != 201: + raise CommandError(f"join_session failed for {nickname}: {join_res.status_code}") + players.append(join_res.json()["player"]) + + start_res = host_client.post( + f"/lobby/sessions/{code}/rounds/start", + data=json.dumps({"category_slug": category.slug}), + content_type="application/json", + ) + if start_res.status_code != 201: + raise CommandError(f"start_round failed: {start_res.status_code}") + + show_res = host_client.post(f"/lobby/sessions/{code}/questions/show", content_type="application/json") + if show_res.status_code != 201: + raise CommandError(f"show_question failed: {show_res.status_code}") + + round_question_id = show_res.json()["round_question"]["id"] + + for player in players: + nick = player["nickname"] + lie_res = Client().post( + f"/lobby/sessions/{code}/questions/{round_question_id}/lies/submit", + data=json.dumps({"player_id": player["id"], "text": f"Lie from {nick}"}), + content_type="application/json", + ) + if lie_res.status_code != 201: + raise CommandError(f"submit_lie failed for {nick}: {lie_res.status_code}") + + mix_res = host_client.post( + f"/lobby/sessions/{code}/questions/{round_question_id}/answers/mix", + content_type="application/json", + ) + if mix_res.status_code != 200: + raise CommandError(f"mix_answers failed: {mix_res.status_code}") + + answers = mix_res.json().get("answers", []) + if not answers: + raise CommandError("mix_answers returned empty answers") + + for player in players: + nick = player["nickname"] + selected = next((a for a in answers if a.get("player_id") != player["id"]), answers[0]) + guess_res = Client().post( + f"/lobby/sessions/{code}/questions/{round_question_id}/guesses/submit", + data=json.dumps({"player_id": player["id"], "selected_text": selected["text"]}), + content_type="application/json", + ) + if guess_res.status_code != 201: + raise CommandError(f"submit_guess failed for {nick}: {guess_res.status_code}") + + calc_res = host_client.post( + f"/lobby/sessions/{code}/questions/{round_question_id}/scores/calculate", + content_type="application/json", + ) + if calc_res.status_code != 200: + raise CommandError(f"calculate_scores failed: {calc_res.status_code}") + + board_res = host_client.get(f"/lobby/sessions/{code}/scoreboard") + if board_res.status_code != 200: + raise CommandError(f"reveal_scoreboard failed: {board_res.status_code}") + + finish_res = host_client.post(f"/lobby/sessions/{code}/finish", content_type="application/json") + if finish_res.status_code != 200: + raise CommandError(f"finish_game failed: {finish_res.status_code}") + + self.stdout.write(self.style.SUCCESS(f"Smoke flow OK for session {code}"))