Редактор ачивок в админке + файловое хранилище ачивок (задел под титулы)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""Ачивки: админ создаёт/правит/удаляет, грузит иконку; condition хранится как текст."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from tests.conftest import csrf_headers
|
||||
|
||||
PNG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 64
|
||||
|
||||
|
||||
def _admin(client: TestClient, make_admin) -> None:
|
||||
make_admin("admin", "secret123")
|
||||
r = client.post(
|
||||
"/api/admin/auth/login",
|
||||
json={"username": "admin", "password": "secret123"},
|
||||
headers=csrf_headers(client),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
|
||||
def _use_tmp_achievements(monkeypatch, tmp_path) -> Path:
|
||||
from app.core.config import settings
|
||||
|
||||
monkeypatch.setattr(settings, "dev_achievements_dir", str(tmp_path))
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_create_list_and_files(client: TestClient, make_admin, monkeypatch, tmp_path):
|
||||
root = _use_tmp_achievements(monkeypatch, tmp_path)
|
||||
_admin(client, make_admin)
|
||||
|
||||
r = client.post(
|
||||
"/api/admin/achievements",
|
||||
json={"name": "Великий полководец", "description": "10 побед подряд"},
|
||||
headers=csrf_headers(client),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
ach = r.json()
|
||||
slug = ach["slug"]
|
||||
assert slug and ach["name"] == "Великий полководец"
|
||||
|
||||
# На диске создана папка с meta.json и condition.py (задел, не исполняется).
|
||||
folder = root / slug
|
||||
assert (folder / "meta.json").is_file()
|
||||
assert (folder / "condition.py").is_file()
|
||||
meta = json.loads((folder / "meta.json").read_text(encoding="utf-8"))
|
||||
assert meta["name"] == "Великий полководец"
|
||||
|
||||
# Виден в админском списке (редактор использует именно его).
|
||||
assert any(a["slug"] == slug for a in client.get("/api/admin/achievements").json())
|
||||
|
||||
|
||||
def test_duplicate_name_conflicts(client: TestClient, make_admin, monkeypatch, tmp_path):
|
||||
_use_tmp_achievements(monkeypatch, tmp_path)
|
||||
_admin(client, make_admin)
|
||||
body = {"name": "Берсерк"}
|
||||
assert client.post("/api/admin/achievements", json=body, headers=csrf_headers(client)).status_code == 200
|
||||
r = client.post("/api/admin/achievements", json=body, headers=csrf_headers(client))
|
||||
assert r.status_code == 409, r.text
|
||||
|
||||
|
||||
def test_icon_upload_and_serve(client: TestClient, make_admin, monkeypatch, tmp_path):
|
||||
_use_tmp_achievements(monkeypatch, tmp_path)
|
||||
_admin(client, make_admin)
|
||||
slug = client.post(
|
||||
"/api/admin/achievements", json={"name": "Герой"}, headers=csrf_headers(client)
|
||||
).json()["slug"]
|
||||
|
||||
# Не картинка → 422.
|
||||
bad = client.put(
|
||||
f"/api/admin/achievements/{slug}/icon",
|
||||
files={"file": ("x.txt", b"nope", "text/plain")},
|
||||
headers=csrf_headers(client),
|
||||
)
|
||||
assert bad.status_code == 422, bad.text
|
||||
|
||||
# Валидный PNG → icon_url, файл отдаётся.
|
||||
ok = client.put(
|
||||
f"/api/admin/achievements/{slug}/icon",
|
||||
files={"file": ("h.png", PNG, "image/png")},
|
||||
headers=csrf_headers(client),
|
||||
)
|
||||
assert ok.status_code == 200 and ok.json()["icon_url"], ok.text
|
||||
g = client.get(f"/api/achievements/{slug}/icon")
|
||||
assert g.status_code == 200 and g.content == PNG
|
||||
|
||||
|
||||
def test_update_and_delete(client: TestClient, make_admin, monkeypatch, tmp_path):
|
||||
_use_tmp_achievements(monkeypatch, tmp_path)
|
||||
_admin(client, make_admin)
|
||||
slug = client.post(
|
||||
"/api/admin/achievements", json={"name": "Стратег"}, headers=csrf_headers(client)
|
||||
).json()["slug"]
|
||||
|
||||
# Правка описания и текста условия (condition хранится как текст).
|
||||
r = client.patch(
|
||||
f"/api/admin/achievements/{slug}",
|
||||
json={"description": "Победа по целям", "condition": "wins_by_objectives >= 5"},
|
||||
headers=csrf_headers(client),
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["description"] == "Победа по целям"
|
||||
assert r.json()["has_condition"] is True
|
||||
assert "wins_by_objectives" in r.json()["condition"]
|
||||
|
||||
# Удаление.
|
||||
assert client.delete(
|
||||
f"/api/admin/achievements/{slug}", headers=csrf_headers(client)
|
||||
).status_code == 200
|
||||
assert all(a["slug"] != slug for a in client.get("/api/admin/achievements").json())
|
||||
Reference in New Issue
Block a user