from unittest import mock import pytest from fansifter_common.auth.account import Account, AccountAccess from fansifter_common.auth.exceptions import PermissionDenied from dmp.shopify.handlers.get_stores_v2 import ( GetStoresV2Handler, GetStoresV2Request, ) from dmp.shopify.models import ShopifyStoreAssociation from tests.unit.faker import FakerTyped from tests.unit.types import CreateReportingModel def _make_request( identity_id: str, *, vendor_id: int | None = None, subaccount_id: int | None = None, limit: int | None = None, offset: int | None = None, order_by: list | None = None, ) -> GetStoresV2Request: return GetStoresV2Request( identity_id=identity_id, vendor_id=vendor_id, subaccount_id=subaccount_id, limit=limit or GetStoresV2Request.DEFAULT_LIMIT, offset=offset or GetStoresV2Request.DEFAULT_OFFSET, order_by=order_by if order_by is not None else GetStoresV2Request.DEFAULT_ORDER_BY, ) class TestGetStoresV2Handler: @pytest.mark.db def test_returns_empty_when_no_stores( self, handler: GetStoresV2Handler, fake: FakerTyped, ) -> None: response = handler.handle(_make_request(fake.uuid4_string())) assert response.total == 0 assert response.items == [] @pytest.mark.db def test_returns_stores_with_total( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, account: Account, fake: FakerTyped, ) -> None: store = create_reporting_model( ShopifyStoreAssociation, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = handler.handle(_make_request(fake.uuid4_string())) assert response.total == 1 assert len(response.items) == 1 assert response.items[0].id == store.id @pytest.mark.db def test_limit_restricts_items_but_total_reflects_full_count( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account = Account(vendor_id=1, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account] ) for _ in range(3): create_reporting_model( ShopifyStoreAssociation, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = handler.handle(_make_request(fake.uuid4_string(), limit=2)) assert response.total == 3 assert len(response.items) == 2 @pytest.mark.db def test_offset_skips_items( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account = Account(vendor_id=1, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account] ) identity_id = fake.uuid4_string() for _ in range(3): create_reporting_model( ShopifyStoreAssociation, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response_all = handler.handle(_make_request(identity_id)) response_offset = handler.handle(_make_request(identity_id, offset=1)) assert response_offset.total == 3 assert len(response_offset.items) == 2 assert response_offset.items == response_all.items[1:] @pytest.mark.db def test_vendor_id_filter_returns_only_matching_stores( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account_1 = Account(vendor_id=1, subaccount_id=0) account_2 = Account(vendor_id=2, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account_1, account_2] ) store_1 = create_reporting_model( ShopifyStoreAssociation, vendor_id=account_1.vendor_id, subaccount_id=account_1.subaccount_id, ) create_reporting_model( ShopifyStoreAssociation, vendor_id=account_2.vendor_id, subaccount_id=account_2.subaccount_id, ) response = handler.handle( _make_request( fake.uuid4_string(), vendor_id=account_1.vendor_id, subaccount_id=account_1.subaccount_id, ) ) assert response.total == 1 assert response.items[0].id == store_1.id def test_vendor_id_not_in_authorized_list_raises_permission_denied( self, handler: GetStoresV2Handler, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[Account(vendor_id=1, subaccount_id=0)] ) with pytest.raises(PermissionDenied): handler.handle( _make_request(fake.uuid4_string(), vendor_id=99, subaccount_id=0) ) def test_subaccount_id_not_in_authorized_list_raises_permission_denied( self, handler: GetStoresV2Handler, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[Account(vendor_id=0, subaccount_id=1)] ) with pytest.raises(PermissionDenied): handler.handle( _make_request(fake.uuid4_string(), vendor_id=0, subaccount_id=99) ) @pytest.mark.db def test_count_query_only_when_no_stores_match( self, handler: GetStoresV2Handler, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account = Account(vendor_id=1, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account] ) db = handler.store_association_repository.db with mock.patch.object( db.session, "execute", wraps=db.session.execute ) as mock_execute: response = handler.handle(_make_request(fake.uuid4_string())) assert response.total == 0 assert response.items == [] # only the count query should run, not the items query assert mock_execute.call_count == 1 @pytest.mark.db def test_pagination_consistency_across_pages( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account = Account(vendor_id=1, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account] ) identity_id = fake.uuid4_string() for _ in range(5): create_reporting_model( ShopifyStoreAssociation, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) page1 = handler.handle(_make_request(identity_id, limit=2, offset=0)) page2 = handler.handle(_make_request(identity_id, limit=2, offset=2)) page3 = handler.handle(_make_request(identity_id, limit=2, offset=4)) assert page1.total == 5 assert page2.total == 5 assert page3.total == 5 assert len(page1.items) == 2 assert len(page2.items) == 2 assert len(page3.items) == 1 assert len(page1.items) + len(page2.items) + len(page3.items) == page1.total @pytest.mark.db def test_order_by_global_participant_id_asc_puts_null_first( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account = Account(vendor_id=1, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account] ) store_assigned = create_reporting_model( ShopifyStoreAssociation, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id="artist-123", ) store_unassigned = create_reporting_model( ShopifyStoreAssociation, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=None, ) response = handler.handle( _make_request( fake.uuid4_string(), order_by=["globalParticipantId.asc.nullsFirst"] ) ) items = response.items assert len(items) == 2 assert items[0].id == store_unassigned.id assert items[1].id == store_assigned.id @pytest.mark.db def test_order_by_global_participant_id_desc_puts_null_first( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account = Account(vendor_id=1, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account] ) store_assigned = create_reporting_model( ShopifyStoreAssociation, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id="artist-123", ) store_unassigned = create_reporting_model( ShopifyStoreAssociation, vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, global_participant_id=None, ) response = handler.handle( _make_request( fake.uuid4_string(), order_by=["globalParticipantId.desc.nullsFirst"] ) ) items = response.items assert len(items) == 2 assert items[0].id == store_unassigned.id assert items[1].id == store_assigned.id @pytest.mark.db def test_order_by_shop_domain_desc_returns_stores_in_reverse_order( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account = Account(vendor_id=1, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account] ) store_a = create_reporting_model( ShopifyStoreAssociation, shop_domain="a.myshopify.com", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) store_z = create_reporting_model( ShopifyStoreAssociation, shop_domain="z.myshopify.com", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = handler.handle( _make_request(fake.uuid4_string(), order_by=["shopDomain.desc"]) ) items = response.items assert len(items) == 2 assert items[0].id == store_z.id assert items[1].id == store_a.id @pytest.mark.db def test_order_by_shop_domain_asc_returns_stores_in_ascending_order( self, handler: GetStoresV2Handler, create_reporting_model: CreateReportingModel, auth_service_mock: mock.MagicMock, fake: FakerTyped, ) -> None: account = Account(vendor_id=1, subaccount_id=0) auth_service_mock.authorize_for_permission.return_value = AccountAccess( accounts=[account] ) store_z = create_reporting_model( ShopifyStoreAssociation, shop_domain="z.myshopify.com", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) store_a = create_reporting_model( ShopifyStoreAssociation, shop_domain="a.myshopify.com", vendor_id=account.vendor_id, subaccount_id=account.subaccount_id, ) response = handler.handle( _make_request(fake.uuid4_string(), order_by=["shopDomain.asc"]) ) items = response.items assert len(items) == 2 assert items[0].id == store_a.id assert items[1].id == store_z.id