Files
Hulumath-Web/backend/contest/test_services.py
T
Jacky 1f98f7152d
CI / test (push) Failing after 1m31s
v0.2 Preview: Construction Plan.
2026-08-08 21:25:27 +08:00

204 lines
5.5 KiB
Python

from datetime import timedelta
import pytest
from django.utils import timezone
from rest_framework.exceptions import ValidationError
from accounts.models import User
from contest.models import (
Contest,
ContestAttempt,
ContestQuestion,
Question,
QuestionVersion,
RatingHistory,
RealtimeMatch,
)
from contest.services import (
finalize_match,
find_match,
normalize_answer,
start_attempt,
submit_attempt,
)
@pytest.fixture
def user(db):
return User.objects.create_user(
username="contest_user",
password="StrongPass_2026",
nickname="比赛用户",
)
@pytest.fixture
def daily_contest(db):
question = Question.objects.create(
slug="sum-question",
track=Question.Track.STANDARD,
)
version = QuestionVersion.objects.create(
question=question,
version=1,
prompt="17 + 25",
answer="42",
explanation="相加得 42",
)
contest = Contest.objects.create(
slug="daily-test",
title="测试今日赛",
kind=Contest.Kind.DAILY,
track=Question.Track.STANDARD,
status=Contest.Status.PUBLISHED,
duration_seconds=60,
)
ContestQuestion.objects.create(
contest=contest,
question_version=version,
order=1,
points=100,
)
return contest
@pytest.mark.parametrize(
"raw, expected",
[
pytest.param(" 1.0 ", "1", id="小数标准化"),
pytest.param("ABC ", "abc", id="文本去空格并转小写"),
pytest.param("-0", "-0", id="保留十进制负零表示"),
],
)
def test_normalize_answer_标准化输入(raw, expected):
assert normalize_answer(raw) == expected
@pytest.mark.django_db
def test_start_attempt_每日赛重复进入复用同一记录(user, daily_contest):
first = start_attempt(user, daily_contest)
second = start_attempt(user, daily_contest)
assert first["attempt_id"] == second["attempt_id"]
assert ContestAttempt.objects.count() == 1
@pytest.mark.django_db
def test_submit_attempt_服务端判分且幂等重放(user, daily_contest):
started = start_attempt(user, daily_contest)
result = submit_attempt(
user,
started["attempt_id"],
[{"order": 1, "answer": "42.0"}],
"submission-1",
)
replay = submit_attempt(
user,
started["attempt_id"],
[{"order": 1, "answer": "0"}],
"submission-1",
)
assert result["status"] == ContestAttempt.Status.SUBMITTED
assert result["score"] == 100
assert result["correct_count"] == 1
assert result["questions"][0]["correct_answer"] == "42"
assert replay["score"] == 100
@pytest.mark.django_db
def test_submit_attempt_缺少幂等键时拒绝(user, daily_contest):
started = start_attempt(user, daily_contest)
with pytest.raises(ValidationError, match="幂等键"):
submit_attempt(
user,
started["attempt_id"],
[{"order": 1, "answer": "42"}],
None,
)
@pytest.mark.django_db
def test_submit_attempt_畸形题号返回校验错误且记录保持进行中(user, daily_contest):
started = start_attempt(user, daily_contest)
with pytest.raises(ValidationError):
submit_attempt(
user,
started["attempt_id"],
[{"order": "first", "answer": "42"}],
"malformed-order",
)
attempt = ContestAttempt.objects.get(id=started["attempt_id"])
assert attempt.status == ContestAttempt.Status.ACTIVE
assert attempt.answers.count() == 0
@pytest.mark.django_db
def test_submit_attempt_超过服务端时限不计分(user, daily_contest):
started = start_attempt(user, daily_contest)
ContestAttempt.objects.filter(id=started["attempt_id"]).update(
started_at=timezone.now() - timedelta(seconds=61)
)
result = submit_attempt(
user,
started["attempt_id"],
[{"order": 1, "answer": "42"}],
"late-submit",
)
assert result["status"] == ContestAttempt.Status.EXPIRED
assert result["score"] == 0
assert result["correct_count"] == 0
@pytest.mark.django_db
def test_find_match_同分玩家配对并由_finalize_match_唯一结算_rating():
first = User.objects.create_user(
username="player_one",
password="StrongPass_2026",
nickname="玩家一",
)
second = User.objects.create_user(
username="player_two",
password="StrongPass_2026",
nickname="玩家二",
)
contest = Contest.objects.create(
slug="realtime-test",
title="测试实时赛",
kind=Contest.Kind.REALTIME,
track=Question.Track.STANDARD,
status=Contest.Status.PUBLISHED,
)
waiting = find_match(first, contest)
active = find_match(second, contest)
active.refresh_from_db()
assert waiting.id == active.id
assert active.status == RealtimeMatch.Status.ACTIVE
assert active.attempts.count() == 2
active.attempts.filter(user=first).update(
status=ContestAttempt.Status.SUBMITTED,
score=200,
)
active.attempts.filter(user=second).update(
status=ContestAttempt.Status.SUBMITTED,
score=100,
)
finalized = finalize_match(active.id)
first.refresh_from_db()
second.refresh_from_db()
assert finalized.status == RealtimeMatch.Status.COMPLETED
assert finalized.winner == first
assert first.rating == 1016
assert second.rating == 984
assert RatingHistory.objects.filter(match=active).count() == 2