diff --git a/.gitignore b/.gitignore index 8b11ccf..7cacb97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ +# Locally specific files .env .idea/ -.idea/vcs.xml + +# Cache +__pycache__/ +*.pyc + +# Tests and Coverage +.pytest_cache/ +.coverage* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e105a6f..6b47718 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,6 @@ stages: - lint + - test - build - deploy @@ -11,17 +12,24 @@ stages: .base: image: python:3.11.2 + only: + changes: + - "/poetry.lock" + - "/pyproject.toml" + - "/app/*" + +.test: + extends: .base + stage: test + before_script: + - *install-deps .lint: extends: .base stage: lint before_script: - *install-deps - only: - changes: - - "/poetry.lock" - - "/pyproject.toml" - - "/app/*" + ###################################### # # # LINT STEPS # @@ -38,6 +46,18 @@ isort: script: - poetry run isort --check . +###################################### +# # +# TEST STEPS # +# # +###################################### + +test: + extends: .test + coverage: '/TOTAL.*\s+(\d+\%)/' + script: + - poetry run pytest --cov -v + ###################################### # # # BUILD STEPS # diff --git a/Dockerfile b/Dockerfile index 814af90..3b2b882 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,4 @@ RUN curl -sSL https://install.python-poetry.org | python3 - ENV PATH=/root/.local/bin:$PATH RUN poetry install -CMD ["poetry", "run", "python", "app/main.py"] +CMD ["poetry", "run", "python", "-m" , "app.main"] diff --git a/app/__pycache__/discord_client.cpython-311.pyc b/app/__pycache__/discord_client.cpython-311.pyc deleted file mode 100644 index 659fe9c..0000000 Binary files a/app/__pycache__/discord_client.cpython-311.pyc and /dev/null differ diff --git a/app/__pycache__/twitch_client.cpython-311.pyc b/app/__pycache__/twitch_client.cpython-311.pyc deleted file mode 100644 index 530ab0b..0000000 Binary files a/app/__pycache__/twitch_client.cpython-311.pyc and /dev/null differ diff --git a/app/discord_client.py b/app/discord_client.py index 610b335..a7a5d7b 100644 --- a/app/discord_client.py +++ b/app/discord_client.py @@ -2,7 +2,7 @@ import os import requests from loguru import logger -from twitch_client import StreamInformation +from app.twitch_client import StreamInformation class DiscordClient: diff --git a/app/main.py b/app/main.py index 5ed6a8f..353d939 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,10 @@ import os import time -from discord_client import DiscordClient +from app.discord_client import DiscordClient +from app.twitch_client import StreamInformation, TwitchClient from loguru import logger from requests import HTTPError -from twitch_client import StreamInformation, TwitchClient class Main: diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/conftest.py b/app/tests/conftest.py new file mode 100644 index 0000000..44a212e --- /dev/null +++ b/app/tests/conftest.py @@ -0,0 +1,16 @@ +from collections import namedtuple +from unittest import mock + +import pytest + + +@pytest.fixture +def mock_loggers(): + with ( + mock.patch("loguru.logger.info") as info_logger, + mock.patch("loguru.logger.error") as error_logger, + ): + mocked_loggers = namedtuple( + "mocked_loggers", ["info_logger", "error_logger"] + ) + yield mocked_loggers(info_logger, error_logger) diff --git a/app/tests/test_discord_client.py b/app/tests/test_discord_client.py new file mode 100644 index 0000000..5422787 --- /dev/null +++ b/app/tests/test_discord_client.py @@ -0,0 +1,44 @@ +import os +from unittest import mock + +import pytest +import requests_mock +from requests import HTTPError + +from app.discord_client import DiscordClient +from app.twitch_client import StreamInformation + + +def test_require_webhook_url(): + with pytest.raises(KeyError): + DiscordClient() + + +def test_send_information_to_discord_fails(mock_loggers): + stream = StreamInformation( + user_id="", + user_name="", + user_login="", + game_name="", + started_at="", + title="", + viewer_count=0, + _thumbnail_url="" + ) + + with ( + mock.patch.dict(os.environ, {"DISCORD_WEBHOOK_URL": "https://test"}), + requests_mock.Mocker() as requests_mocker, + ): + requests_mocker.post(url="https://test", status_code=400) + + discord_client = DiscordClient() + with pytest.raises(HTTPError): + discord_client.send_information_to_discord( + stream=stream, profile_image="" + ) + + assert len(mock_loggers.info_logger.call_args_list) == 1 + assert mock_loggers.info_logger.call_args.args[0] == ( + "Sending a message with an embed to the webhook..." + ) diff --git a/poetry.lock b/poetry.lock index c0708f5..6ec27a0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,19 @@ +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs"] +docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["attrs", "zope.interface"] +tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] +tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] + [[package]] name = "black" version = "23.1.0" @@ -54,6 +70,17 @@ category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +[[package]] +name = "coverage" +version = "7.2.1" +description = "Code coverage measurement for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +toml = ["tomli"] + [[package]] name = "idna" version = "3.4" @@ -62,6 +89,14 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "isort" version = "5.12.0" @@ -127,6 +162,73 @@ python-versions = ">=3.7" docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx (>=6.1.3)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2.1)"] +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.2.2" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.0.0" +description = "Pytest plugin for measuring coverage." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-cover" +version = "3.0.0" +description = "Pytest plugin for measuring coverage. Forked from `pytest-cov`." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pytest-cov = ">=2.0" + +[[package]] +name = "pytest-coverage" +version = "0.0" +description = "Pytest plugin for measuring coverage. Forked from `pytest-cov`." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pytest-cover = "*" + [[package]] name = "requests" version = "2.28.2" @@ -145,6 +247,30 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-mock" +version = "1.10.0" +description = "Mock out responses from the requests package" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.3,<3" +six = "*" + +[package.extras] +fixture = ["fixtures"] +test = ["fixtures", "mock", "purl", "pytest", "sphinx", "testrepository (>=0.0.18)", "testtools", "requests-futures"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "urllib3" version = "1.26.14" @@ -172,9 +298,10 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] [metadata] lock-version = "1.1" python-versions = "^3.11" -content-hash = "2cd384690a48bd802789a049147162c975c512098cc42b7716ba54b06f448092" +content-hash = "c88f613c25a933f82f5abb7053b249fe6bc13d021c655f1da432fca43c5d2fba" [metadata.files] +attrs = [] black = [] certifi = [] charset-normalizer = [] @@ -183,13 +310,22 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [] +coverage = [] idna = [] +iniconfig = [] isort = [] loguru = [] mypy-extensions = [] packaging = [] pathspec = [] platformdirs = [] +pluggy = [] +pytest = [] +pytest-cov = [] +pytest-cover = [] +pytest-coverage = [] requests = [] +requests-mock = [] +six = [] urllib3 = [] win32-setctime = [] diff --git a/pyproject.toml b/pyproject.toml index c3eb69b..1a2ef44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,9 @@ requests = "^2.28.2" loguru = "^0.6.0" black = "^23.1.0" isort = "^5.12.0" +pytest = "^7.2.2" +requests-mock = "^1.10.0" +pytest-coverage = "^0.0" [tool.poetry.dev-dependencies]