From 6af246ea5cbd34944ef260ed1120cc00bc330428 Mon Sep 17 00:00:00 2001 From: volta_cabral Date: Tue, 19 Mar 2024 22:01:56 -0300 Subject: [PATCH] add tests --- botgen/core.py | 4 +- poetry.lock | 180 +++++++++++- pyproject.toml | 3 + tests/__init__.py | 0 tests/botgen/__init__.py | 0 tests/botgen/adapters/__init__.py | 0 tests/botgen/adapters/web_adapter_test.py | 47 ++++ tests/botgen/bot_worker_test.py | 70 +++++ tests/botgen/conversation_state_test.py | 42 +++ tests/botgen/core_test.py | 319 ++++++++++++++++++++++ 10 files changed, 661 insertions(+), 4 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/botgen/__init__.py create mode 100644 tests/botgen/adapters/__init__.py create mode 100644 tests/botgen/adapters/web_adapter_test.py create mode 100644 tests/botgen/bot_worker_test.py create mode 100644 tests/botgen/conversation_state_test.py create mode 100644 tests/botgen/core_test.py diff --git a/botgen/core.py b/botgen/core.py index e8b4890..fb4a754 100644 --- a/botgen/core.py +++ b/botgen/core.py @@ -217,12 +217,12 @@ def hears(self, pattern: str | list, handler: Callable, event: str = "message") self._triggers[event].append(bot_trigger) - async def on(self, event: str, handler: Callable): + def on(self, event: str, handler: Callable): if not event in self._events: self._events[event] = [] - self._events[event] = handler + self._events[event].append(handler) async def spawn( self, config: TurnContext | DialogContext = None, custom_adapter: BotAdapter = None diff --git a/poetry.lock b/poetry.lock index 72e2cfd..d7fd973 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -477,6 +477,73 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.4.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "cryptography" version = "42.0.2" @@ -566,6 +633,20 @@ files = [ [package.extras] dev = ["coverage", "coveralls", "pytest"] +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "filelock" version = "3.13.1" @@ -723,6 +804,17 @@ files = [ {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "isodate" version = "0.6.1" @@ -1161,6 +1253,21 @@ files = [ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pre-commit" version = "3.6.0" @@ -1243,6 +1350,64 @@ pyyaml = "*" [package.extras] extra = ["pygments (>=2.12)"] +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.6" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, + {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1590,6 +1755,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + [[package]] name = "typing-extensions" version = "4.9.0" @@ -1796,4 +1972,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3fb34509e6e154935af4797f53105ddae6a1713d390aaab1652cffae043be11d" +content-hash = "34bb769b5ff6797376a30d4a091ad2c41c0f475bfd826226e5cc339febc6c12d" diff --git a/pyproject.toml b/pyproject.toml index 4d6a95b..4ce13bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,9 @@ loguru = "^0.7.2" pre-commit = "^3.6.0" mkdocs = "^1.5.3" mkdocs-material = "==8.*" +pytest = "^8.1.1" +pytest-cov = "^4.1.0" +pytest-asyncio = "^0.23.6" [build-system] requires = ["poetry-core"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/botgen/__init__.py b/tests/botgen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/botgen/adapters/__init__.py b/tests/botgen/adapters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/botgen/adapters/web_adapter_test.py b/tests/botgen/adapters/web_adapter_test.py new file mode 100644 index 0000000..1cebc01 --- /dev/null +++ b/tests/botgen/adapters/web_adapter_test.py @@ -0,0 +1,47 @@ +import pytest +from unittest.mock import AsyncMock, Mock +from botgen.adapters import WebAdapter +from botbuilder.schema import Activity, ConversationReference, ResourceResponse +from botbuilder.core import TurnContext + +@pytest.fixture +def web_adapter(): + return WebAdapter() + +def test_activity_to_message(web_adapter): + activity = Activity(type="message", text="Test message") + message = web_adapter.activity_to_message(activity) + assert message.type == activity.type + assert message.text == activity.text + +@pytest.mark.asyncio +async def test_send_activities_webhook(web_adapter): + context = Mock() + context.activity.channel_id = "websocket" + context.turn_state.get.return_value = None + activities = [Activity(type="message", text="Test message")] + + with pytest.raises(NotImplementedError): + await web_adapter.send_activities(context, activities) + +@pytest.mark.asyncio +async def test_update_activity(web_adapter): + context = Mock() + activity = Mock() + with pytest.raises(NotImplementedError): + await web_adapter.update_activity(context, activity) + +@pytest.mark.asyncio +async def test_delete_activity(web_adapter): + context = Mock() + reference = Mock() + with pytest.raises(NotImplementedError): + await web_adapter.delete_activity(context, reference) + +@pytest.mark.asyncio +async def test_process_activity(web_adapter): + request = Mock() + request.json = AsyncMock(return_value={"type": "message", "text": "Test message", "user": "user_id"}) + logic_callback = AsyncMock() + response = await web_adapter.process_activity(request, logic_callback) + assert response == None diff --git a/tests/botgen/bot_worker_test.py b/tests/botgen/bot_worker_test.py new file mode 100644 index 0000000..839d400 --- /dev/null +++ b/tests/botgen/bot_worker_test.py @@ -0,0 +1,70 @@ +import pytest +from unittest.mock import Mock, AsyncMock +from botgen.bot_worker import BotWorker +from botbuilder.schema import Activity +from botbuilder.core import TurnContext + +@pytest.fixture +def bot_worker(): + # Mock Bot and config + bot_mock = Mock() + config = {"context": Mock()} + return BotWorker(bot_mock, config) + +def test_get_controller(bot_worker): + controller = bot_worker.get_controller() + assert controller == bot_worker._controller + +def test_get_config(bot_worker): + config = bot_worker.get_config() + assert config == bot_worker._config + +@pytest.mark.asyncio +async def test_say(bot_worker): + # Mock the context's send_activity method + bot_worker._config["context"].send_activity = AsyncMock() + + message = "Test message" + await bot_worker.say(message) + + bot_worker._config["context"].send_activity.assert_called_once() + +@pytest.mark.asyncio +async def test_reply(bot_worker): + # Mock methods and objects needed for reply + activity = Activity(type="message", text="Reply message", channel_data={}) + bot_worker.ensure_message_format = AsyncMock(return_value=activity) + TurnContext.get_conversation_reference = Mock(return_value={"conversation": {"id": "123"}}) + TurnContext.apply_conversation_reference = Mock(return_value=activity) + bot_worker.say = AsyncMock() + + message_src = Mock() + message_src.incoming_message = Mock() + message_resp = "Replying to the message" + + await bot_worker.reply(message_src, message_resp) + + bot_worker.ensure_message_format.assert_called_once_with(message=message_resp) + TurnContext.get_conversation_reference.assert_called_once_with(message_src.incoming_message) + TurnContext.apply_conversation_reference.assert_called_once_with(activity, {"conversation": {"id": "123"}}) + bot_worker.say.assert_called_once_with(activity) + +@pytest.mark.asyncio +async def test_ensure_message_format_string(bot_worker): + message = "Test message" + activity = await bot_worker.ensure_message_format(message) + assert isinstance(activity, Activity) + assert activity.type == "message" + assert activity.text == message + +@pytest.mark.asyncio +async def test_ensure_message_format_message_object(bot_worker): + message = Mock() + message.__dict__ = {"type": "test", "text": "Test message"} + activity = await bot_worker.ensure_message_format(message) + assert isinstance(activity, Activity) + assert activity.type == message.type + assert activity.text == message.text + +if __name__ == '__main__': + pytest.main() diff --git a/tests/botgen/conversation_state_test.py b/tests/botgen/conversation_state_test.py new file mode 100644 index 0000000..aab3cff --- /dev/null +++ b/tests/botgen/conversation_state_test.py @@ -0,0 +1,42 @@ +import pytest +from botgen.conversation_state import BotConversationState +from botbuilder.core import MemoryStorage +from unittest.mock import Mock + +@pytest.fixture +def conversation_state(): + return BotConversationState(MemoryStorage()) + +def test_get_storage_key(conversation_state): + # Mocking the necessary attributes of TurnContext + activity_mock = Mock() + activity_mock.channel_id = "test_channel" + activity_mock.conversation = {"id": "test_conversation_id"} + turn_context_mock = Mock() + turn_context_mock.activity = activity_mock + + expected_key = "test_channel/conversations/test_conversation_id" + actual_key = conversation_state.get_storage_key(turn_context_mock) + assert actual_key == expected_key + +def test_get_storage_key_missing_conversation(conversation_state): + # Mocking the necessary attributes of TurnContext + activity_mock = Mock() + activity_mock.channel_id = "test_channel" + activity_mock.conversation = None # Simulating missing conversation + turn_context_mock = Mock() + turn_context_mock.activity = activity_mock + + with pytest.raises(Exception): + conversation_state.get_storage_key(turn_context_mock) + +def test_get_storage_key_missing_conversation_id(conversation_state): + # Mocking the necessary attributes of TurnContext + activity_mock = Mock() + activity_mock.channel_id = "test_channel" + activity_mock.conversation = {"id": None} # Simulating missing conversation id + turn_context_mock = Mock() + turn_context_mock.activity = activity_mock + + with pytest.raises(Exception): + conversation_state.get_storage_key(turn_context_mock) diff --git a/tests/botgen/core_test.py b/tests/botgen/core_test.py new file mode 100644 index 0000000..9e1bcf9 --- /dev/null +++ b/tests/botgen/core_test.py @@ -0,0 +1,319 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, Mock +from botgen import Bot +from botbuilder.core import MemoryStorage +from botbuilder.dialogs import DialogSet +from aiohttp import web +from botbuilder.core import TurnContext +from botgen.bot_worker import BotWorker +from botgen.core import BotMessage +from botbuilder.dialogs import DialogContext +from botbuilder.core import BotAdapter + + +@pytest.fixture +def bot(): + return Bot() + +@pytest.fixture +def mock_request(): + return Mock() + +@pytest.fixture +def mock_turn_context(): + return MagicMock(spec=TurnContext) + +@pytest.fixture +def mock_bot_worker(): + return Mock(spec=BotWorker) + +@pytest.fixture +def mock_bot_message(): + return Mock(spec=BotMessage) + +@pytest.fixture +def mock_trigger(): + return Mock() + +@pytest.fixture +def mock_dialog_context(): + return Mock(spec=DialogContext) + +@pytest.fixture +def mock_bot_adapter(): + return Mock(spec=BotAdapter) + +def test_bot_initialization(bot): + assert bot.webhook_uri == "/api/messages" + assert bot.dialog_state_property == "dialogState" + assert bot.adapter is None + assert bot.adapter_config is None + assert bot.webserver is not None + assert bot.webserver_middlewares is None + assert bot.storage is None + assert bot.disable_webserver is None + assert bot.disable_console is None + assert bot.json_limit == "100kb" + assert bot.url_encoded_limit == "100kb" + assert bot.booted is False + assert isinstance(bot._storage, MemoryStorage) + assert isinstance(bot.dialog_set, DialogSet) + +def test_configure_webhook(bot): + bot.webserver = Mock() + bot.configure_webhook() + +@pytest.mark.asyncio +async def test_process_incoming_message(bot, mock_request): + bot.adapter = Mock() + bot.handle_turn = Mock() + bot.adapter.process_activity = AsyncMock(return_value={"response": "message"}) + + response = await bot.process_incoming_message(mock_request) + + bot.adapter.process_activity.assert_called_once_with(mock_request, bot.handle_turn) + assert response.status == 200 + +def test_configure_webhook(bot): + bot.webserver = Mock() + bot.webserver.add_routes = Mock() + + bot.configure_webhook() + + bot.webserver.add_routes.assert_called_once_with([web.post(bot.webhook_uri, bot.process_incoming_message)]) + + +@pytest.mark.asyncio +async def test_handle_turn(bot, mock_turn_context): + # Set up the mocked TurnContext + activity = Mock() + activity.type = "message" + activity.from_property = {"id": "user_id"} + activity.text = "Test message" + activity.conversation = {"id": "channel_id"} + activity.value = "message_value" + TurnContext.get_conversation_reference = Mock(return_value={"reference": "conversation_reference"}) + mock_turn_context.activity = activity + + # Set up expectations for BotMessage creation + expected_bot_message = BotMessage( + type="message", + user="user_id", + text="Test message", + channel="channel_id", + value="message_value", + reference={"reference": "conversation_reference"}, + incoming_message=activity + ) + + # Mock the dialog set and create_context method + dialog_context_mock = Mock() + bot.dialog_set.create_context = AsyncMock(return_value=dialog_context_mock) + + # Mock the spawn method + bot.spawn = AsyncMock() + + # Mock _process_trigger_and_events method + bot._process_trigger_and_events = AsyncMock() + + # Call the handle_turn method + await bot.handle_turn(mock_turn_context) + + # Assertions + bot.dialog_set.create_context.assert_called_once_with(turn_context=mock_turn_context) + bot.spawn.assert_called_once_with(dialog_context_mock) + bot._process_trigger_and_events.assert_called_once_with(bot_worker=bot.spawn.return_value, message=expected_bot_message) + + +@pytest.mark.asyncio +async def test_process_trigger_and_events_with_listen_results(bot, mock_bot_worker, mock_bot_message): + # Mock _listen_for_triggers method to return some results + bot._listen_for_triggers = AsyncMock(return_value=["listen_result"]) + + # Mock trigger method + bot.trigger = AsyncMock() + + # Call the _process_trigger_and_events method + result = await bot._process_trigger_and_events(bot_worker=mock_bot_worker, message=mock_bot_message) + + # Assertions + assert result == ["listen_result"] + bot._listen_for_triggers.assert_called_once_with(bot_worker=mock_bot_worker, message=mock_bot_message) + bot.trigger.assert_not_called() # Since listen_results is not empty, trigger should not be called + +@pytest.mark.asyncio +async def test_process_trigger_and_events_with_trigger_results(bot, mock_bot_worker, mock_bot_message): + # Mock _listen_for_triggers method to return None + bot._listen_for_triggers = AsyncMock(return_value=None) + + # Mock trigger method to return some results + bot.trigger = AsyncMock(return_value=["trigger_result"]) + + # Call the _process_trigger_and_events method + result = await bot._process_trigger_and_events(bot_worker=mock_bot_worker, message=mock_bot_message) + + # Assertions + assert result == ["trigger_result"] + bot._listen_for_triggers.assert_called_once_with(bot_worker=mock_bot_worker, message=mock_bot_message) + bot.trigger.assert_called_once_with(mock_bot_message.type, mock_bot_worker, mock_bot_message) + +@pytest.mark.asyncio +async def test_trigger_with_registered_event_handler(bot, mock_bot_worker, mock_bot_message): + # Register some event handlers + mock_event_handler = AsyncMock() + bot._events = {"some_event": [mock_event_handler]} + + # Call the trigger method with the event that has a registered handler + await bot.trigger("some_event", mock_bot_worker, mock_bot_message) + + # Assertions + mock_event_handler.assert_called_once_with(mock_bot_worker, mock_bot_worker, mock_bot_message) + +@pytest.mark.asyncio +async def test_trigger_with_unregistered_event_handler(bot, mock_bot_worker, mock_bot_message): + # Call the trigger method with an event that doesn't have any registered handler + await bot.trigger("unregistered_event", mock_bot_worker, mock_bot_message) + + # No event handler should be called, so assert that it's not called + assert not mock_bot_worker.called + +@pytest.mark.asyncio +async def test_listen_for_triggers_with_matching_trigger(bot, mock_bot_worker, mock_bot_message): + # Mock a trigger and its handler + mock_trigger = Mock() + mock_trigger.pattern = "hello" + mock_trigger.handler = AsyncMock(return_value="trigger_results") + bot._triggers = {"message": [mock_trigger]} + mock_bot_message.type = "message" + mock_bot_message.text = "hello" + + # Call the _listen_for_triggers method with a message of type "message" + result = await bot._listen_for_triggers(mock_bot_worker, mock_bot_message) + + # Assertions + assert result == "trigger_results" + mock_trigger.handler.assert_called_once_with(mock_bot_worker, mock_bot_message) + +@pytest.mark.asyncio +async def test_listen_for_triggers_with_no_matching_trigger(bot, mock_bot_worker, mock_bot_message): + # Call the _listen_for_triggers method with a message of type "unregistered_type" + result = await bot._listen_for_triggers(mock_bot_worker, mock_bot_message) + + # Assertions + assert result == False # No trigger matched, so the result should be False + + +@pytest.mark.asyncio +async def test_test_trigger_with_string_pattern(bot, mock_trigger, mock_bot_message): + # Set up the trigger with a string pattern + mock_trigger.pattern = "test_pattern" + + # Call the _test_trigger method with a message that matches the pattern + mock_bot_message.text = "test_pattern" + result = await bot._test_trigger(mock_trigger, mock_bot_message) + + # Assertions + assert result == True + + # Call the _test_trigger method with a message that does not match the pattern + mock_bot_message.text = "different_pattern" + result = await bot._test_trigger(mock_trigger, mock_bot_message) + + # Assertions + assert result == False + +@pytest.mark.asyncio +async def test_test_trigger_with_list_pattern(bot, mock_trigger, mock_bot_message): + # Set up the trigger with a list pattern + mock_trigger.pattern = ["pattern1", "pattern2"] + + # Call the _test_trigger method with a message that matches one of the patterns + mock_bot_message.text = "pattern1" + result = await bot._test_trigger(mock_trigger, mock_bot_message) + + # Assertions + assert result == True + + # Call the _test_trigger method with a message that does not match any pattern + mock_bot_message.text = "pattern3" + result = await bot._test_trigger(mock_trigger, mock_bot_message) + + # Assertions + assert result == False + +@pytest.mark.asyncio +async def test_test_trigger_with_callable_pattern(bot, mock_trigger, mock_bot_message): + # Set up the trigger with a callable pattern + mock_trigger.pattern = AsyncMock(return_value=True) + + # Call the _test_trigger method with any message (the pattern will always return True) + result = await bot._test_trigger(mock_trigger, mock_bot_message) + + # Assertions + assert result == True + +@pytest.mark.asyncio +async def test_test_trigger_with_invalid_pattern(bot, mock_trigger, mock_bot_message): + # Set up the trigger with an invalid pattern (None in this case) + mock_trigger.pattern = None + + # Call the _test_trigger method with any message + result = await bot._test_trigger(mock_trigger, mock_bot_message) + + # Assertions + assert result == False + + +def test_hears_adds_trigger(bot): + # Mock the handler and pattern + handler = Mock() + pattern = "hello" + + # Call the hears method + bot.hears(pattern, handler) + + # Assert that the trigger is added to the list of triggers for the "message" event + assert len(bot._triggers["message"]) == 1 + assert bot._triggers["message"][0].pattern == pattern + assert bot._triggers["message"][0].handler == handler + +def test_on_adds_event_handler(bot): + # Mock the handler + handler = Mock() + event = "some_event" + + # Call the on method + bot.on(event, handler) + print(bot._events) + # Assert that the event handler is added to the list of events + assert len(bot._events[event]) == 1 + assert bot._events[event][0] == handler + +@pytest.mark.asyncio +async def test_spawn_with_turn_context(bot, mock_turn_context): + # Mock TurnContext configuration + mock_turn_context.activity = Mock() + mock_turn_context.activity.channel_id = "channel_id" + + # Call the spawn method with TurnContext + bot_worker = await bot.spawn(config=mock_turn_context) + + # Assertions + assert bot_worker._controller == bot + assert bot_worker._config["context"] == mock_turn_context + assert bot_worker._config["activity"] == mock_turn_context.activity + +@pytest.mark.asyncio +async def test_spawn_with_dialog_context(bot, mock_dialog_context): + # Mock DialogContext configuration + mock_dialog_context.context.activity = Mock() + mock_dialog_context.context.activity.channel_id = "channel_id" + + # Call the spawn method with DialogContext + bot_worker = await bot.spawn(config=mock_dialog_context) + + # Assertions + assert isinstance(bot_worker, BotWorker) + assert bot_worker._controller == bot + assert bot_worker._config["context"] == mock_dialog_context.context + assert bot_worker._config["activity"] == mock_dialog_context.context.activity