Files
Hulumath-Web/backend/contest/test_realtime_api.py
T

206 lines
6.7 KiB
Python

from datetime import timedelta
import pytest
from django.test import Client
from django.utils import timezone
from accounts.models import User
from contest.models import (
Contest,
ContestQuestion,
Question,
QuestionVersion,
RealtimeMatch,
)
@pytest.fixture
def realtime_api_setup(db):
question = Question.objects.create(
slug="realtime-api-question",
track=Question.Track.STANDARD,
)
version = QuestionVersion.objects.create(
question=question,
version=1,
prompt="18 + 24",
answer="42",
explanation="18 + 24 = 42",
)
contest = Contest.objects.create(
slug="realtime-api",
title="API 联机赛",
kind=Contest.Kind.REALTIME,
track=Question.Track.STANDARD,
status=Contest.Status.PUBLISHED,
duration_seconds=60,
)
ContestQuestion.objects.create(
contest=contest,
question_version=version,
order=1,
points=100,
)
first = User.objects.create_user(
username="api_player_one",
password="StrongPass_2026",
nickname="API 玩家一",
)
second = User.objects.create_user(
username="api_player_two",
password="StrongPass_2026",
nickname="API 玩家二",
)
first_client = Client()
second_client = Client()
first_client.force_login(first)
second_client.force_login(second)
return contest, first_client, second_client
@pytest.mark.django_db
def test_challenge_api_创建加入状态提交形成完整闭环(realtime_api_setup):
contest, first_client, second_client = realtime_api_setup
created = first_client.post(
f"/api/v1/contests/{contest.slug}/challenges/",
{},
content_type="application/json",
)
code = created.json()["challenge_code"]
joined = second_client.post(
"/api/v1/contests/challenges/join/",
{"challenge_code": code.lower()},
content_type="application/json",
)
match_id = joined.json()["match_id"]
first_state = first_client.get(f"/api/v1/contests/matches/{match_id}/")
assert created.status_code == 201
assert joined.status_code == 200
assert joined.json()["status"] == "active"
assert first_state.json()["opponent"]["nickname"] == "API 玩家二"
first_attempt = first_state.json()["attempt"]["attempt_id"]
second_attempt = joined.json()["attempt"]["attempt_id"]
first_submit = first_client.post(
f"/api/v1/contests/attempts/{first_attempt}/submit/",
{"answers": [{"order": 1, "answer": "42"}]},
content_type="application/json",
HTTP_IDEMPOTENCY_KEY="api-submit-one",
)
second_submit = second_client.post(
f"/api/v1/contests/attempts/{second_attempt}/submit/",
{"answers": [{"order": 1, "answer": "0"}]},
content_type="application/json",
HTTP_IDEMPOTENCY_KEY="api-submit-two",
)
assert first_submit.json()["status"] == "active"
assert "correct_answer" not in first_submit.json()["attempt"]["questions"][0]
assert second_submit.json()["status"] == "completed"
assert second_submit.json()["result"]["winner"] == "opponent"
final_state = first_client.get(f"/api/v1/contests/matches/{match_id}/").json()
assert final_state["result"]["winner"] == "self"
assert final_state["attempt"]["questions"][0]["correct_answer"] == "42"
@pytest.mark.django_db
def test_challenge_api_无效联机码返回具体原因_request_id_和诊断日志(
realtime_api_setup,
caplog,
):
_, _, second_client = realtime_api_setup
caplog.set_level("INFO", logger="common.api")
response = second_client.post(
"/api/v1/contests/challenges/join/",
{"challenge_code": "ABC234"},
content_type="application/json",
HTTP_X_REQUEST_ID="challenge-invalid-test",
)
assert response.status_code == 400
assert response["X-Request-ID"] == "challenge-invalid-test"
assert response.json()["error"] == {
"code": "invalid",
"message": "联机码不存在",
"details": {"challenge_code": "联机码不存在"},
"request_id": "challenge-invalid-test",
}
assert "api_request_error method=POST" in caplog.text
assert "path=/api/v1/contests/challenges/join/" in caplog.text
assert "fields=challenge_code message=联机码不存在" in caplog.text
@pytest.mark.django_db
def test_challenge_api_创建者不能加入自己的联机码(realtime_api_setup):
contest, first_client, _ = realtime_api_setup
created = first_client.post(
f"/api/v1/contests/{contest.slug}/challenges/",
{},
content_type="application/json",
)
response = first_client.post(
"/api/v1/contests/challenges/join/",
{"challenge_code": created.json()["challenge_code"]},
content_type="application/json",
)
assert response.status_code == 400
assert response.json()["error"]["message"] == "不能加入自己创建的约战"
@pytest.mark.django_db
def test_challenge_api_过期联机码返回具体原因(realtime_api_setup):
contest, first_client, second_client = realtime_api_setup
created = first_client.post(
f"/api/v1/contests/{contest.slug}/challenges/",
{},
content_type="application/json",
)
RealtimeMatch.objects.filter(id=created.json()["match_id"]).update(
expires_at=timezone.now() - timedelta(seconds=1)
)
response = second_client.post(
"/api/v1/contests/challenges/join/",
{"challenge_code": created.json()["challenge_code"]},
content_type="application/json",
)
assert response.status_code == 400
assert response.json()["error"]["message"] == "联机码已失效或已被使用"
@pytest.mark.django_db
def test_challenge_api_已使用联机码返回具体原因(realtime_api_setup):
contest, first_client, second_client = realtime_api_setup
created = first_client.post(
f"/api/v1/contests/{contest.slug}/challenges/",
{},
content_type="application/json",
)
second_client.post(
"/api/v1/contests/challenges/join/",
{"challenge_code": created.json()["challenge_code"]},
content_type="application/json",
)
third = User.objects.create_user(
username="api_player_three",
password="StrongPass_2026",
nickname="API 玩家三",
)
third_client = Client()
third_client.force_login(third)
response = third_client.post(
"/api/v1/contests/challenges/join/",
{"challenge_code": created.json()["challenge_code"]},
content_type="application/json",
)
assert response.status_code == 400
assert response.json()["error"]["message"] == "联机码已失效或已被使用"