add tests (#44)

* fixed test_window_no_event

* added strategy tests

* removed unused code

* added config tests

* added more config tests

* refactored tests

* updated pytest
This commit is contained in:
Simone Robutti 2021-07-18 18:23:30 +02:00 committed by GitHub
parent b75f0ff057
commit 8a27de8981
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 257 additions and 100 deletions

View File

@ -11,7 +11,6 @@ from mobilizon_bots.config.publishers import publisher_names
def build_settings(
settings_files: List[str] = None, validators: List[Validator] = None
):
SETTINGS_FILE = (
settings_files
or os.environ.get("MOBILIZON_BOTS_SETTINGS_FILE")
@ -38,29 +37,8 @@ def build_and_validate_settings(settings_files: List[str] = None):
specific for each publisher, notifier and publication strategy.
"""
# we first do a preliminary load of the settings without validation. We will later use them to determine which
# publishers, notifiers and strategy have been selected
raw_settings = build_settings(settings_files=settings_files)
# These validators are always applied
base_validators = (
[
# strategy to decide events to publish
Validator("selection.strategy", must_exist=True, is_type_of=str),
Validator(
"publishing.window.begin",
must_exist=True,
is_type_of=int,
gte=0,
lte=24,
),
Validator(
"publishing.window.end", must_exist=True, is_type_of=int, gte=0, lte=24
),
# url of the main Mobilizon instance to download events from
Validator("source.mobilizon.url", must_exist=True, is_type_of=str),
Validator("source.mobilizon.group", must_exist=True, is_type_of=str),
]
preliminary_validators = (
[Validator("selection.strategy", must_exist=True, is_type_of=str)]
+ [
Validator(
f"publisher.{publisher_name}.active", must_exist=True, is_type_of=bool
@ -75,6 +53,26 @@ def build_and_validate_settings(settings_files: List[str] = None):
]
)
# we first do a preliminary load of the settings without validation. We will later use them to determine which
# publishers, notifiers and strategy have been selected
raw_settings = build_settings(
settings_files=settings_files, validators=preliminary_validators
)
# These validators are always applied
base_validators = [
# strategy to decide events to publish
Validator(
"publishing.window.begin", must_exist=True, is_type_of=int, gte=0, lte=24,
),
Validator(
"publishing.window.end", must_exist=True, is_type_of=int, gte=0, lte=24
),
# url of the main Mobilizon instance to download events from
Validator("source.mobilizon.url", must_exist=True, is_type_of=str),
Validator("source.mobilizon.group", must_exist=True, is_type_of=str),
] + preliminary_validators
# we retrieve validators that are conditional. Each module will analyze the settings and decide which validators
# need to be applied.
strategy_validators = strategies.get_validators(raw_settings)
@ -107,11 +105,11 @@ class CustomConfig:
if cls._instance is None:
print("Creating the object")
cls._instance = super(CustomConfig, cls).__new__(cls)
cls.settings = build_settings(settings_files)
cls.settings = build_and_validate_settings(settings_files)
return cls._instance
def update(self, settings_files: List[str] = None):
self.settings = build_settings(settings_files)
self.settings = build_and_validate_settings(settings_files)
def get_settings():

View File

@ -89,23 +89,6 @@ class SelectNextEventStrategy(EventSelectionStrategy):
return first_unpublished_event
class EventSelector:
def __init__(
self,
published_events: List[MobilizonEvent],
unpublished_events: List[MobilizonEvent],
):
self.published_events = published_events.sort(key=lambda x: x.begin_datetime)
self.unpublished_events = unpublished_events.sort(
key=lambda x: x.begin_datetime
)
def select_event_to_publish(
self, strategy: EventSelectionStrategy
) -> Optional[MobilizonEvent]:
return strategy._select(self.published_events, self.unpublished_events)
STRATEGY_NAME_TO_STRATEGY_CLASS = {"next_event": SelectNextEventStrategy}

View File

@ -163,9 +163,5 @@ class AbstractPublisher(AbstractNotifier):
"""
Retrieves publisher's message template.
"""
template_path = (
self.conf.msg_template_path
if hasattr(self.conf, "msg_template_path")
else self.default_template_path
)
template_path = self.conf.msg_template_path or self.default_template_path
return JINJA_ENV.get_template(template_path)

95
poetry.lock generated
View File

@ -32,7 +32,7 @@ python-versions = ">=3.5"
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
@ -40,7 +40,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
name = "attrs"
version = "21.2.0"
description = "Classes Without Boilerplate"
category = "dev"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@ -110,6 +110,14 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "iso8601"
version = "0.1.14"
@ -140,19 +148,11 @@ category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "more-itertools"
version = "8.8.0"
description = "More routines for operating on iterables, beyond itertools"
category = "dev"
optional = false
python-versions = ">=3.5"
[[package]]
name = "packaging"
version = "21.0"
description = "Core utilities for Python packages"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.6"
@ -163,7 +163,7 @@ pyparsing = ">=2.0.2"
name = "pluggy"
version = "0.13.1"
description = "plugin and hook calling mechanisms for python"
category = "dev"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
@ -174,7 +174,7 @@ dev = ["pre-commit", "tox"]
name = "py"
version = "1.10.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
@ -182,7 +182,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
category = "dev"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
@ -196,39 +196,38 @@ python-versions = ">=3.7,<4.0"
[[package]]
name = "pytest"
version = "5.4.3"
version = "6.2.4"
description = "pytest: simple powerful testing with Python"
category = "dev"
category = "main"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.6"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=17.4.0"
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
more-itertools = ">=4.0.0"
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.5.0"
wcwidth = "*"
pluggy = ">=0.12,<1.0.0a1"
py = ">=1.8.2"
toml = "*"
[package.extras]
checkqa-mypy = ["mypy (==v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "0.10.0"
version = "0.15.1"
description = "Pytest support for asyncio."
category = "dev"
optional = false
python-versions = ">= 3.5"
python-versions = ">= 3.6"
[package.dependencies]
pytest = ">=3.0.6"
pytest = ">=5.4.0"
[package.extras]
testing = ["async-generator (>=1.3)", "coverage", "hypothesis (>=3.64)"]
testing = ["coverage", "hypothesis (>=5.7.1)"]
[[package]]
name = "python-dateutil"
@ -291,6 +290,14 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "tortoise-orm"
version = "0.17.4"
@ -333,18 +340,10 @@ brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "wcwidth"
version = "0.2.5"
description = "Measures the displayed width of unicode strings in a terminal"
category = "dev"
optional = false
python-versions = "*"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "0879accc273f3fc21de4ca7a01682392ed44f1173b17b1e09e884a5b2c7ad51d"
content-hash = "8b2e404c14110b5a47d3ec0480b838c4811ce1ebfbe68170bd60a3d414dbb7c8"
[metadata.files]
aiosqlite = [
@ -391,6 +390,10 @@ idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
iso8601 = [
{file = "iso8601-0.1.14-py2.py3-none-any.whl", hash = "sha256:e7e1122f064d626e17d47cd5106bed2c620cb38fe464999e0ddae2b6d2de6004"},
{file = "iso8601-0.1.14.tar.gz", hash = "sha256:8aafd56fa0290496c5edbb13c311f78fa3a241f0853540da09d9363eae3ebd79"},
@ -435,10 +438,6 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
more-itertools = [
{file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"},
{file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"},
]
packaging = [
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
@ -460,12 +459,12 @@ pypika-tortoise = [
{file = "pypika_tortoise-0.1.1-py3-none-any.whl", hash = "sha256:860020094e01058ea80602c90d4a843d0a42cffefcf4f3cb1a7f2c18b880c638"},
]
pytest = [
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
{file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"},
{file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"},
]
pytest-asyncio = [
{file = "pytest-asyncio-0.10.0.tar.gz", hash = "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf"},
{file = "pytest_asyncio-0.10.0-py3-none-any.whl", hash = "sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"},
{file = "pytest-asyncio-0.15.1.tar.gz", hash = "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f"},
{file = "pytest_asyncio-0.15.1-py3-none-any.whl", hash = "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea"},
]
python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
@ -487,6 +486,10 @@ six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tortoise-orm = [
{file = "tortoise-orm-0.17.4.tar.gz", hash = "sha256:8314a9ae63d3f009bac5da3e7d1f7e3f2de8f9bad43ce1efcd3e059209cd3f9d"},
{file = "tortoise_orm-0.17.4-py3-none-any.whl", hash = "sha256:f052b6089e30748afec88669f1a1cf01a3662cdac81cf5427dfb338839ad6027"},
@ -500,7 +503,3 @@ urllib3 = [
{file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"},
{file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]

View File

@ -13,12 +13,12 @@ Jinja2 = "^2.11.3"
requests = "^2.25.1"
arrow = "^1.1.0"
click = "^8.0.1"
pytest = "^6.2.4"
[tool.poetry.dev-dependencies]
asynctest = "^0.13"
pytest = "^5.3"
responses = "^0.13.3"
pytest-asyncio = "^0.10"
pytest-asyncio = "^0.15.1"
asynctest = "^0.13.0"
[build-system]
requires = ["poetry-core>=1.0.0"]

0
tests/config/__init__.py Normal file
View File

View File

@ -0,0 +1,15 @@
from mobilizon_bots.config.config import get_settings, update_settings_files
def test_singleton():
config_1 = get_settings()
config_2 = get_settings()
assert id(config_1) == id(config_2)
def test_singleton_update():
config_1 = get_settings()
config_2 = update_settings_files([])
config_3 = get_settings()
assert id(config_1) != id(config_2)
assert id(config_2) == id(config_3)

View File

@ -0,0 +1,40 @@
import dynaconf
import pkg_resources
import pytest
from mobilizon_bots.config.config import update_settings_files
@pytest.fixture
def invalid_settings_file(tmp_path, toml_content):
file = tmp_path / "tmp.toml"
file.write_text(toml_content)
return file
@pytest.mark.parametrize("toml_content", ["invalid toml["])
def test_update_failure_invalid_toml(invalid_settings_file):
with pytest.raises(dynaconf.vendor.toml.decoder.TomlDecodeError):
update_settings_files([invalid_settings_file.absolute()])
@pytest.mark.parametrize("toml_content", [""])
def test_update_failure_invalid_preliminary_config(invalid_settings_file):
with pytest.raises(dynaconf.validator.ValidationError):
update_settings_files([invalid_settings_file.absolute()])
@pytest.mark.parametrize(
"invalid_toml,pattern_in_exception",
[
["config_with_strategy.toml", "publisher.*.active"],
["config_with_preliminary.toml", "publishing.window.begin"],
["config_with_invalid_telegram.toml", "token"],
],
)
def test_update_failure_config_without_publishers(invalid_toml, pattern_in_exception):
with pytest.raises(dynaconf.validator.ValidationError) as e:
update_settings_files(
[pkg_resources.resource_filename("tests.resources.config", invalid_toml)]
)
assert e.match(pattern_in_exception)

View File

@ -34,7 +34,8 @@ def mock_publication_window(publication_window):
)
def test_window_no_event():
@pytest.mark.parametrize("current_hour", [15])
def test_window_no_event(mock_arrow_now):
selected_event = SelectNextEventStrategy().select([], [])
assert selected_event is None
@ -105,6 +106,55 @@ def test_window_simple_event_found(
assert selected_event is unpublished_events[0]
@pytest.mark.parametrize("current_hour", [15])
@pytest.mark.parametrize("strategy_name", ["next_event"])
def test_window_simple_no_published_events(
event_generator, set_strategy, mock_arrow_now,
):
"Testing that if no event is published, the function takes the first available unpublished event"
unpublished_events = [
event_generator(
published=False,
begin_date=arrow.Arrow(year=2021, month=1, day=5, hour=11, minute=30),
),
event_generator(
published=False,
begin_date=arrow.Arrow(year=2021, month=1, day=5, hour=11, minute=50),
),
]
selected_event = select_event_to_publish([], unpublished_events)
assert selected_event is unpublished_events[0]
@pytest.mark.parametrize("current_hour", [15])
@pytest.mark.parametrize("strategy_name", ["next_event"])
def test_window_simple_event_too_recent(
event_generator, set_strategy, mock_arrow_now,
):
"Testing that if an event has been published too recently, no event is selected for publication"
unpublished_events = [
event_generator(
published=False,
begin_date=arrow.Arrow(year=2021, month=1, day=5, hour=11, minute=30),
),
event_generator(
published=False,
begin_date=arrow.Arrow(year=2021, month=1, day=5, hour=11, minute=50),
),
]
published_events = [
event_generator(
published=True,
publication_time={"telegram": arrow.now().shift(minutes=-5)},
)
]
selected_event = select_event_to_publish(published_events, unpublished_events)
assert selected_event is None
@pytest.mark.parametrize("current_hour", [15])
@pytest.mark.parametrize(
"desired_break_window_days,days_passed_from_publication", [[1, 2], [2, 10], [4, 4]]

View File

View File

@ -0,0 +1,46 @@
[default.selection]
strategy = "next_event"
[default.source.mobilizon]
url="https://mobilizon.it/api"
group="tech_workers_coalition_italia"
[default.publishing.window]
begin=12
end=24
[default.selection.strategy_options]
break_between_events_in_minutes =1
[default.publisher.telegram]
active=true
chat_id="xxx"
token="xxx"
username="xxx"
[default.publisher.facebook]
active=false
[default.publisher.zulip]
active=false
[default.publisher.twitter]
active=false
[default.publisher.mastodon]
active=false
[default.notifier.telegram]
active=true
chat_id="xxx"
misspelled_token="xxx"
username="xxx"
[default.notifier.zulip]
active=false
[default.notifier.twitter]
active=false
[default.notifier.mastodon]
active=false

View File

@ -0,0 +1,27 @@
[default.selection]
strategy = "next_event"
[default.publisher.telegram]
active=true
[default.publisher.facebook]
active=false
[default.publisher.zulip]
active=false
[default.publisher.twitter]
active=false
[default.publisher.mastodon]
active=false
[default.notifier.telegram]
active=true
[default.notifier.zulip]
active=false
[default.notifier.twitter]
active=false
[default.notifier.mastodon]
active=false

View File

@ -0,0 +1,3 @@
[default.selection]
strategy = "next_event"