fix: harden twenty four game submissions
This commit is contained in:
@@ -63,6 +63,8 @@ def test_toolbox_and_games_资源入口与移动端触控样式存在():
|
|||||||
assert "pointerdown" in toolbox
|
assert "pointerdown" in toolbox
|
||||||
assert "window.HuluGames" in games
|
assert "window.HuluGames" in games
|
||||||
assert "sudoku-board" in games
|
assert "sudoku-board" in games
|
||||||
|
assert 'errorMessage.className = "form-error game-form-error"' in games
|
||||||
|
assert "errorMessage.textContent = error.message" in games
|
||||||
assert "touch-action: none" in styles
|
assert "touch-action: none" in styles
|
||||||
assert ".calculator-controls [hidden]" in styles
|
assert ".calculator-controls [hidden]" in styles
|
||||||
assert ".sudoku-board" in styles
|
assert ".sudoku-board" in styles
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import ast
|
import ast
|
||||||
import secrets
|
import secrets
|
||||||
|
import unicodedata
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
|
||||||
@@ -36,6 +37,18 @@ TWENTY_FOUR_PUZZLES = {
|
|||||||
MathGameAttempt.Difficulty.HARD: [(1, 5, 5, 5), (3, 3, 7, 7), (5, 5, 7, 11)],
|
MathGameAttempt.Difficulty.HARD: [(1, 5, 5, 5), (3, 3, 7, 7), (5, 5, 7, 11)],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TWENTY_FOUR_SYMBOLS = str.maketrans(
|
||||||
|
{
|
||||||
|
"×": "*",
|
||||||
|
"·": "*",
|
||||||
|
"∙": "*",
|
||||||
|
"÷": "/",
|
||||||
|
"−": "-",
|
||||||
|
"–": "-",
|
||||||
|
"—": "-",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _grid_from_text(value):
|
def _grid_from_text(value):
|
||||||
return [[int(value[row * 9 + column]) for column in range(9)] for row in range(9)]
|
return [[int(value[row * 9 + column]) for column in range(9)] for row in range(9)]
|
||||||
@@ -80,7 +93,10 @@ def start_game(user, kind, difficulty):
|
|||||||
|
|
||||||
|
|
||||||
def _validate_twenty_four_expression(source, numbers):
|
def _validate_twenty_four_expression(source, numbers):
|
||||||
source = str(source or "").strip()
|
source = unicodedata.normalize("NFKC", str(source or "")).translate(
|
||||||
|
TWENTY_FOUR_SYMBOLS
|
||||||
|
)
|
||||||
|
source = source.strip()
|
||||||
if not source or len(source) > 120:
|
if not source or len(source) > 120:
|
||||||
raise ValidationError({"expression": "请输入不超过 120 个字符的表达式"})
|
raise ValidationError({"expression": "请输入不超过 120 个字符的表达式"})
|
||||||
try:
|
try:
|
||||||
@@ -90,7 +106,11 @@ def _validate_twenty_four_expression(source, numbers):
|
|||||||
used = []
|
used = []
|
||||||
|
|
||||||
def evaluate(node):
|
def evaluate(node):
|
||||||
if isinstance(node, ast.Constant) and isinstance(node.value, int):
|
if (
|
||||||
|
isinstance(node, ast.Constant)
|
||||||
|
and isinstance(node.value, int)
|
||||||
|
and not isinstance(node.value, bool)
|
||||||
|
):
|
||||||
used.append(node.value)
|
used.append(node.value)
|
||||||
return Fraction(node.value)
|
return Fraction(node.value)
|
||||||
if isinstance(node, ast.BinOp) and isinstance(
|
if isinstance(node, ast.BinOp) and isinstance(
|
||||||
|
|||||||
@@ -47,6 +47,55 @@ def test_twenty_four_服务端校验数字使用与幂等提交(game_user):
|
|||||||
assert replay["score"] == result["score"]
|
assert replay["score"] == result["score"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_twenty_four_api_兼容常见数学符号并完成计分(client, game_user):
|
||||||
|
attempt = MathGameAttempt.objects.create(
|
||||||
|
user=game_user,
|
||||||
|
kind=MathGameAttempt.Kind.TWENTY_FOUR,
|
||||||
|
puzzle={"numbers": [1, 3, 4, 6]},
|
||||||
|
solution={"target": 24},
|
||||||
|
)
|
||||||
|
client.force_login(game_user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1/contests/games/attempts/{attempt.id}/submit/",
|
||||||
|
{"expression": "6÷(1−3÷4)"},
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_IDEMPOTENCY_KEY="unicode-game-submit",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["status"] == MathGameAttempt.Status.COMPLETED
|
||||||
|
assert response.json()["score"] >= 100
|
||||||
|
attempt.refresh_from_db()
|
||||||
|
assert attempt.submission == {"expression": "6/(1-3/4)"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_twenty_four_api_答案错误时返回具体原因和请求编号(client, game_user):
|
||||||
|
attempt = MathGameAttempt.objects.create(
|
||||||
|
user=game_user,
|
||||||
|
kind=MathGameAttempt.Kind.TWENTY_FOUR,
|
||||||
|
puzzle={"numbers": [1, 3, 4, 6]},
|
||||||
|
solution={"target": 24},
|
||||||
|
)
|
||||||
|
client.force_login(game_user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1/contests/games/attempts/{attempt.id}/submit/",
|
||||||
|
{"expression": "1 + 3 + 4 + 6"},
|
||||||
|
content_type="application/json",
|
||||||
|
HTTP_IDEMPOTENCY_KEY="incorrect-game-submit",
|
||||||
|
HTTP_X_REQUEST_ID="twenty-four-invalid-test",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json()["error"]["message"] == "当前结果是 14,还没有得到 24"
|
||||||
|
assert response.json()["error"]["request_id"] == "twenty-four-invalid-test"
|
||||||
|
attempt.refresh_from_db()
|
||||||
|
assert attempt.status == MathGameAttempt.Status.ACTIVE
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_twenty_four_拒绝额外数字和非四则表达式(game_user):
|
def test_twenty_four_拒绝额外数字和非四则表达式(game_user):
|
||||||
attempt = MathGameAttempt.objects.create(
|
attempt = MathGameAttempt.objects.create(
|
||||||
@@ -70,6 +119,13 @@ def test_twenty_four_拒绝额外数字和非四则表达式(game_user):
|
|||||||
{"expression": "pow(2, 3) * 3"},
|
{"expression": "pow(2, 3) * 3"},
|
||||||
"invalid-function",
|
"invalid-function",
|
||||||
)
|
)
|
||||||
|
with pytest.raises(ValidationError, match="只允许"):
|
||||||
|
submit_game(
|
||||||
|
game_user,
|
||||||
|
attempt.id,
|
||||||
|
{"expression": "6 / (True - 3 / 4)"},
|
||||||
|
"invalid-boolean",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -143,6 +143,13 @@
|
|||||||
input.placeholder = "例如:6 / (1 - 3 / 4)";
|
input.placeholder = "例如:6 / (1 - 3 / 4)";
|
||||||
input.autocomplete = "off";
|
input.autocomplete = "off";
|
||||||
input.inputMode = "text";
|
input.inputMode = "text";
|
||||||
|
input.spellcheck = false;
|
||||||
|
const errorMessage = document.createElement("p");
|
||||||
|
errorMessage.className = "form-error game-form-error";
|
||||||
|
errorMessage.setAttribute("role", "alert");
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
errorMessage.textContent = "";
|
||||||
|
});
|
||||||
const keypad = document.createElement("div");
|
const keypad = document.createElement("div");
|
||||||
keypad.className = "game-keypad";
|
keypad.className = "game-keypad";
|
||||||
["+", "-", "*", "/", "(", ")"].forEach((operator) => {
|
["+", "-", "*", "/", "(", ")"].forEach((operator) => {
|
||||||
@@ -159,10 +166,11 @@
|
|||||||
submit.className = "primary-button";
|
submit.className = "primary-button";
|
||||||
submit.type = "submit";
|
submit.type = "submit";
|
||||||
submit.textContent = "验证并计分";
|
submit.textContent = "验证并计分";
|
||||||
form.append(input, keypad, submit);
|
form.append(input, keypad, errorMessage, submit);
|
||||||
form.addEventListener("submit", async (event) => {
|
form.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submit.disabled = true;
|
submit.disabled = true;
|
||||||
|
errorMessage.textContent = "";
|
||||||
try {
|
try {
|
||||||
const result = await api(
|
const result = await api(
|
||||||
`contests/games/attempts/${attempt.attempt_id}/submit/`,
|
`contests/games/attempts/${attempt.attempt_id}/submit/`,
|
||||||
@@ -175,6 +183,7 @@
|
|||||||
renderTwentyFour(result);
|
renderTwentyFour(result);
|
||||||
showToast("得到 24,成绩已记录");
|
showToast("得到 24,成绩已记录");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
errorMessage.textContent = error.message;
|
||||||
showToast(error.message);
|
showToast(error.message);
|
||||||
submit.disabled = false;
|
submit.disabled = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user