"""Ачивки: админ создаёт/правит/удаляет, грузит иконку; 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()) def test_delete_rejects_traversal_slug(client: TestClient, make_admin, monkeypatch, tmp_path): """Slug из URL не должен уводить файловые операции за каталог ачивок. Регрессия: `DELETE /api/admin/achievements/%2E%2E` снимал rmtree'ом родительскую папку каталога (в проде это /data — БД, uploads и ачивки разом).""" root = _use_tmp_achievements(monkeypatch, tmp_path / "achievements") root.mkdir(parents=True, exist_ok=True) sibling = tmp_path / "db.sqlite3" sibling.write_bytes(b"data") _admin(client, make_admin) r = client.delete("/api/admin/achievements/..", headers=csrf_headers(client)) assert r.status_code == 404, r.text assert sibling.exists() and root.is_dir()