Files
social-post-writer-bot/tests/test_writer_features.py
T

159 lines
7.4 KiB
Python

import asyncio
import json
import unittest
from unittest.mock import patch
import apinex_client
import bot
import user_store
class FakeResponse:
def __init__(self, status, payload):
self.status = status
self.payload = payload
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
async def json(self, content_type=None):
return self.payload
class FakeClientSession:
def __init__(self, response, **kwargs):
self.response = response
self.kwargs = kwargs
self.request: tuple[str, dict] = ("", {})
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
return False
def post(self, url, **kwargs):
self.request = (url, kwargs)
return self.response
class ApinexTests(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.env = patch.dict("os.environ", {"APINEX_API_KEY": "not-logged"}, clear=False)
self.env.start()
self.addCleanup(self.env.stop)
def test_extract_transcript(self):
payload = {"results": {"channels": [{"alternatives": [{"transcript": " Привет, мир! "}]}]}}
self.assertEqual(apinex_client.extract_transcript(payload), "Привет, мир!")
self.assertEqual(apinex_client.extract_transcript({}), "")
async def test_transcribe_ogg_and_model_config(self):
response = FakeResponse(200, {"results": {"channels": [{"alternatives": [{"transcript": "Готово"}]}]}})
fake_session = FakeClientSession(response)
with patch.object(apinex_client.aiohttp, "ClientSession", return_value=fake_session), patch.dict(
"os.environ", {"APINEX_STT_MODEL": "nova-2"}, clear=False
):
result = await apinex_client.transcribe_audio(b"ogg-data", "audio/ogg")
self.assertEqual(result, "Готово")
self.assertIsNotNone(fake_session.request)
url, kwargs = fake_session.request
self.assertEqual(url, apinex_client.APINEX_STT_URL)
self.assertEqual(kwargs["params"]["language"], "ru")
self.assertEqual(kwargs["params"]["model"], "nova-2")
self.assertEqual(kwargs["headers"]["Content-Type"], "audio/ogg")
self.assertEqual(kwargs["headers"]["Authorization"], "Bearer not-logged")
self.assertIn("Mozilla/5.0", kwargs["headers"]["User-Agent"])
async def test_chat_completion_uses_selected_model_and_parses_response(self):
response = FakeResponse(200, {"choices": [{"message": {"content": "Ответ модели"}}]})
fake_session = FakeClientSession(response)
with patch.object(apinex_client.aiohttp, "ClientSession", return_value=fake_session), patch.dict(
"os.environ", {"APINEX_POST_WRITER_MODEL": "gemini-3.8-flash"}, clear=False
):
result = await apinex_client.chat_text("APINEX_POST_WRITER_MODEL", "system", "user")
self.assertEqual(result, "Ответ модели")
self.assertIsNotNone(fake_session.request)
_, kwargs = fake_session.request
self.assertEqual(kwargs["json"]["model"], "gemini-3.8-flash")
self.assertEqual(kwargs["json"]["messages"][0]["role"], "system")
async def test_image_is_sent_as_private_inline_data_url(self):
response = FakeResponse(200, {"choices": [{"message": {"content": "Дружелюбно и кратко"}}]})
fake_session = FakeClientSession(response)
with patch.object(apinex_client.aiohttp, "ClientSession", return_value=fake_session):
tone = await apinex_client.extract_tone_from_image(b"PNG", "image/png")
self.assertEqual(tone, "Дружелюбно и кратко")
self.assertIsNotNone(fake_session.request)
_, kwargs = fake_session.request
content = kwargs["json"]["messages"][1]["content"]
self.assertTrue(content[1]["image_url"]["url"].startswith("data:image/png;base64,"))
async def test_image_rejects_invalid_screenshot_marker(self):
response = FakeResponse(200, {"choices": [{"message": {"content": "NOT_POST_SCREENSHOT"}}]})
with patch.object(apinex_client.aiohttp, "ClientSession", return_value=FakeClientSession(response)):
self.assertEqual(await apinex_client.extract_tone_from_image(b"img", "image/jpeg"), "")
async def test_intent_validation_and_three_posts(self):
with patch.object(apinex_client, "chat_text", return_value='{"intent":"write_post","topic":"кофе"}'):
intent = await apinex_client.classify_intent("придумай пост про кофе")
self.assertEqual(intent["intent"], "write_post")
with patch.object(apinex_client, "chat_text", return_value='{"posts":["A","B","C"]}'):
posts = await apinex_client.generate_posts(intent, "дружелюбный")
self.assertEqual(posts, ["A", "B", "C"])
with patch.object(apinex_client, "chat_text", return_value='{"posts":["A", "B"]}'):
with self.assertRaises(ValueError):
await apinex_client.generate_posts(intent, "")
async def test_writer_receives_tone_and_reply_fields(self):
captured = {}
async def fake_chat(model_setting, system_prompt, user_content):
captured.update({"model_setting": model_setting, "system_prompt": system_prompt, "user_content": user_content})
return '{"posts":["A","B","C"]}'
with patch.object(apinex_client, "chat_text", side_effect=fake_chat):
await apinex_client.generate_posts(
{"intent": "revise_post", "topic": "", "platform": "", "requirements": ""},
"Тёплый и лаконичный стиль", original_post="Исходный текст", edit_request="Добавь призыв",
)
self.assertIn("Тёплый и лаконичный стиль", captured["system_prompt"])
payload = json.loads(captured["user_content"])
self.assertEqual(payload["original_post"], "Исходный текст")
self.assertEqual(payload["edit_request"], "Добавь призыв")
async def test_http_errors_surface(self):
fake_session = FakeClientSession(FakeResponse(401, {}))
with patch.object(apinex_client.aiohttp, "ClientSession", return_value=fake_session):
with self.assertRaisesRegex(RuntimeError, "HTTP 401"):
await apinex_client.transcribe_audio(b"ogg-data", "audio/ogg")
class StorageTests(unittest.TestCase):
def test_user_tone_isolated_per_id(self):
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as temp_dir:
db = str(Path(temp_dir) / "users.sqlite3")
user_store.init_db(db)
self.assertEqual(user_store.get_tone_of_voice(1, db), "")
user_store.save_tone_of_voice(1, "дружелюбный", db)
user_store.save_tone_of_voice(2, "официальный", db)
self.assertEqual(user_store.get_tone_of_voice(1, db), "дружелюбный")
self.assertEqual(user_store.get_tone_of_voice(2, db), "официальный")
class TelegramUtilsTests(unittest.TestCase):
def test_split_text_preserves_tokens_and_limit(self):
chunks = bot.split_text("слово " * 2000, 4000)
self.assertTrue(all(len(chunk) <= 4000 for chunk in chunks))
self.assertEqual(" ".join(chunks).split(), ("слово " * 2000).split())
if __name__ == "__main__":
unittest.main()