diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index f9dcc8e..56cb694 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -20,7 +20,7 @@ jobs: pip install pylint - name: pylint run: | - pylint $(git ls-files '*.py') --fail-under=9.0 --output-format=json > pylint-report.json + pylint $(git ls-files '*.py' | grep -v '^test/') --fail-under=9.0 --output-format=json > pylint-report.json - name: Generate Pylint Score id: pylint_score diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..45bcc80 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,29 @@ +name: PyTest + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements_dev.txt + - name: Test with pytest + run: | + mkdir -p junit + pytest test/ --doctest-modules --junitxml=junit/test-results.xml --cov=superfaktura --cov-report=xml --cov-report=html + - name: Upload pytest test results + uses: actions/upload-artifact@v4 + with: + name: pytest-results + path: junit/test-results.xml + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} diff --git a/requirements.txt b/requirements.txt index ca6d6b6..6079683 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests~=2.32.3 -python-dotenv~=1.0.1 \ No newline at end of file +python-dotenv~=1.0.1 +setuptools~=75.8.0 \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..a34cc3b --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,4 @@ +requests~=2.32.3 +python-dotenv~=1.0.1 +pytest~=8.3.5 +pytest-cov diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_superfaktura_api.py b/test/test_superfaktura_api.py new file mode 100644 index 0000000..c4e295e --- /dev/null +++ b/test/test_superfaktura_api.py @@ -0,0 +1,88 @@ +import os +import pytest +from unittest.mock import patch, mock_open, MagicMock + +import requests + +from superfaktura.superfaktura_api import ( + SuperFakturaAPI, + SuperFakturaAPIException, + SuperFakturaAPIMissingCredentialsException, +) + +@pytest.fixture +def api(): + with patch.dict(os.environ, { + "SUPERFAKTURA_API_KEY": "test_key", + "SUPERFAKTURA_API_URL": "https://api.superfaktura.cz", + "SUPERFAKTURA_API_EMAIL": "test_email", + "SUPERFAKTURA_API_COMPANY_ID": "test_company_id" + }): + return SuperFakturaAPI() + +def test_missing_credentials(): + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(SuperFakturaAPIMissingCredentialsException): + SuperFakturaAPI() + +def test_get(api): + with patch("requests.get") as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = {"data": "test"} + response = api.get("test_endpoint") + assert response == {"data": "test"} + +def test_get_failure(api): + with patch("requests.get") as mock_get: + mock_get.return_value.status_code = 404 + with pytest.raises(SuperFakturaAPIException): + api.get("test_endpoint") + +def test_download(api): + with patch("requests.get") as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.content = b"test_content" + with patch("builtins.open", mock_open()) as mock_file: + with open("test_file", "wb") as f: + api.download("test_endpoint", f) + mock_file().write.assert_called_once_with(b"test_content") + +def test_download_failure(api): + with patch("requests.get") as mock_get: + mock_get.return_value.status_code = 404 + with patch("builtins.open", mock_open()): + with open("test_file", "wb") as f: + with pytest.raises(SuperFakturaAPIException): + api.download("test_endpoint", f) + +def test_post(api): + with patch("requests.post") as mock_post: + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"data": "test"} + response = api.post("test_endpoint", '{"name": "Example"}') + assert response == {"data": "test"} + +def test_post_failure(api): + with patch("requests.post") as mock_post: + mock_post.return_value.status_code = 404 + mock_post.return_value.json.return_value = {"error": "not found"} + with pytest.raises(SuperFakturaAPIException): + api.post("test_endpoint", '{"name": "Example"}') + +def test_get_invalid_json(api): + with patch("requests.get") as mock_get: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.side_effect = requests.exceptions.JSONDecodeError("msg", "doc", 0) + mock_get.return_value = mock_response + with pytest.raises(SuperFakturaAPIException, match="Unable to decode response as JSON"): + api.get("test_endpoint") + +def test_download_not_writable_descriptor(api): + with patch("requests.get") as mock_get: + mock_get.return_value.status_code = 200 + mock_get.return_value.content = b"test_content" + mock_descriptor = MagicMock() + mock_descriptor.writable.return_value = False + with pytest.raises(SuperFakturaAPIException, match=" is not writable"): + api.download("test_endpoint", mock_descriptor)