diff --git a/tests/test_contents/test_base.py b/tests/test_contents/test_base.py index 7cdc630..f29dd7e 100644 --- a/tests/test_contents/test_base.py +++ b/tests/test_contents/test_base.py @@ -1,111 +1,78 @@ import re -import unittest.mock -import pydantic import pytest +from pydantic import ValidationError from aiogram_broadcaster.contents.base import VALIDATOR_KEY, BaseContent +class DummyContent(BaseContent, register=False): + async def __call__(self, **kwargs): + return {"method": "dummy_method", **kwargs} + + class TestBaseContent: - def test_register_and_unregister(self): + def test_registration(self): + assert not DummyContent.is_registered() + DummyContent.register() + assert DummyContent.is_registered() + DummyContent.unregister() + assert not DummyContent.is_registered() with pytest.raises( TypeError, match="BaseContent cannot be registered.", ): BaseContent.register() - class RegisteredContent(BaseContent): - pass - - class UnregisteredContent(BaseContent, register=False): - pass - - assert RegisteredContent.is_registered() - RegisteredContent.unregister() - assert not RegisteredContent.is_registered() + def test_validate_invalid_type(self): + with pytest.raises(ValidationError): + BaseContent.model_validate(object()) + def test_double_registration(self): + DummyContent.register() with pytest.raises( RuntimeError, - match="The content 'RegisteredContent' is not registered.", + match="The content 'DummyContent' is already registered.", ): - RegisteredContent.unregister() - - assert not UnregisteredContent.is_registered() - UnregisteredContent.register() - assert UnregisteredContent.is_registered() + DummyContent.register() + DummyContent.unregister() + def test_unregister_non_registered(self): with pytest.raises( RuntimeError, - match="The content 'UnregisteredContent' is already registered.", + match="The content 'DummyContent' is not registered.", ): - UnregisteredContent.register() + DummyContent.unregister() - UnregisteredContent.unregister() + async def test_callable(self): + content = DummyContent() + result = await content(param="value") + assert result == {"method": "dummy_method", "param": "value"} async def test_as_method(self): - MOCK_RESULT = unittest.mock.sentinel.RESULT - - class MyContent(BaseContent, register=False): - async def __call__(self, test_param): - MOCK_RESULT.test_param = test_param - return MOCK_RESULT - - content = MyContent() - result = await content.as_method(test_param=1) - assert result is MOCK_RESULT - assert result.test_param == 1 - - async def test_as_method_calls_callback(self): - class MyContent(BaseContent, register=False, frozen=False): - async def __call__(self, **kwargs): - return - - content = MyContent() - with unittest.mock.patch.object( - target=MyContent, - attribute="_callback", - new_callable=unittest.mock.AsyncMock, - ) as mocked_callback: - await content.as_method(test_param=1) - mocked_callback.call.assert_called_once_with(content, test_param=1) - - def test_serialization(self): - class MyContent(BaseContent, register=False): - some_field: str - - async def __call__(self): - return - - content = MyContent(some_field="test") - serialized = content.model_dump() - assert serialized[VALIDATOR_KEY] == "MyContent" - assert serialized["some_field"] == "test" + content = DummyContent() + result = await content.as_method(param="value") + assert result == {"method": "dummy_method", "param": "value"} def test_validation(self): - class MyContent(BaseContent, register=True): - some_field: str - - async def __call__(self): - return - - with pytest.raises(pydantic.ValidationError): - BaseContent.model_validate(object()) + DummyContent.register() + valid_data = {VALIDATOR_KEY: "DummyContent", "param": "value"} + content = DummyContent.model_validate(valid_data) + assert content.param == "value" + invalid_data = {VALIDATOR_KEY: "NonExistentContent", "param": "value"} with pytest.raises( - ValueError, + ValidationError, match=re.escape( - "Content 'NotExistsContent' has not been registered, " - "you can register using the 'NotExistsContent.register()' method.", + "Content 'NonExistentContent' has not been registered, " + "you can register using the 'NonExistentContent.register()' method.", ), ): - BaseContent.model_validate({VALIDATOR_KEY: "NotExistsContent", "some_field": "test"}) - - validated_content = BaseContent.model_validate({ - VALIDATOR_KEY: "MyContent", - "some_field": "test", - }) - assert isinstance(validated_content, MyContent) - assert validated_content.some_field == "test" + BaseContent.model_validate(invalid_data) + DummyContent.unregister() - MyContent.unregister() + def test_serialization(self): + content = DummyContent(param="value") + serialized = content.model_dump() + assert serialized[VALIDATOR_KEY] == "DummyContent" + assert serialized["param"] == "value" diff --git a/tests/test_contents/test_contents.py b/tests/test_contents/test_contents.py new file mode 100644 index 0000000..3ca6a7a --- /dev/null +++ b/tests/test_contents/test_contents.py @@ -0,0 +1,195 @@ +import pytest +from aiogram.methods.base import TelegramMethod +from aiogram.methods.copy_message import CopyMessage +from aiogram.methods.forward_message import ForwardMessage +from aiogram.methods.send_animation import SendAnimation +from aiogram.methods.send_audio import SendAudio +from aiogram.methods.send_chat_action import SendChatAction +from aiogram.methods.send_contact import SendContact +from aiogram.methods.send_dice import SendDice +from aiogram.methods.send_document import SendDocument +from aiogram.methods.send_game import SendGame +from aiogram.methods.send_invoice import SendInvoice +from aiogram.methods.send_location import SendLocation +from aiogram.methods.send_media_group import SendMediaGroup +from aiogram.methods.send_message import SendMessage +from aiogram.methods.send_photo import SendPhoto +from aiogram.methods.send_poll import SendPoll +from aiogram.methods.send_sticker import SendSticker +from aiogram.methods.send_venue import SendVenue +from aiogram.methods.send_video import SendVideo +from aiogram.methods.send_video_note import SendVideoNote +from aiogram.methods.send_voice import SendVoice + +from aiogram_broadcaster.contents.animation import AnimationContent +from aiogram_broadcaster.contents.audio import AudioContent +from aiogram_broadcaster.contents.chat_action import ChatActionContent +from aiogram_broadcaster.contents.contact import ContactContent +from aiogram_broadcaster.contents.dice import DiceContent +from aiogram_broadcaster.contents.document import DocumentContent +from aiogram_broadcaster.contents.from_chat import FromChatCopyContent, FromChatForwardContent +from aiogram_broadcaster.contents.game import GameContent +from aiogram_broadcaster.contents.invoice import InvoiceContent +from aiogram_broadcaster.contents.location import LocationContent +from aiogram_broadcaster.contents.media_group import MediaGroupContent +from aiogram_broadcaster.contents.message import ( + MessageCopyContent, + MessageForwardContent, + MessageSendContent, +) +from aiogram_broadcaster.contents.photo import PhotoContent +from aiogram_broadcaster.contents.poll import PollContent +from aiogram_broadcaster.contents.sticker import StickerContent +from aiogram_broadcaster.contents.text import TextContent +from aiogram_broadcaster.contents.venue import VenueContent +from aiogram_broadcaster.contents.video import VideoContent +from aiogram_broadcaster.contents.video_note import VideoNoteContent +from aiogram_broadcaster.contents.voice import VoiceContent + + +@pytest.mark.parametrize( + ("expected_method", "content_class", "content_data"), + [ + ( + SendAnimation, + AnimationContent, + {"animation": "test"}, + ), + ( + SendAudio, + AudioContent, + {"audio": "test"}, + ), + ( + SendChatAction, + ChatActionContent, + {"action": "test"}, + ), + ( + SendContact, + ContactContent, + {"phone_number": "test", "first_name": "test"}, + ), + ( + SendDice, + DiceContent, + {}, + ), + ( + SendDocument, + DocumentContent, + {"document": "test"}, + ), + ( + CopyMessage, + FromChatCopyContent, + {"from_chat_id": 0, "message_id": 0}, + ), + ( + ForwardMessage, + FromChatForwardContent, + {"from_chat_id": 0, "message_id": 0}, + ), + ( + SendGame, + GameContent, + {"game_short_name": "test"}, + ), + ( + SendInvoice, + InvoiceContent, + { + "title": "test", + "description": "test", + "payload": "test", + "provider_token": "test", + "currency": "test", + "prices": [{"label": "test", "amount": 0}], + }, + ), + ( + SendLocation, + LocationContent, + {"latitude": 0, "longitude": 0}, + ), + ( + SendMediaGroup, + MediaGroupContent, + {"media": [{"media": "test"}]}, + ), + ( + CopyMessage, + MessageCopyContent, + {"message": {"message_id": 0, "date": 0, "chat": {"id": 0, "type": "test"}}}, + ), + ( + ForwardMessage, + MessageForwardContent, + {"message": {"message_id": 0, "date": 0, "chat": {"id": 0, "type": "test"}}}, + ), + ( + TelegramMethod, + MessageSendContent, + { + "message": { + "message_id": 0, + "date": 0, + "chat": {"id": 0, "type": "test"}, + "text": "test", + }, + }, + ), + ( + SendPhoto, + PhotoContent, + {"photo": "test"}, + ), + ( + SendPoll, + PollContent, + {"question": "test", "options": ["test"]}, + ), + ( + SendSticker, + StickerContent, + {"sticker": "test"}, + ), + ( + SendMessage, + TextContent, + {"text": "test"}, + ), + ( + SendVenue, + VenueContent, + {"latitude": 0, "longitude": 0, "title": "test", "address": "test"}, + ), + ( + SendVideo, + VideoContent, + {"video": "test"}, + ), + ( + SendVideoNote, + VideoNoteContent, + {"video_note": "test"}, + ), + ( + SendVoice, + VoiceContent, + {"voice": "test"}, + ), + ], +) +class TestContents: + async def test_as_method(self, expected_method, content_class, content_data): + content = content_class(**content_data, test_extra=1) + result = await content.as_method(chat_id=1) + assert isinstance(result, expected_method) + assert result.chat_id == 1 + if content_class is MessageSendContent: + pytest.skip( + "MessageSendContent does not support passing extra, " + "since the Message.send_copy method does not accept **kwargs.", + ) + assert result.test_extra == 1 diff --git a/tests/test_contents/test_key_based.py b/tests/test_contents/test_key_based.py index d205a36..ec653b0 100644 --- a/tests/test_contents/test_key_based.py +++ b/tests/test_contents/test_key_based.py @@ -4,52 +4,69 @@ from aiogram_broadcaster.contents.key_based import KeyBasedContent -class MyContent(BaseContent, register=False): - some_field: str = "test" +class DummyContent(BaseContent, register=False): + async def __call__(self, **kwargs): + return { + "method": "dummy_method", + **kwargs, + **(self.model_extra or {}), + } - async def __call__(self): - return "my content result" - -class MyKeyBasedContent(KeyBasedContent, register=False): - async def __call__(self): - return "test_key" +class DummyKeyBasedContent(KeyBasedContent, register=False): + async def __call__(self, **kwargs): + return "key1" class TestKeyBasedContent: - def test_init_without_args(self): + async def test_key_based_content_as_method(self): + content = DummyKeyBasedContent( + key1=DummyContent(param="value1"), + key2=DummyContent(param="value2"), + default=DummyContent(param="default_value"), + ) + result = await content.as_method(context_param="context_value") + assert isinstance(result, dict) + assert result == { + "method": "dummy_method", + "param": "value1", + "context_param": "context_value", + } + + async def test_key_based_content_getitem(self): + content = DummyKeyBasedContent( + key1=DummyContent(param="value1"), + key2=DummyContent(param="value2"), + default=DummyContent(param="default_value"), + ) + assert content["key1"].param == "value1" + assert content["key2"].param == "value2" + assert content["key3"].param == "default_value" + + async def test_get_not_exists_key(self): + content = DummyKeyBasedContent( + key1=DummyContent(param="value1"), + key2=DummyContent(param="value2"), + ) + assert content["key1"].param == "value1" + assert content["key2"].param == "value2" + + with pytest.raises(KeyError): + content["not_exists_key"] + + def test_key_based_content_contains(self): + content = DummyKeyBasedContent( + key1=DummyContent(param="value1"), + key2=DummyContent(param="value2"), + default=DummyContent(param="default_value"), + ) + assert "key1" in content + assert "key2" in content + assert "key3" not in content + + def test_key_based_content_no_contents(self): with pytest.raises( ValueError, match="At least one content must be specified.", ): - MyKeyBasedContent() - - def test_init_only_extra(self): - MyKeyBasedContent(foo=MyContent()) - - def test_init_only_default(self): - MyKeyBasedContent(default=MyContent()) - - def test_init(self): - foo = MyContent() - bar = MyContent() - content = MyKeyBasedContent(foo=foo, bar=bar) - assert foo == content["foo"] == content.__pydantic_extra__["foo"] - assert bar == content["bar"] == content.__pydantic_extra__["bar"] - assert "foo" in content - assert "bar" in content - assert "baz" not in content - - def test_init_with_default(self): - default = MyContent() - foo = MyContent() - content = MyKeyBasedContent(default=default, foo=foo) - assert foo == content["foo"] == content.__pydantic_extra__["foo"] - assert content["bar"] == default - assert "bar" not in content.__pydantic_extra__ - assert "foo" in content - assert "bar" not in content - - async def test_as_method(self): - content = MyKeyBasedContent(default=MyContent()) - assert await content.as_method() == "my content result" + DummyKeyBasedContent() diff --git a/tests/test_contents/test_lazy.py b/tests/test_contents/test_lazy.py index 570bc1f..ed3aa02 100644 --- a/tests/test_contents/test_lazy.py +++ b/tests/test_contents/test_lazy.py @@ -1,50 +1,43 @@ -import unittest.mock - import pytest from aiogram_broadcaster.contents.base import BaseContent from aiogram_broadcaster.contents.lazy import LazyContent -class MyContent(BaseContent, register=False): - some_field: str = "test" - - async def __call__(self): - return "content result" +class DummyContent(BaseContent, register=False): + async def __call__(self, **kwargs): + return { + "method": "dummy_method", + **kwargs, + **(self.model_extra or {}), + } -class MyLazyContent(LazyContent, register=False, frozen=False): - async def __call__(self): - return MyContent() +class DummyLazyContent(LazyContent, register=False): + async def __call__(self, **kwargs): + return DummyContent(param="value") class TestLazyContent: - async def test_as_method(self): - content = MyLazyContent() - assert await content.as_method() == "content result" - - async def test_as_method_calls_callback(self): - content = MyLazyContent() - with unittest.mock.patch.object( - target=content._callback, - attribute="call", - new_callable=unittest.mock.AsyncMock, - return_value=MyContent(), - ) as mocked_callback: - await content.as_method(test_param=1) - mocked_callback.assert_called_once_with(content, test_param=1) - - async def test_as_method_invalid_type(self): - content = MyLazyContent() - with unittest.mock.patch.object( - target=content._callback, - attribute="call", - return_value="invalid type", - ), pytest.raises( + async def test_lazy_content_as_method(self): + content = DummyLazyContent() + result = await content.as_method(context_param="context_value") + assert isinstance(result, dict) + assert result == { + "method": "dummy_method", + "param": "value", + "context_param": "context_value", + } + + async def test_lazy_content_as_method_invalid_return(self): + class InvalidLazyContent(LazyContent): + async def __call__(self, **kwargs): + return "not_a_base_content" + + content = InvalidLazyContent() + with pytest.raises( TypeError, - match=( - "The MyLazyContent expected to return an content of " - "type BaseContent, not a str." - ), + match="The 'InvalidLazyContent' expected to return an content of " + "type BaseContent, not a str.", ): - await content.as_method() + await content.as_method(context_param="context_value") diff --git a/tests/test_event.py b/tests/test_event.py deleted file mode 100644 index 3e233e2..0000000 --- a/tests/test_event.py +++ /dev/null @@ -1,102 +0,0 @@ -import unittest.mock - -import pytest - -from aiogram_broadcaster.event import EventManager, EventObserver, EventRouter - - -async def callback(): - return {"test_key": "test_value"} - - -async def second_callback(): - return {"test_key_2": "test_value_2"} - - -class TestEventObserver: - def test_init(self): - observer = EventObserver() - assert observer.callbacks == [] - - def test_register(self): - observer = EventObserver() - observer.register(callback) - assert tuple(observer) == (callback,) - assert len(observer) == 1 - - def test_register_many_callbacks(self): - observer = EventObserver() - observer.register(callback, second_callback) - assert tuple(observer) == (callback, second_callback) - assert len(observer) == 2 - - def test_call(self): - observer = EventObserver() - - @observer() - async def decorated_callback(): - return - - assert tuple(observer) == (decorated_callback,) - assert len(observer) == 1 - - def test_register_without_args(self): - observer = EventObserver() - with pytest.raises( - ValueError, - match="At least one callback must be provided to register.", - ): - observer.register() - - def test_register_no_callable(self): - observer = EventObserver() - with pytest.raises( - TypeError, - match="The callback must be callable.", - ): - observer.register("invalid type") - - def test_fluent_register(self): - observer = EventObserver() - assert observer.register(callback) == observer - - -class TestEventRouter: - def test_observers(self): - router = EventRouter() - assert router.started == router.observers["started"] == router["started"] - assert router.stopped == router.observers["stopped"] == router["stopped"] - assert router.completed == router.observers["completed"] == router["completed"] - assert router.before_sent == router.observers["before_sent"] == router["before_sent"] - assert router.success_sent == router.observers["success_sent"] == router["success_sent"] - assert router.failed_sent == router.observers["failed_sent"] == router["failed_sent"] - - -@pytest.mark.parametrize( - "event_name", - [ - "started", - "stopped", - "completed", - "before_sent", - "success_sent", - "failed_sent", - ], -) -class TestEventManager: - async def test_emit_event(self, event_name): - manager = EventManager() - manager.observers[event_name].register(callback, second_callback) - result = await manager.emit_event(event_name) - assert result == {"test_key": "test_value", "test_key_2": "test_value_2"} - - async def test_emit_methods(self, event_name): - manager = EventManager() - with unittest.mock.patch.object( - target=manager, - attribute="emit_event", - new_callable=unittest.mock.AsyncMock, - ) as mocked_emit_event: - emit_method = getattr(manager, f"emit_{event_name}") - await emit_method(test_param=1) - mocked_emit_event.assert_called_once_with(event_name, test_param=1) diff --git a/tests/test_event/__init__.py b/tests/test_event/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_event/test_manager.py b/tests/test_event/test_manager.py new file mode 100644 index 0000000..4cee697 --- /dev/null +++ b/tests/test_event/test_manager.py @@ -0,0 +1,65 @@ +import unittest.mock + +import pytest + +from aiogram_broadcaster.event.manager import EventManager + + +class TestEventManager: + async def test_emit_event(self): + manager = EventManager() + + mock_callback1 = unittest.mock.Mock(return_value={"data1": "value1"}) + mock_callback2 = unittest.mock.Mock(return_value={"data2": "value2"}) + + manager.started.register(mock_callback1) + manager.started.register(mock_callback2) + + context = {"initial": "context"} + result = await manager.emit_event("started", **context) + + assert result == {"data1": "value1", "data2": "value2"} + mock_callback1.assert_called_once_with(**context) + mock_callback2.assert_called_once_with(**context, data1="value1") + + async def test_emit_event_no_callbacks(self): + manager = EventManager() + + context = {"initial": "context"} + result = await manager.emit_event("started", **context) + + assert result == {} + + async def test_emit_event_with_non_dict_result(self): + manager = EventManager() + + mock_callback = unittest.mock.Mock(return_value="non_dict_result") + manager.started.register(mock_callback) + + context = {"initial": "context"} + result = await manager.emit_event("started", **context) + + assert result == {} + mock_callback.assert_called_once_with(**context) + + @pytest.mark.parametrize( + "event_name", + [ + "started", + "stopped", + "completed", + "before_sent", + "success_sent", + "failed_sent", + ], + ) + async def test_emit_methods(self, event_name): + manager = EventManager() + + with unittest.mock.patch.object( + target=manager, + attribute="emit_event", + ) as mocked_emit_event: + emit_method = getattr(manager, f"emit_{event_name}") + await emit_method(data1="value1") + mocked_emit_event.assert_called_once_with(event_name, data1="value1") diff --git a/tests/test_event/test_observer.py b/tests/test_event/test_observer.py new file mode 100644 index 0000000..5cc491a --- /dev/null +++ b/tests/test_event/test_observer.py @@ -0,0 +1,66 @@ +import pytest + +from aiogram_broadcaster.event.observer import EventObserver + + +class TestEventObserver: + def test_register_callback(self): + observer = EventObserver() + + def callback(): + pass + + observer.register(callback) + assert len(observer) == 1 + assert list(observer)[0] == callback + + def test_register_multiple_callbacks(self): + observer = EventObserver() + + def callback1(): + pass + + def callback2(): + pass + + observer.register(callback1, callback2) + assert len(observer) == 2 + callbacks = list(observer) + assert callback1 in callbacks + assert callback2 in callbacks + + def test_register_no_callbacks(self): + observer = EventObserver() + + with pytest.raises( + ValueError, + match="At least one callback must be provided to register.", + ): + observer.register() + + def test_register_non_callable(self): + observer = EventObserver() + + with pytest.raises( + TypeError, + match="The callback must be callable.", + ): + observer.register("not a callable") + + def test_callable_registration(self): + observer = EventObserver() + + @observer() + def callback(): + pass + + assert len(observer) == 1 + assert list(observer)[0] == callback + + def test_register_fluent(self): + observer = EventObserver() + + def callback(): + pass + + assert observer.register(callback) == observer diff --git a/tests/test_event/test_registry.py b/tests/test_event/test_registry.py new file mode 100644 index 0000000..1575b91 --- /dev/null +++ b/tests/test_event/test_registry.py @@ -0,0 +1,14 @@ +from aiogram_broadcaster.event.registry import EventRegistry + + +class TestEventRegistry: + def test_observers(self): + registry = EventRegistry() + assert registry.started == registry.observers["started"] == registry["started"] + assert registry.stopped == registry.observers["stopped"] == registry["stopped"] + assert registry.completed == registry.observers["completed"] == registry["completed"] + assert registry.before_sent == registry.observers["before_sent"] == registry["before_sent"] + assert ( + registry.success_sent == registry.observers["success_sent"] == registry["success_sent"] + ) + assert registry.failed_sent == registry.observers["failed_sent"] == registry["failed_sent"] diff --git a/tests/test_mailer/test_settings.py b/tests/test_mailer/test_settings.py new file mode 100644 index 0000000..e6aba37 --- /dev/null +++ b/tests/test_mailer/test_settings.py @@ -0,0 +1,66 @@ +import sys + +import pytest +from pydantic import ValidationError + +from aiogram_broadcaster.mailer.settings import DefaultMailerSettings, MailerSettings + + +class TestMailerSettings: + def test_default_values(self): + settings = MailerSettings() + assert settings.interval == 0 + assert settings.run_on_startup is False + assert settings.handle_retry_after is False + assert settings.destroy_on_complete is False + assert settings.disable_events is False + assert settings.preserved is True + assert settings.exclude_placeholders is None + + def test_invalid_interval(self): + with pytest.raises(ValidationError): + MailerSettings(interval=-1) + + +class TestDefaultMailerSettings: + def test_default_values(self): + settings = DefaultMailerSettings() + assert settings.interval == 0 + assert settings.dynamic_interval is False + assert settings.run_on_startup is False + assert settings.handle_retry_after is False + assert settings.destroy_on_complete is False + assert settings.preserve is False + + def test_invalid_interval(self): + with pytest.raises( + ValueError, + match="The interval must be greater than or equal to 0.", + ): + DefaultMailerSettings(interval=-1) + + def test_prepare_method(self): + default_settings = DefaultMailerSettings(interval=5, dynamic_interval=True) + prepared_settings = default_settings.prepare(run_on_startup=True, preserve=True) + assert prepared_settings.interval == 5 + assert prepared_settings.dynamic_interval is True + assert prepared_settings.run_on_startup is True + assert prepared_settings.handle_retry_after is False + assert prepared_settings.preserve is True + assert prepared_settings.destroy_on_complete is False + + def test_prepare_method_without_args(self): + default_settings = DefaultMailerSettings(interval=5, dynamic_interval=True, preserve=True) + prepared_settings = default_settings.prepare() + assert prepared_settings.interval == 5 + assert prepared_settings.dynamic_interval is True + assert prepared_settings.run_on_startup is False + assert prepared_settings.handle_retry_after is False + assert prepared_settings.preserve is True + assert prepared_settings.destroy_on_complete is False + + @pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10 or higher.") + def test_dataclass_params(self): + dataclass_params = DefaultMailerSettings.__dataclass_params__ + assert dataclass_params.slots is True + assert dataclass_params.kw_only is True diff --git a/tests/test_mailer/test_task_manager.py b/tests/test_mailer/test_task_manager.py new file mode 100644 index 0000000..8bdb4e6 --- /dev/null +++ b/tests/test_mailer/test_task_manager.py @@ -0,0 +1,64 @@ +import asyncio +import unittest.mock + +import pytest + +from aiogram_broadcaster.mailer.task_manager import TaskManager + + +@pytest.fixture() +def task_manager(): + return TaskManager() + + +@pytest.fixture() +def task_coroutine(): + async def task_coroutine(): + pass + + return task_coroutine() + + +class TestTaskManager: + async def test_start(self, task_manager, task_coroutine): + assert not task_manager.started + task_manager.start(task_coroutine) + assert task_manager.started + assert not task_manager.waited + + async def test_start_already_started(self, task_manager, task_coroutine): + task_manager.start(task_coroutine) + with pytest.raises( + RuntimeError, + match="Task is already started.", + ): + task_manager.start(task_coroutine) + + async def test_wait(self, task_manager, task_coroutine): + task_manager.start(task_coroutine) + with unittest.mock.patch.object( + target=task_manager, + attribute="wait", + new_callable=unittest.mock.AsyncMock, + ) as mocked_wait: + await task_manager.wait() + mocked_wait.assert_awaited_once() + + async def test_wait_no_task(self, task_manager): + with pytest.raises(RuntimeError, match="No task for wait."): + await task_manager.wait() + + async def test_wait_already_waited(self, task_manager, task_coroutine): + task_manager.start(task_coroutine) + + with pytest.raises(RuntimeError, match="Task is already waited."): + await asyncio.gather(task_manager.wait(), task_manager.wait()) + + async def test_on_task_done(self, task_manager, task_coroutine): + task_manager.start(task_coroutine) + assert task_manager.started + assert not task_manager.waited + + task_manager._on_task_done(task_coroutine) + assert not task_manager.started + assert not task_manager.waited diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py deleted file mode 100644 index f09bb7b..0000000 --- a/tests/test_placeholder.py +++ /dev/null @@ -1,274 +0,0 @@ -import re -import unittest.mock -from string import Template - -import pydantic -import pytest - -from aiogram_broadcaster.placeholder import PlaceholderItem, PlaceholderManager, PlaceholderRouter -from aiogram_broadcaster.utils.interrupt import interrupt - - -@pytest.fixture() -def valid_model(): - return pydantic.create_model( - "ValidModel", - text=(str, "Hello, $name!"), - )() - - -@pytest.fixture() -def without_text_field_model(): - return pydantic.create_model( - "WithoutTextFieldModel", - foo=(str, "any text"), - )() - - -@pytest.fixture() -def invalid_type_text_field_model(): - return pydantic.create_model( - "InvalidTypeTextFieldModel", - caption=(int, 1111), - )() - - -class MyPlaceholderItem(PlaceholderItem, key="test_key"): - async def __call__(self): - return "test_value" - - -@pytest.fixture() -def placeholder_item(): - return MyPlaceholderItem() - - -class TestPlaceholderItem: - def test_class_attrs(self): - assert MyPlaceholderItem.key == "test_key" - - def test_repr(self, placeholder_item): - assert repr(placeholder_item) == "MyPlaceholderItem(key='test_key')" - - def test_subclass_without_key(self): - with pytest.raises( - ValueError, - match="Missing required argument 'key' when subclassing PlaceholderItem.", - ): - - class InvalidPlaceholderItem(PlaceholderItem): - async def __call__(self): - return - - -class TestPlaceholderRouter: - def test_add(self): - router = PlaceholderRouter() - router.add(key="test_key", value="test_value") - router["test_key_2"] = "test_value_2" - - assert "test_key" in router - assert "test_key_2" in router - assert router["test_key"] == "test_value" - assert router["test_key_2"] == "test_value_2" - assert dict(router) == {"test_key": "test_value", "test_key_2": "test_value_2"} - - assert "test_key" in router.items - assert "test_key_2" in router.items - assert router.items["test_key"] == "test_value" - assert router.items["test_key_2"] == "test_value_2" - assert router.items == {"test_key": "test_value", "test_key_2": "test_value_2"} - - assert tuple(router.chain_keys) == ("test_key", "test_key_2") - assert tuple(router.chain_items) == ( - ("test_key", "test_value"), - ("test_key_2", "test_value_2"), - ) - - def test_call(self): - router = PlaceholderRouter() - - @router(key="test_key") - async def decorated_callback(): - return - - assert router["test_key"].callback == decorated_callback - - def test_add_already_exists_key(self): - router = PlaceholderRouter() - router["test_key"] = "test_value" - - with pytest.raises( - ValueError, - match="Key 'test_key' is already exists.", - ): - router.add(key="test_key", value="test_value") - - def test_fluent_add(self): - router = PlaceholderRouter() - assert router.add(key="test", value="test") == router - - def test_register(self, placeholder_item): - router = PlaceholderRouter() - router.register(placeholder_item) - assert "test_key" in router - assert router["test_key"].callback == placeholder_item.__call__ - assert router.items["test_key"].callback == placeholder_item.__call__ - - def test_register_without_args(self): - router = PlaceholderRouter() - with pytest.raises( - ValueError, - match="At least one placeholder item must be provided to register.", - ): - router.register() - - def test_register_invalid_type(self): - router = PlaceholderRouter() - with pytest.raises( - TypeError, - match="The placeholder item must be an instance of PlaceholderItem, not a str.", - ): - router.register("invalid type") - - def test_register_fluent(self, placeholder_item): - router = PlaceholderRouter() - assert router.register(placeholder_item) == router - - def test_attach(self): - router = PlaceholderRouter() - router.attach({"test_key": "test_value"}, test_key_2="test_value_2") - assert router.items == {"test_key": "test_value", "test_key_2": "test_value_2"} - - def test_attach_without_args(self): - router = PlaceholderRouter() - with pytest.raises( - ValueError, - match="At least one keyword argument must be specified to attaching.", - ): - router.attach() - - def test_fluent_attach(self): - router = PlaceholderRouter() - assert router.attach(key="value") == router - - def test_include(self): - router1 = PlaceholderRouter() - router2 = PlaceholderRouter() - router1["test_key"] = "test_value" - router2["test_key_2"] = "test_value_2" - router1.include(router2) - assert router1.items == {"test_key": "test_value"} - assert router2.items == {"test_key_2": "test_value_2"} - assert dict(router1) == {"test_key": "test_value", "test_key_2": "test_value_2"} - - def test_include_collusion(self): - router1 = PlaceholderRouter(name="placeholder1") - router2 = PlaceholderRouter(name="placeholder2") - router1["test_key"] = "test_value" - router2["test_key"] = "test_value_2" - with pytest.raises( - ValueError, - match=re.escape( - "The PlaceholderRouter(name='placeholder1') already has the keys: ['test_key'].", - ), - ): - router1.include(router2) - - -class TestPlaceholderManager: - async def test_fetch_data(self, placeholder_item): - manager = PlaceholderManager() - manager.register(placeholder_item) - manager["test_key_2"] = "test_value_2" - assert await manager.fetch_data({"test_key", "test_key_2"}) == { - "test_key": "test_value", - "test_key_2": "test_value_2", - } - assert await manager.fetch_data({}) == {} - assert await manager.fetch_data({"not_exists_key"}) == {} - - async def test_fetch_data_calls_callback(self, placeholder_item): - manager = PlaceholderManager() - - @manager(key="test_key") - async def callback(**kwargs): - return "test_value" - - with unittest.mock.patch.object( - target=manager["test_key"], - attribute="call", - new_callable=unittest.mock.AsyncMock, - ) as mocked_callback: - await manager.fetch_data({"test_key"}, test_param=1) - mocked_callback.assert_called_once_with(test_param=1) - - def test_extract_text_field( - self, - valid_model, - without_text_field_model, - invalid_type_text_field_model, - ): - manager = PlaceholderManager() - assert manager.extract_text_field(model=valid_model) == ("text", "Hello, $name!") - assert manager.extract_text_field(model=without_text_field_model) is None - assert manager.extract_text_field(model=invalid_type_text_field_model) is None - - def test_extract_keys(self): - manager = PlaceholderManager() - assert manager.extract_keys(template=Template("Hello, $name!")) == {"name"} - assert manager.extract_keys(template=Template("$mention $age")) == {"mention", "age"} - assert manager.extract_keys(template=Template("text without vars")) == set() - - async def test_render_with_placeholders(self, valid_model): - manager = PlaceholderManager() - manager["name"] = "John" - rendered_model = await manager.render(valid_model) - assert rendered_model.text == "Hello, John!" - - async def test_render_without_placeholders(self, valid_model): - manager = PlaceholderManager() - rendered_model = await manager.render(valid_model) - assert rendered_model.text == "Hello, $name!" - - async def test_render_with_excluded_keys(self, valid_model): - manager = PlaceholderManager() - manager["name"] = "John" - rendered_model = await manager.render(valid_model, {"name"}) - assert rendered_model.text == "Hello, $name!" - - async def test_render_with_no_keys(self, valid_model): - manager = PlaceholderManager() - manager["unused_key"] = "Some value" - rendered_model = await manager.render(valid_model) - assert rendered_model.text == "Hello, $name!" - - async def test_render_with_empty_exclude_keys(self, valid_model): - manager = PlaceholderManager() - manager["name"] = "John" - rendered_model = await manager.render(valid_model) - assert rendered_model.text == "Hello, John!" - - async def test_render_with_non_matching_keys(self, valid_model): - manager = PlaceholderManager() - manager["non_matching_key"] = "Some value" - rendered_model = await manager.render(valid_model) - assert rendered_model.text == "Hello, $name!" - - async def test_render_without_text_field(self, without_text_field_model): - manager = PlaceholderManager() - manager["name"] = "John" - rendered_model = await manager.render(without_text_field_model) - assert rendered_model == without_text_field_model - - async def test_render_with_invalid_type_text_field(self, invalid_type_text_field_model): - manager = PlaceholderManager() - manager["name"] = "John" - rendered_model = await manager.render(invalid_type_text_field_model) - assert rendered_model == invalid_type_text_field_model - - async def test_render_without_data(self, valid_model): - manager = PlaceholderManager() - manager["name"] = interrupt - rendered_model = await manager.render(valid_model) - assert rendered_model == valid_model diff --git a/tests/test_placeholder/__init__.py b/tests/test_placeholder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_placeholder/test_item.py b/tests/test_placeholder/test_item.py new file mode 100644 index 0000000..d5ac3ce --- /dev/null +++ b/tests/test_placeholder/test_item.py @@ -0,0 +1,23 @@ +from aiogram_broadcaster.placeholder.item import PlaceholderItem +from aiogram_broadcaster.placeholder.placeholder import Placeholder + + +class DummyPlaceholderItem(PlaceholderItem, key="test_key"): + async def __call__(self, **kwargs): + return "test_value" + + +class TestPlaceholderItem: + def test_key_assignment(self): + assert DummyPlaceholderItem.key == "test_key" + + def test_repr(self): + item = DummyPlaceholderItem() + assert repr(item) == "DummyPlaceholderItem(key='test_key')" + + def test_as_placeholder(self): + item = DummyPlaceholderItem() + placeholder = item.as_placeholder() + assert isinstance(placeholder, Placeholder) + assert placeholder.key == "test_key" + assert placeholder.value == item.__call__ diff --git a/tests/test_placeholder/test_manager.py b/tests/test_placeholder/test_manager.py new file mode 100644 index 0000000..e52c9e8 --- /dev/null +++ b/tests/test_placeholder/test_manager.py @@ -0,0 +1,115 @@ +import unittest.mock +from string import Template + +import pydantic + +from aiogram_broadcaster.placeholder.manager import PlaceholderManager +from aiogram_broadcaster.utils.interrupt import Interrupt + + +class TestPlaceholderManager: + async def test_fetch_data_no_select_keys(self): + manager = PlaceholderManager() + data = await manager.fetch_data(set()) + assert data == {} + + async def test_fetch_data_with_select_keys(self): + manager = PlaceholderManager() + manager["key1"] = unittest.mock.Mock(return_value="value1") + manager["key2"] = unittest.mock.Mock(return_value="value2") + data = await manager.fetch_data({"key1", "key2"}) + assert data == {"key1": "value1", "key2": "value2"} + + async def test_fetch_data_missing_keys(self): + manager = PlaceholderManager() + manager["key1"] = unittest.mock.Mock(return_value="value1") + data = await manager.fetch_data({"key2"}) + assert data == {} + + async def test_fetch_data_interrupt(self): + manager = PlaceholderManager() + manager["key1"] = unittest.mock.Mock(side_effect=Interrupt) + data = await manager.fetch_data({"key1"}) + assert data == {} + + def test_extract_text_field_found(self): + manager = PlaceholderManager() + model = pydantic.create_model("TestModel", caption=(str, "Hello, $name!")) + field = manager.extract_text_field(model()) + assert field == ("caption", "Hello, $name!") + + def test_extract_text_field_not_found(self): + manager = PlaceholderManager() + model = pydantic.create_model("TestModel", description=(str, "Some description")) + field = manager.extract_text_field(model()) + assert field is None + + def test_extract_keys_with_placeholders(self): + manager = PlaceholderManager() + template = manager.extract_keys(Template("Hello, $name!")) + assert template == {"name"} + + def test_extract_keys_no_placeholders(self): + manager = PlaceholderManager() + template = manager.extract_keys(Template("Hello, world!")) + assert template == set() + + async def test_render_no_keys(self): + manager = PlaceholderManager() + model = pydantic.create_model("TestModel", caption=(str, "Hello, $name!")) + rendered_model = await manager.render(model()) + assert rendered_model == model() + + async def test_render_all_keys_excluded(self): + manager = PlaceholderManager() + model = pydantic.create_model("TestModel", caption=(str, "Hello, $name!")) + rendered_model = await manager.render(model(), {"name"}) + assert rendered_model == model() + + async def test_render_no_placeholders_found(self): + manager = PlaceholderManager() + model = pydantic.create_model("TestModel", description=(str, "Some description")) + rendered_model = await manager.render(model()) + assert rendered_model == model() + + async def test_render_placeholders_and_data_fetched(self): + manager = PlaceholderManager() + manager["name"] = unittest.mock.Mock(return_value="John") + model = pydantic.create_model("TestModel", caption=(str, "Hello, $name!")) + rendered_model = await manager.render(model()) + assert rendered_model == model(caption="Hello, John!") + + async def test_render_placeholders_but_no_data_fetched(self): + manager = PlaceholderManager() + model = pydantic.create_model("TestModel", caption=(str, "Hello, $name!")) + rendered_model = await manager.render(model()) + assert rendered_model == model() + + async def test_render_interrupt(self): + manager = PlaceholderManager() + manager["name"] = unittest.mock.Mock(side_effect=Interrupt) + model = pydantic.create_model("TestModel", caption=(str, "Hello, $name!")) + rendered_model = await manager.render(model()) + assert rendered_model == model() + + async def test_render_empty_keys(self): + manager = PlaceholderManager() + manager["name"] = "test_value" + model = pydantic.create_model("TestModel", caption=(str, "Hello, $name!")) + rendered_model = await manager.render(model(), {"name"}) + assert rendered_model == model() + + async def test_render_without_field(self): + manager = PlaceholderManager() + manager["name"] = "test_value" + model = pydantic.create_model("TestModel", not_text_field=(str, "value")) + rendered_model = await manager.render(model()) + assert rendered_model == model() + + async def test_render_without_select_keys(self): + manager = PlaceholderManager() + manager["name"] = "test_value" + manager["age"] = "test_value" + model = pydantic.create_model("TestModel", text=(str, "Hello, $name!")) + rendered_model = await manager.render(model(), {"name"}) + assert rendered_model == model() diff --git a/tests/test_placeholder/test_placeholder.py b/tests/test_placeholder/test_placeholder.py new file mode 100644 index 0000000..2680a2e --- /dev/null +++ b/tests/test_placeholder/test_placeholder.py @@ -0,0 +1,43 @@ +from aiogram_broadcaster.placeholder.placeholder import Placeholder + + +class TestPlaceholder: + def test_initialization_with_value(self): + value = "test_value" + placeholder = Placeholder(key="test_key", value=value) + assert placeholder.key == "test_key" + assert placeholder.value == value + + def test_initialization_with_callable_value(self): + async def mock_callback(**kwargs): + pass + + placeholder = Placeholder(key="test_key", value=mock_callback) + assert placeholder.key == "test_key" + assert placeholder.value == mock_callback + + def test_hash(self): + placeholder1 = Placeholder(key="key1", value="value1") + placeholder2 = Placeholder(key="key2", value="value2") + assert hash(placeholder1) != hash(placeholder2) + + def test_eq(self): + placeholder1 = Placeholder(key="key1", value="value1") + placeholder2 = Placeholder(key="key1", value="value2") + placeholder3 = Placeholder(key="key2", value="value1") + assert placeholder1 == placeholder2 + assert placeholder1 != placeholder3 + assert placeholder1 != "invalid_type" + + async def test_get_value_with_value(self): + value = "test_value" + placeholder = Placeholder(key="test_key", value=value) + assert await placeholder.get_value() == value + + async def test_get_value_with_callable(self): + async def mock_callback(**kwargs): + return "callback_result" + + placeholder = Placeholder(key="test_key", value=mock_callback) + result = await placeholder.get_value() + assert result == "callback_result" diff --git a/tests/test_placeholder/test_registry.py b/tests/test_placeholder/test_registry.py new file mode 100644 index 0000000..98024fa --- /dev/null +++ b/tests/test_placeholder/test_registry.py @@ -0,0 +1,139 @@ +import re + +import pytest + +from aiogram_broadcaster.placeholder.item import PlaceholderItem +from aiogram_broadcaster.placeholder.placeholder import Placeholder +from aiogram_broadcaster.placeholder.registry import PlaceholderRegistry + + +class DummyPlaceholderItem(PlaceholderItem, key="test_key"): + async def __call__(self, **kwargs): + return "test_value" + + +class TestPlaceholderRegistry: + def test_initialization(self): + registry = PlaceholderRegistry(name="test_registry") + assert registry.name == "test_registry" + assert registry.placeholders == set() + + def test_add_placeholder(self): + registry = PlaceholderRegistry() + registry["key1"] = "value1" + assert registry.placeholders == {Placeholder(key="key1", value="value1")} + + def test_register_placeholder_items(self): + registry = PlaceholderRegistry() + item1 = DummyPlaceholderItem() + item2 = DummyPlaceholderItem() + registry.register(item1, item2) + assert registry.placeholders == {item1.as_placeholder(), item2.as_placeholder()} + registry = PlaceholderRegistry() + with pytest.raises(TypeError): + registry.register("invalid_type") + assert registry.register(item2) == registry + with pytest.raises( + ValueError, + match="At least one placeholder item must be provided to register.", + ): + registry.register() + + def test_add_method(self): + registry = PlaceholderRegistry() + registry.add(key1="value1", key2="value2") + assert registry.placeholders == { + Placeholder(key="key1", value="value1"), + Placeholder(key="key2", value="value2"), + } + assert registry.add({"key1": "value1"}) == registry + with pytest.raises(ValueError, match="At least one argument must be provided."): + registry.add() + + def test_items_property(self): + registry = PlaceholderRegistry() + registry["key1"] = "value1" + registry["key2"] = "value2" + assert sorted(registry.items) == [("key1", "value1"), ("key2", "value2")] + + def test_keys_property(self): + registry = PlaceholderRegistry() + registry["key1"] = "value1" + registry["key2"] = "value2" + assert registry.keys == {"key1", "key2"} + + def test_chain_placeholders_property(self): + registry1 = PlaceholderRegistry() + registry2 = PlaceholderRegistry() + registry1["key1"] = "value1" + registry2["key2"] = "value2" + registry1.bind(registry2) + assert set(registry1.chain_placeholders) == { + Placeholder(key="key1", value="value1"), + Placeholder(key="key2", value="value2"), + } + + def test_chain_items_property(self): + registry1 = PlaceholderRegistry() + registry2 = PlaceholderRegistry() + registry1["key1"] = "value1" + registry2["key2"] = "value2" + registry1.bind(registry2) + assert set(registry1.chain_items) == {("key1", "value1"), ("key2", "value2")} + + def test_chain_keys_property(self): + registry1 = PlaceholderRegistry() + registry2 = PlaceholderRegistry() + registry1["key1"] = "value1" + registry2["key2"] = "value2" + registry1.bind(registry2) + assert set(registry1.chain_keys) == {"key1", "key2"} + + def test_iter(self): + registry = PlaceholderRegistry() + registry["key1"] = "value1" + registry["key2"] = "value2" + assert sorted(registry) == [("key1", "value1"), ("key2", "value2")] + + def test_contains(self): + registry = PlaceholderRegistry() + registry["key1"] = "value1" + assert "key1" in registry + assert "key2" not in registry + + def test_chain_bind_no_collision(self): + registry1 = PlaceholderRegistry(name="registry1") + registry2 = PlaceholderRegistry(name="registry2") + registry1["key1"] = "value1" + registry2["key2"] = "value2" + + registry1.bind(registry2) + assert set(registry1.chain_placeholders) == { + Placeholder(key="key1", value="value1"), + Placeholder(key="key2", value="value2"), + } + + def test_chain_bind_with_collision(self): + registry1 = PlaceholderRegistry(name="registry1") + registry2 = PlaceholderRegistry(name="registry2") + registry1["key1"] = "value1" + registry2["key1"] = "value2" + + with pytest.raises( + RuntimeError, + match=re.escape( + "Collision keys=['key1'] between PlaceholderRegistry(name='registry1') " + "and PlaceholderRegistry(name='registry2').", + ), + ): + registry1.bind(registry2) + + def test_call(self): + registry = PlaceholderRegistry() + + @registry(key="test_key") + async def callback(): + return + + assert registry.placeholders == {Placeholder(key="test_key", value=callback)} + assert registry["test_key"] == callback diff --git a/tests/test_utils/test_chain.py b/tests/test_utils/test_chain.py index 260ddce..331d072 100644 --- a/tests/test_utils/test_chain.py +++ b/tests/test_utils/test_chain.py @@ -2,43 +2,43 @@ import pytest -from aiogram_broadcaster.utils.chain import ChainObject +from aiogram_broadcaster.utils.chain import Chain -class MyChainObject(ChainObject["MyChainObject"], sub_name="chain"): +class MyChain(Chain["MyChain"], sub_name="chain"): pass -class MyRootChainObject(MyChainObject): +class MyRootChain(MyChain): __chain_root__ = True class TestChainObject: def test_class_attrs(self): - assert MyChainObject.__chain_entity__ is MyChainObject - assert MyChainObject.__chain_sub_name__ == "chain" - assert MyChainObject.__chain_root__ is False + assert MyChain.__chain_entity__ is MyChain + assert MyChain.__chain_sub_name__ == "chain" + assert MyChain.__chain_root__ is False - assert MyRootChainObject.__chain_entity__ is MyChainObject - assert MyRootChainObject.__chain_sub_name__ == "chain" - assert MyRootChainObject.__chain_root__ is True + assert MyRootChain.__chain_entity__ is MyChain + assert MyRootChain.__chain_sub_name__ == "chain" + assert MyRootChain.__chain_root__ is True def test_init(self): - chain = MyChainObject(name="test_name") + chain = MyChain(name="test_name") assert chain.name == "test_name" assert chain.head is None assert chain.tail == [] def test_name(self): - chain = MyChainObject() + chain = MyChain() assert id(chain) == int(chain.name, 16) - def test_chain_include(self): - chain1 = MyChainObject() - chain2 = MyChainObject() - chain3 = MyChainObject() - chain1.include(chain2) - chain2.include(chain3) + def test_chain_bind(self): + chain1 = MyChain() + chain2 = MyChain() + chain3 = MyChain() + chain1.bind(chain2) + chain2.bind(chain3) assert chain1.head is None assert chain1.tail == [chain2] @@ -55,11 +55,11 @@ def test_chain_include(self): assert tuple(chain3.chain_head) == (chain3, chain2, chain1) assert tuple(chain3.chain_tail) == (chain3,) - def test_parental_include(self): - parent = MyChainObject() - child1 = MyChainObject() - child2 = MyChainObject() - parent.include(child1, child2) + def test_parental_bind(self): + parent = MyChain() + child1 = MyChain() + child2 = MyChain() + parent.bind(child1, child2) assert parent.head is None assert parent.tail == [child1, child2] @@ -77,81 +77,80 @@ def test_parental_include(self): assert tuple(child2.chain_tail) == (child2,) def test_repr_and_str(self) -> None: - parent = MyChainObject(name="parent") - child = MyChainObject(name="child") - parent.include(child) + parent = MyChain(name="parent") + child = MyChain(name="child") + parent.bind(child) - assert repr(parent) == "MyChainObject(name='parent', nested=[MyChainObject(name='child')])" - assert repr(child) == "MyChainObject(name='child')" + assert repr(parent) == "MyChain(name='parent', nested=[MyChain(name='child')])" + assert repr(child) == "MyChain(name='child')" - assert str(parent) == "MyChainObject(name='parent')" - assert str(child) == "MyChainObject(name='child', parent=MyChainObject(name='parent'))" + assert str(parent) == "MyChain(name='parent')" + assert str(child) == "MyChain(name='child', parent=MyChain(name='parent'))" - def test_valid_fluent_include(self): - chain1 = MyChainObject() - chain2 = MyChainObject() - assert chain1.include(chain2) == chain2 + def test_one_fluent_bind(self): + chain1 = MyChain() + chain2 = MyChain() + assert chain1.bind(chain2) == chain2 - def test_invalid_fluent_include(self): - chain1 = MyChainObject() - chain2 = MyChainObject() - chain3 = MyChainObject() - assert chain1.include(chain2, chain3) is None + def test_many_fluent_bind(self): + chain1 = MyChain() + chain2 = MyChain() + chain3 = MyChain() + assert chain1.bind(chain2, chain3) == chain1 - def test_include_without_args(self): - chain = MyChainObject() + def test_bind_without_args(self): + chain = MyChain() with pytest.raises( ValueError, - match="At least one chain must be provided to include.", + match="At least one chain must be provided to bind.", ): - chain.include() + chain.bind() - def test_include_invalid_type(self): - chain = MyChainObject() + def test_bind_invalid_type(self): + chain = MyChain() with pytest.raises( TypeError, - match="The chain must be an instance of MyChainObject, not a str.", + match="The chain must be an instance of MyChain, not a str.", ): - chain.include("invalid type") + chain.bind("invalid type") - def test_include_itself(self): - chain = MyChainObject() + def test_bind_itself(self): + chain = MyChain() with pytest.raises( ValueError, - match="Cannot include the chain on itself.", + match="Cannot bind the chain on itself.", ): - chain.include(chain) + chain.bind(chain) - def test_include_already_included(self): - chain1 = MyChainObject(name="chain1") - chain2 = MyChainObject(name="chain2") - chain1.include(chain2) + def test_bind_already_bind(self): + chain1 = MyChain(name="chain1") + chain2 = MyChain(name="chain2") + chain1.bind(chain2) with pytest.raises( RuntimeError, match=re.escape( - "The MyChainObject(name='chain2') is already " - "attached to MyChainObject(name='chain1').", + "The MyChain(name='chain2') is already attached to MyChain(name='chain1').", ), ): - chain1.include(chain2) + chain1.bind(chain2) - def test_circular_include(self): - chain1 = MyChainObject() - chain2 = MyChainObject() - chain1.include(chain2) + def test_circular_bind(self): + chain1 = MyChain() + chain2 = MyChain() + chain1.bind(chain2) with pytest.raises( RuntimeError, match="Circular referencing detected.", ): - chain2.include(chain1) + chain2.bind(chain1) - def test_include_root(self): - root_chain = MyRootChainObject(name="root_chain") - chain = MyChainObject(name="chain") + def test_bind_root(self): + root_chain = MyRootChain(name="root_chain") + chain = MyChain(name="chain") with pytest.raises( RuntimeError, match=re.escape( - "MyRootChainObject(name='root_chain') cannot be attached to another chain.", + "MyRootChain(name='root_chain') cannot be attached to another chain.", ), ): - chain.include(root_chain) + chain.bind(root_chain)