Source code for test.cata_log_hub.providers.test_base

# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Cata-Log - the central hub for digital flyers
# Copyright (C) 2026 David Aderbauer & The Cata-Log Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from datetime import UTC, datetime, timedelta

import httpx
import pytest
from freezegun import freeze_time
from pydantic import ValidationError

from cata_log_hub import exceptions
from cata_log_hub.providers import Provider
from cata_log_hub.settings import get_settings
from cata_log_hub.utils.page_numbers import PageNumber
from test.cata_log_hub.conftest import SideEffects


[docs] def test_duplicate_provider(provider_test_class): class DuplicateProvider(Provider): uid = provider_test_class.uid assert provider_test_class.uid in Provider._registry assert Provider._registry[provider_test_class.uid] == provider_test_class
[docs] def test_relevant_datetime(faker, provider_test_class): with freeze_time(faker.date_time(tzinfo=UTC)): assert provider_test_class( provider_test_class.default_configuration, )._relevant_datetime == datetime.now(UTC)
[docs] def test_client(provider_test_class, fake_request): provider_class_client = provider_test_class( provider_test_class.default_configuration, )._client assert provider_class_client.follow_redirects is True assert provider_class_client.timeout.connect == get_settings().request_timeout assert "httpx" not in provider_class_client.headers["User-Agent"] assert provider_class_client.event_hooks["response"] with pytest.raises( httpx.HTTPStatusError, check=lambda status_error: status_error.response.status_code == 401, ): assert provider_class_client.event_hooks["response"][0]( httpx.Response(status_code=401, request=fake_request) ) with pytest.raises( httpx.HTTPStatusError, check=lambda status_error: status_error.response.status_code == 404, ): assert provider_class_client.event_hooks["response"][0]( httpx.Response(status_code=404, request=fake_request) ) with pytest.raises( httpx.HTTPStatusError, check=lambda status_error: status_error.response.status_code == 500, ): assert provider_class_client.event_hooks["response"][0]( httpx.Response(status_code=500, request=fake_request) ) provider_class_client.event_hooks["response"][0]( httpx.Response(status_code=301, request=fake_request) ) provider_class_client.event_hooks["response"][0]( httpx.Response(status_code=200, request=fake_request) )
[docs] @pytest.mark.parametrize( ("side_effect", "expected_error"), [ (SideEffects.DONOTHING, None), (SideEffects.HTTP_400, exceptions.ProviderMisconfiguredOrBrokenWarning), (SideEffects.HTTP_500, exceptions.ProviderMisconfiguredOrBrokenWarning), (SideEffects.HTTP_404, exceptions.ProviderMisconfiguredOrBrokenWarning), (SideEffects.TRANSPORTERROR, exceptions.NetworkError), (SideEffects.KEYERROR, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, exceptions.ProviderBrokenWarning), (SideEffects.CATALOGUNAVAILABLE, exceptions.CatalogUnavailableWarning), ], ) def test_get_catalog_data(provider_test_class, side_effect, expected_error): if expected_error: with pytest.raises(expected_error): provider_test_class( { "side_effect": side_effect, **provider_test_class.default_configuration, } ) else: provider_test_class( {"side_effect": side_effect, **provider_test_class.default_configuration} )
[docs] @pytest.mark.parametrize( ("side_effect", "expected_error"), [ (SideEffects.DONOTHING, None), (SideEffects.HTTP_400, exceptions.ProviderBrokenWarning), (SideEffects.HTTP_500, exceptions.ProviderBrokenWarning), (SideEffects.HTTP_404, exceptions.ProviderBrokenWarning), (SideEffects.TRANSPORTERROR, exceptions.ProviderBrokenWarning), (SideEffects.KEYERROR, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, exceptions.ProviderBrokenWarning), (SideEffects.CATALOGUNAVAILABLE, exceptions.ProviderBrokenWarning), ], ) def test_get_valid_since(provider_test_class, side_effect, expected_error): if expected_error: with pytest.raises(expected_error): provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **provider_test_class.default_configuration, } ).get_valid_until() else: result = provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **provider_test_class.default_configuration, } ).get_valid_until() assert result assert result.tzinfo assert result == provider_test_class.const_valid_since.replace( tzinfo=provider_test_class.region.timezone )
[docs] @pytest.mark.parametrize( ("side_effect", "expected_error"), [ (SideEffects.DONOTHING, None), (SideEffects.HTTP_400, exceptions.ProviderBrokenWarning), (SideEffects.HTTP_500, exceptions.ProviderBrokenWarning), (SideEffects.HTTP_404, exceptions.ProviderBrokenWarning), (SideEffects.TRANSPORTERROR, exceptions.ProviderBrokenWarning), (SideEffects.KEYERROR, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, exceptions.ProviderBrokenWarning), (SideEffects.CATALOGUNAVAILABLE, exceptions.ProviderBrokenWarning), ], ) def test_get_valid_until(provider_test_class, side_effect, expected_error): if expected_error: with pytest.raises(expected_error): provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **provider_test_class.default_configuration, } ).get_valid_until() else: result = provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **provider_test_class.default_configuration, } ).get_valid_until() assert result assert result.tzinfo
[docs] @pytest.mark.parametrize( ("side_effect", "page_number", "expected_error"), [ (SideEffects.DONOTHING, 0, None), (SideEffects.DONOTHING, 6, None), (SideEffects.HTTP_400, 0, exceptions.ProviderMisconfiguredOrBrokenWarning), (SideEffects.HTTP_400, 3, exceptions.PagesExhausted), (SideEffects.HTTP_500, 0, exceptions.ProviderMisconfiguredOrBrokenWarning), (SideEffects.HTTP_500, 4, exceptions.PagesExhausted), (SideEffects.HTTP_404, 0, exceptions.ProviderMisconfiguredOrBrokenWarning), (SideEffects.HTTP_404, 6, exceptions.PagesExhausted), (SideEffects.TRANSPORTERROR, 0, exceptions.NetworkError), (SideEffects.TRANSPORTERROR, 2, exceptions.NetworkError), (SideEffects.KEYERROR, 0, exceptions.ProviderBrokenWarning), (SideEffects.KEYERROR, 1, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, 0, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, 9, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, 0, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, 5, exceptions.ProviderBrokenWarning), ( SideEffects.PAGESEXHAUSTED, 0, exceptions.ProviderMisconfiguredOrBrokenWarning, ), (SideEffects.PAGESEXHAUSTED, 34, exceptions.PagesExhausted), (SideEffects.CATALOGUNAVAILABLE, 0, exceptions.CatalogUnavailableWarning), ( SideEffects.CATALOGUNAVAILABLE, 6, exceptions.ProviderMisconfiguredOrBrokenWarning, ), ], ) def test_get_page( provider_test_class, side_effect, page_number, expected_error, ): if expected_error: with pytest.raises(expected_error): provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **provider_test_class.default_configuration, } ).get_page( PageNumber( page_number, start_number=provider_test_class.first_page_number ) ) else: result = provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **provider_test_class.default_configuration, } ).get_page( PageNumber(page_number, start_number=provider_test_class.first_page_number) ) assert result
[docs] @pytest.mark.parametrize( ("side_effect", "expected_error"), [ (SideEffects.DONOTHING, None), (SideEffects.HTTP_400, exceptions.CatalogUnavailableWarning), (SideEffects.HTTP_500, exceptions.CatalogUnavailableWarning), (SideEffects.HTTP_404, exceptions.CatalogUnavailableWarning), (SideEffects.TRANSPORTERROR, exceptions.NetworkError), (SideEffects.KEYERROR, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, exceptions.ProviderBrokenWarning), (SideEffects.CATALOGUNAVAILABLE, exceptions.CatalogUnavailableWarning), ], ) def test_get_catalog_data__preview( preview_provider_test_class, side_effect, expected_error, ): if expected_error: with pytest.raises(expected_error): preview_provider_test_class( { "side_effect": side_effect, **preview_provider_test_class.default_configuration, } ) else: preview_provider_test_class( { "side_effect": side_effect, **preview_provider_test_class.default_configuration, } )
[docs] @pytest.mark.parametrize( ("side_effect", "expected_error"), [ (SideEffects.DONOTHING, None), (SideEffects.HTTP_400, exceptions.ProviderBrokenWarning), (SideEffects.HTTP_500, exceptions.ProviderBrokenWarning), (SideEffects.HTTP_404, exceptions.ProviderBrokenWarning), (SideEffects.TRANSPORTERROR, exceptions.ProviderBrokenWarning), (SideEffects.KEYERROR, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, exceptions.ProviderBrokenWarning), (SideEffects.CATALOGUNAVAILABLE, exceptions.ProviderBrokenWarning), ], ) def test_get_valid_since__preview( preview_provider_test_class, side_effect, expected_error, ): if expected_error: with pytest.raises(expected_error): preview_provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **preview_provider_test_class.default_configuration, } ).get_valid_since() else: result = preview_provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **preview_provider_test_class.default_configuration, } ).get_valid_since() assert result assert result.tzinfo assert result == preview_provider_test_class.const_valid_since.replace( tzinfo=preview_provider_test_class.region.timezone )
[docs] @pytest.mark.parametrize( ("side_effect", "expected_error"), [ (SideEffects.DONOTHING, None), (SideEffects.HTTP_400, exceptions.ProviderBrokenWarning), (SideEffects.HTTP_500, exceptions.ProviderBrokenWarning), (SideEffects.HTTP_404, exceptions.ProviderBrokenWarning), (SideEffects.TRANSPORTERROR, exceptions.ProviderBrokenWarning), (SideEffects.KEYERROR, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, exceptions.ProviderBrokenWarning), (SideEffects.CATALOGUNAVAILABLE, exceptions.ProviderBrokenWarning), ], ) def test_get_valid_until__preview( preview_provider_test_class, side_effect, expected_error, ): if expected_error: with pytest.raises(expected_error): preview_provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **preview_provider_test_class.default_configuration, } ).get_valid_until() else: result = preview_provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **preview_provider_test_class.default_configuration, } ).get_valid_until() assert result assert result.tzinfo
[docs] @pytest.mark.parametrize( ("side_effect", "page_number", "expected_error"), [ (SideEffects.DONOTHING, 0, None), (SideEffects.DONOTHING, 6, None), (SideEffects.HTTP_400, 0, exceptions.CatalogUnavailableWarning), (SideEffects.HTTP_400, 3, exceptions.PagesExhausted), (SideEffects.HTTP_500, 0, exceptions.CatalogUnavailableWarning), (SideEffects.HTTP_500, 4, exceptions.PagesExhausted), (SideEffects.HTTP_404, 0, exceptions.CatalogUnavailableWarning), (SideEffects.HTTP_404, 6, exceptions.PagesExhausted), (SideEffects.TRANSPORTERROR, 0, exceptions.NetworkError), (SideEffects.TRANSPORTERROR, 2, exceptions.NetworkError), (SideEffects.KEYERROR, 0, exceptions.ProviderBrokenWarning), (SideEffects.KEYERROR, 1, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, 0, exceptions.ProviderBrokenWarning), (SideEffects.VALUEERROR, 9, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, 0, exceptions.ProviderBrokenWarning), (SideEffects.EXCEPTION, 5, exceptions.ProviderBrokenWarning), ( SideEffects.PAGESEXHAUSTED, 0, exceptions.ProviderMisconfiguredOrBrokenWarning, ), (SideEffects.PAGESEXHAUSTED, 34, exceptions.PagesExhausted), (SideEffects.CATALOGUNAVAILABLE, 0, exceptions.CatalogUnavailableWarning), ( SideEffects.CATALOGUNAVAILABLE, 6, exceptions.ProviderMisconfiguredOrBrokenWarning, ), ], ) def test_get_page__preview( preview_provider_test_class, side_effect, page_number, expected_error, ): if expected_error: with pytest.raises(expected_error): preview_provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **preview_provider_test_class.default_configuration, } ).get_page( PageNumber( page_number, start_number=preview_provider_test_class.first_page_number, ) ) else: result = preview_provider_test_class( { "side_effect": side_effect, "pass_get_catalog_data": "True", **preview_provider_test_class.default_configuration, } ).get_page( PageNumber( page_number, start_number=preview_provider_test_class.first_page_number ) ) assert result
[docs] def test_relevant_datetime_preview(faker, preview_provider_test_class): with freeze_time(faker.date_time(tzinfo=UTC)): assert preview_provider_test_class( preview_provider_test_class.default_configuration, )._relevant_datetime == datetime.now(UTC) + timedelta(days=3)
[docs] def test_relevant_datetime_preview_error(faker, preview_provider_test_class): def bad__get_preview_timedelta(*args): raise ValueError preview_provider_test_class._get_preview_timedelta = bad__get_preview_timedelta with pytest.raises(exceptions.ProviderBrokenWarning): preview_provider_test_class(preview_provider_test_class.default_configuration)
[docs] @pytest.mark.parametrize( "configuration", [ { "required_config": "asdf", "optional_config": "test", "typed_config": 4, "optional_typed_config": 2.31, }, { "required_config": "", "optional_config": "test", "typed_config": 0, "optional_typed_config": 2.31, }, {"required_config": "asdf", "typed_config": 4}, { "required_config": "asdf", "optional_config": "test", "typed_config": 4, "optional_typed_config": 2.31, "extra_config": "rgn", }, ], ) def test_validate_configuration__success(provider_test_class, configuration): validated_configuration = provider_test_class.validate_configuration( configuration ).model_dump() for ( config_name, config_field, ) in provider_test_class.Configuration.model_fields.items(): assert config_name in validated_configuration assert validated_configuration[config_name] == configuration.get( config_name, config_field.default )
[docs] @pytest.mark.parametrize( ("configuration", "missing_configurations"), [ ( { "required_config": "asdf", "optional_config": "test", "optional_typed_config": 2.31, }, ["typed_config"], ), ( { "optional_config": "test", "typed_config": 4, "optional_typed_config": 2.31, }, ["required_config"], ), ( {"optional_config": "test", "optional_typed_config": 2.31}, ["required_config", "typed_config"], ), ({"optional_typed_config": 2.31}, ["required_config", "typed_config"]), ({}, ["required_config", "typed_config"]), ], ) def test_validate_configuration__missing_field( provider_test_class, configuration, missing_configurations ): with pytest.raises(exceptions.ProviderInvalidConfigurationWarning) as exc_info: provider_test_class.validate_configuration(configuration) assert exc_info.value.__cause__ assert isinstance(exc_info.value.__cause__, ValidationError)
[docs] @pytest.mark.parametrize( ("configuration", "bad_configurations"), [ ( { "required_config": "asdf", "typed_config": "notanumber", }, ["typed_config"], ), ( { "required_config": "asdf", "typed_config": 4, "optional_typed_config": "badfloat", }, ["optional_typed_config"], ), ( { "required_config": "asdf", "typed_config": "string", "optional_typed_config": "badint", }, ["typed_config", "optional_typed_config"], ), ], ) def test_validate_configuration__invalid_field( provider_test_class, configuration, bad_configurations ): with pytest.raises(exceptions.ProviderInvalidConfigurationWarning) as exc_info: provider_test_class.validate_configuration(configuration) assert exc_info.value.__cause__ assert isinstance(exc_info.value.__cause__, ValidationError)