206 lines
6.7 KiB
Python
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"] == "联机码已失效或已被使用"
|