import io from django.core.files.uploadedfile import SimpleUploadedFile from django.test import Client, TestCase from django.urls import reverse from PIL import Image from .forms import HalftoneGenerateForm from .models import TbHypn0Item, TbVote from .services.halftone import encode_to_base36, generate_halftone_svg, prepare_gallery_svg from .services.naming import generate_hypno_title, generate_title_openrouter class HalftoneServiceTests(TestCase): """Тестирование сервиса генерации SVG (services/halftone.py).""" def setUp(self): # Создаем простое тестовое изображение 50x50 с градиентом self.img = Image.new("RGB", (50, 50), color="white") for x in range(25): for y in range(50): self.img.putpixel((x, y), (0, 0, 0)) def test_encode_to_base36(self): self.assertEqual(encode_to_base36(0), "a") self.assertEqual(encode_to_base36(1), "b") with self.assertRaises(ValueError): encode_to_base36(-1) def test_generate_default_svg(self): svg = generate_halftone_svg(self.img, cols=30, max_radius=6) self.assertIn("", svg) self.assertIn("", svg) self.assertIn('', svg) def test_generate_shapes(self): shapes = ["circle", "ring", "square", "diamond", "triangle", "hexagon", "star", "cross", "line", "wave", "heart"] for shape in shapes: svg = generate_halftone_svg(self.img, cols=20, max_radius=5, shape=shape) self.assertIn("", svg) self.assertIn("", svg) def test_generate_static_blink_zero(self): svg = generate_halftone_svg(self.img, cols=20, blink=0) self.assertIn("animation:none", svg) self.assertNotIn("@keyframes noise", svg) def test_generate_animated_params(self): svg = generate_halftone_svg( self.img, cols=20, blink=8, rotation=12, scale=920, angle=15, color="#ff5500", ) self.assertIn("@keyframes noise", svg) self.assertIn("rotate(12deg)", svg) self.assertIn("scale(0.92)", svg) self.assertIn("#ff5500", svg) def test_generate_from_bytes(self): buf = io.BytesIO() self.img.save(buf, format="PNG") svg = generate_halftone_svg(buf.getvalue(), cols=20) self.assertIn(" width = 10 * 12 = 120, height = 30 * 12 = 360 self.assertIn('viewBox="0 0 120 360"', svg_tall) # Для горизонтального изображения (300x100) wide_img = Image.new("RGB", (300, 100), color="black") svg_wide = generate_halftone_svg(wide_img, cols=30, max_radius=5) self.assertIn('viewBox="0 0 360 120"', svg_wide) class HalftoneFormTests(TestCase): """Тестирование формы валидации HalftoneGenerateForm.""" def test_form_validation(self): buf = io.BytesIO() img = Image.new("RGB", (30, 30), color="red") img.save(buf, format="PNG") buf.seek(0) uploaded = SimpleUploadedFile("test.png", buf.read(), content_type="image/png") form = HalftoneGenerateForm( data={ "shape": "square", "cols": 60, "max_radius": 7, "blink": 5, "rotation": 10, "scale": 950, "angle": -10, }, files={"image": uploaded}, ) self.assertTrue(form.is_valid()) self.assertEqual(form.cleaned_data["shape"], "square") self.assertEqual(form.cleaned_data["cols"], 60) class HalftoneViewTests(TestCase): """Тестирование view-обработчиков (index, generate).""" def setUp(self): self.client = Client() def test_index_page(self): response = self.client.get(reverse("hypn0_site:index")) self.assertEqual(response.status_code, 200) self.assertContains(response, "logo-hypn0.svg") self.assertContains(response, 'id="preview"') self.assertContains(response, 'name="csrfmiddlewaretoken"') self.assertContains(response, 'X-CSRFToken') self.assertContains(response, "handleFileChange") self.assertContains(response, "imagePreviewUrl") self.assertContains(response, "Гипножаба облизывается на:") # Проверяем, что csrf_token не пустой в hx-headers content = response.content.decode("utf-8") self.assertNotIn('hx-headers=\'{"X-CSRFToken": ""}\'', content) def test_generate_with_enforce_csrf(self): csrf_client = Client(enforce_csrf_checks=True) response = csrf_client.get(reverse("hypn0_site:index")) self.assertEqual(response.status_code, 200) buf = io.BytesIO() img = Image.new("RGB", (40, 40), color="black") img.save(buf, format="PNG") buf.seek(0) uploaded = SimpleUploadedFile("test.png", buf.read(), content_type="image/png") # Без CSRF токена вернет 403 bad_response = csrf_client.post( reverse("hypn0_site:generate"), data={"shape": "circle", "image": uploaded}, ) self.assertEqual(bad_response.status_code, 403) # С CSRF токеном в POST data вернет 200 csrf_token = response.cookies["csrftoken"].value buf.seek(0) uploaded = SimpleUploadedFile("test.png", buf.read(), content_type="image/png") good_response = csrf_client.post( reverse("hypn0_site:generate"), data={ "csrfmiddlewaretoken": csrf_token, "shape": "circle", "image": uploaded, }, ) self.assertEqual(good_response.status_code, 200) self.assertContains(good_response, " 5) # Проверка детерминированности по seed self.assertEqual(generate_hypno_title(seed=42), title) def test_openrouter_stub(self): res = generate_title_openrouter(b"dummy_bytes") self.assertIsNone(res) class GalleryPreparationTests(TestCase): """Тестирование подготовки SVG к галерейному хранению.""" def test_prepare_gallery_svg(self): raw_svg = '' prepared = prepare_gallery_svg(raw_svg) self.assertIn("svg:not(:hover)", prepared) self.assertIn("animation-play-state:paused!important", prepared) class PublishViewTests(TestCase): """Тестирование эндпоинта публикации кандидата в галерею (/publish).""" def setUp(self): self.client = Client() self.sample_svg = '' def test_publish_without_cookie_returns_slug_protest(self): response = self.client.post( reverse("hypn0_site:publish"), data={ "svg_content": self.sample_svg, "shape": "circle", "cols": "35", }, ) self.assertEqual(response.status_code, 200) self.assertContains(response, "Мозговые слизняки протестуют!") self.assertContains(response, "Подчиниться и опубликовать") self.assertEqual(TbHypn0Item.objects.count(), 0) def test_publish_with_cookie_success(self): vid = "123e4567-e89b-12d3-a456-426614174000" self.client.cookies["hypn0_vid"] = vid response = self.client.post( reverse("hypn0_site:publish"), data={ "svg_content": self.sample_svg, "shape": "diamond", "cols": "40", "max_radius": "6", "blink": "7", "rotation": "5", "scale": "950", "angle": "10", "color": "#10b981", }, ) self.assertEqual(response.status_code, 200) self.assertContains(response, "Уже в галерее транса") self.assertContains(response, "идет прорастание психо-паутины...") # Проверяем запись в БД self.assertEqual(TbHypn0Item.objects.count(), 1) item = TbHypn0Item.objects.first() self.assertTrue(len(item.s_hash_id) >= 6) self.assertIn(f"#{item.s_hash_id}", response.content.decode("utf-8")) self.assertEqual(item.i_level, TbHypn0Item.Level.CANDIDATE) self.assertEqual(item.i_likes_count, 1) self.assertEqual(item.j_metadata["shape"], "diamond") self.assertEqual(item.j_metadata["cols"], 40) self.assertEqual(item.j_metadata["color"], "#10b981") # Проверяем авторский голос в TbVote self.assertEqual(TbVote.objects.count(), 1) author_vote = TbVote.objects.first() self.assertEqual(author_vote.k_item, item) self.assertEqual(author_vote.i_direction, TbVote.Direction.AUTHOR) def test_publish_empty_svg_returns_error(self): self.client.cookies["hypn0_vid"] = "123e4567-e89b-12d3-a456-426614174000" response = self.client.post( reverse("hypn0_site:publish"), data={"svg_content": ""}, ) self.assertEqual(response.status_code, 200) self.assertContains(response, "Нет данных SVG для публикации") self.assertEqual(TbHypn0Item.objects.count(), 0)