DispelEvilBot/tests/test_admin_sequences.py

272 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Тесты раздела «Последовательности» и парсера кнопок."""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
import pytest
from fastapi.testclient import TestClient
from admin.routes.sequences import parse_buttons
from common.repositories import sequences_repo
SEQUENCE: dict[str, Any] = {
"key": "welcome",
"title": "Приветствие",
"description": "Сообщения при входе",
"item_count": 2,
"updated_at": datetime(2026, 10, 25, tzinfo=UTC),
"updated_by": None,
}
ITEM_1: dict[str, Any] = {
"id": 1,
"position": 0,
"text": "Привет, {username}!",
"enabled": True,
"image_name": "welcome_image.jpg",
"buttons": [],
}
ITEM_2: dict[str, Any] = {
"id": 2,
"position": 1,
"text": "Правила клана",
"enabled": True,
"image_name": None,
"buttons": [{"text": "Канал", "url": "https://t.me/example"}],
}
@pytest.fixture
def fake_sequences(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
state: dict[str, Any] = {}
async def list_sequences() -> list[dict[str, Any]]:
return [SEQUENCE]
async def get_sequence(key: str) -> dict[str, Any] | None:
return {**SEQUENCE, "key": key}
async def get_items(key: str) -> list[dict[str, Any]]:
return [
{**ITEM_1, "buttons": [dict(button) for button in ITEM_1["buttons"]]},
{**ITEM_2, "buttons": [dict(button) for button in ITEM_2["buttons"]]},
]
async def history(key: str, *, limit: int = 10) -> list[dict[str, Any]]:
return []
async def create_sequence(key: str, title: str, description: str, *, editor_id: int | None = None) -> bool:
state["created"] = (key, title, description)
return True
async def update_sequence(key: str, title: str, description: str, *, editor_id: int | None = None) -> bool:
state["meta"] = (key, title, description)
return True
async def add_item(key: str, *, editor_id: int | None = None) -> int:
state["added"] = key
return 3
async def update_item(
item_id: int,
text: str,
enabled: bool,
buttons: list[tuple[str, str]],
*,
image_name: str | None = None,
update_image: bool = False,
editor_id: int | None = None,
) -> tuple[str, str | None]:
state["item"] = {
"id": item_id,
"text": text,
"enabled": enabled,
"buttons": buttons,
"image_name": image_name,
"update_image": update_image,
}
return "welcome", None
async def move_item(item_id: int, direction: str, *, editor_id: int | None = None) -> str:
state["move"] = (item_id, direction)
return "welcome"
async def delete_item(item_id: int, *, editor_id: int | None = None) -> str:
state["deleted"] = item_id
return "welcome"
monkeypatch.setattr(sequences_repo, "list_sequences", list_sequences)
monkeypatch.setattr(sequences_repo, "get_sequence", get_sequence)
monkeypatch.setattr(sequences_repo, "get_items", get_items)
monkeypatch.setattr(sequences_repo, "history", history)
monkeypatch.setattr(sequences_repo, "create_sequence", create_sequence)
monkeypatch.setattr(sequences_repo, "update_sequence", update_sequence)
monkeypatch.setattr(sequences_repo, "add_item", add_item)
monkeypatch.setattr(sequences_repo, "update_item", update_item)
monkeypatch.setattr(sequences_repo, "move_item", move_item)
monkeypatch.setattr(sequences_repo, "delete_item", delete_item)
return state
def _csrf(client: TestClient, csrf_token, path: str = "/sequences/welcome") -> str:
return csrf_token(client.get(path).text)
def test_parse_buttons() -> None:
buttons, errors = parse_buttons("Канал | https://t.me/x\n\nПравила | http://example.com")
assert errors == []
assert buttons == [("Канал", "https://t.me/x"), ("Правила", "http://example.com")]
def test_parse_buttons_reports_bad_lines() -> None:
buttons, errors = parse_buttons("без ссылки\nТекст | ftp://bad\n | https://x")
assert buttons == []
assert len(errors) == 3
def test_sequences_list(fake_sequences: dict[str, Any], client_factory) -> None:
response = client_factory().get("/sequences")
assert response.status_code == 200
assert "welcome" in response.text
assert "Приветствие" in response.text
assert "<built-in method" not in response.text
assert "Шаблоны сообщений" in response.text
def test_sequence_editor_has_toolbar(fake_sequences: dict[str, Any], client_factory) -> None:
response = client_factory().get("/sequences/welcome")
assert response.status_code == 200
assert "Правила клана" in response.text
assert 'data-tag="b"' in response.text
assert "Канал | https://t.me/example" in response.text
def test_create_sequence(fake_sequences: dict[str, Any], client_factory, csrf_token) -> None:
client: TestClient = client_factory(follow_redirects=False)
token = _csrf(client, csrf_token, "/sequences")
response = client.post(
"/sequences",
data={"key": "rules", "title": "Правила", "description": "Текст", "csrf_token": token},
)
assert response.status_code == 303
assert response.headers["location"] == "/sequences/rules"
assert fake_sequences["created"] == ("rules", "Правила", "Текст")
def test_update_sequence_meta(fake_sequences: dict[str, Any], client_factory, csrf_token) -> None:
client: TestClient = client_factory(follow_redirects=False)
token = _csrf(client, csrf_token)
response = client.post(
"/sequences/welcome",
data={"title": "Новое", "description": "Описание", "csrf_token": token},
)
assert response.status_code == 303
assert fake_sequences["meta"] == ("welcome", "Новое", "Описание")
def test_add_item(fake_sequences: dict[str, Any], client_factory, csrf_token) -> None:
client: TestClient = client_factory(follow_redirects=False)
token = _csrf(client, csrf_token)
response = client.post("/sequences/welcome/items", data={"csrf_token": token})
assert response.status_code == 303
assert fake_sequences["added"] == "welcome"
def test_update_item_text_and_buttons(fake_sequences: dict[str, Any], client_factory, csrf_token) -> None:
client: TestClient = client_factory(follow_redirects=False)
token = _csrf(client, csrf_token)
response = client.post(
"/sequences/welcome/items/2",
data={
"text": "Новые правила",
"buttons": "Канал | https://t.me/new\nЧат | https://t.me/chat",
"enabled": "on",
"csrf_token": token,
},
)
assert response.status_code == 303
saved = fake_sequences["item"]
assert saved["text"] == "Новые правила"
assert saved["buttons"] == [("Канал", "https://t.me/new"), ("Чат", "https://t.me/chat")]
assert saved["enabled"] is True
def test_update_item_rejects_bad_buttons(fake_sequences: dict[str, Any], client_factory, csrf_token) -> None:
client: TestClient = client_factory()
token = _csrf(client, csrf_token)
response = client.post(
"/sequences/welcome/items/2",
data={"text": "Текст", "buttons": "без ссылки", "csrf_token": token},
)
assert response.status_code == 400
assert "Кнопка без разделителя" in response.text
assert "item" not in fake_sequences
def test_update_item_rejects_caption_overflow(fake_sequences: dict[str, Any], client_factory, csrf_token) -> None:
client: TestClient = client_factory()
token = _csrf(client, csrf_token)
response = client.post(
"/sequences/welcome/items/1",
data={"text": "а" * 1500, "enabled": "on", "csrf_token": token},
)
assert response.status_code == 400
assert "не больше 1024" in response.text
assert "item" not in fake_sequences
def test_update_item_uploads_image(
fake_sequences: dict[str, Any],
client_factory,
csrf_token,
settings_factory,
tmp_path,
) -> None:
settings = settings_factory(media_dir=str(tmp_path))
client: TestClient = client_factory(settings=settings, follow_redirects=False)
token = _csrf(client, csrf_token)
response = client.post(
"/sequences/welcome/items/2",
data={"text": "С картинкой", "enabled": "on", "csrf_token": token},
files={"image": ("pic.png", b"fake-png-bytes", "image/png")},
)
assert response.status_code == 303
assert fake_sequences["item"]["update_image"] is True
files = list(tmp_path.glob("*.png"))
assert len(files) == 1
def test_move_and_delete_item(fake_sequences: dict[str, Any], client_factory, csrf_token) -> None:
client: TestClient = client_factory(follow_redirects=False)
token = _csrf(client, csrf_token)
moved = client.post(
"/sequences/welcome/items/1/move",
data={"direction": "down", "csrf_token": token},
)
deleted = client.post("/sequences/welcome/items/2/delete", data={"csrf_token": token})
assert moved.status_code == 303
assert deleted.status_code == 303
assert fake_sequences["move"] == (1, "down")
assert fake_sequences["deleted"] == 2