from unittest import mock import pytest from anydi import Container from starlette.testclient import TestClient from dmp.adapters.fivetran.models import ConnectCard from dmp.app_connections.enums import AppConnectionStatus from dmp.fandata.models import ArtistMaxSpendDbt from dmp.shopify.dtos import ( ShopifyCollectionWithArtist, ShopifyStore, ShopifyStoreArtist, ) from dmp.shopify.handlers import ( ConnectStoreHandler, DeleteStoreHandler, GetCollectionArtistAssociationsHandler, GetCollectionArtistAssociationsResponse, GetStoreArtistsHandler, GetStoresHandler, GetStoresV2Handler, GetStoresV2Response, NotifyStoreSyncCompletedHandler, ReconnectStoreHandler, RefreshStoreHandler, SyncShopifyFivetranTablesStateHandler, UpdateCollectionArtistAssociationsHandler, ) from dmp.shopify.handlers.get_stores_v2 import GetStoresV2Request from dmp.shopify.models import ShopifyStoreAssociation from tests.unit.equals import IsISODatetimeOrNone from tests.unit.faker import FakerTyped from tests.unit.types import BuildModel, CreateReportingModel @pytest.mark.parametrize( "shop_domain", [ "example.com", "example.myshopify.com", "example.myshopify.com/merch/vinyl-123", "https://example.myshopify.com", "tps://example.myshopify.com", ], ) def test_connect_shopify_store( shop_domain: str, client: TestClient, container: Container, fake: FakerTyped, build_model: BuildModel, ) -> None: association = build_model(ShopifyStoreAssociation) connect_card = build_model(ConnectCard) handler_mock = mock.MagicMock(spec=ConnectStoreHandler) handler_mock.handle.return_value = association, connect_card with container.override(ConnectStoreHandler, handler_mock): response = client.post( "/shopify/stores/connection", json={ "vendorId": fake.integer(), "subaccountId": fake.integer(), "globalParticipantId": fake.uuid4_string(), "shopDomain": shop_domain, "redirectUri": fake.url(), }, ) assert response.status_code == 200 assert response.json() == { "storeId": association.id, "connectCardUri": connect_card.uri, } @pytest.mark.parametrize( "shop_domain", [ "oneword", "two words", "unknown.domain", "https://127.0.0.1/", ], ) def test_connect_shopify_store_invalid_shop_domain( shop_domain: str, client: TestClient, container: Container, fake: FakerTyped, build_model: BuildModel, ) -> None: association = build_model(ShopifyStoreAssociation) connect_card = build_model(ConnectCard) handler_mock = mock.MagicMock(spec=ConnectStoreHandler) handler_mock.handle.return_value = association, connect_card with container.override(ConnectStoreHandler, handler_mock): response = client.post( "/shopify/stores/connection", json={ "vendorId": fake.integer(), "subaccountId": fake.integer(), "globalParticipantId": fake.uuid4_string(), "shopDomain": shop_domain, "redirectUri": fake.url(), }, ) assert response.status_code == 422 def test_get_shopify_stores( client: TestClient, container: Container, build_model: BuildModel, ) -> None: store = build_model( ShopifyStore, status=AppConnectionStatus.CONNECTED, shop_domain="example.myshopify.com", ) handler_mock = mock.MagicMock(GetStoresHandler) handler_mock.handle.return_value = [store] with container.override(GetStoresHandler, handler_mock): response = client.get("/shopify/stores") assert response.status_code == 200 assert response.json() == [ { "id": store.id, "name": store.name, "shopDomain": store.shop_domain, "customDomain": store.custom_domain, "status": store.status, "vendorId": store.vendor_id, "subaccountId": store.subaccount_id, "globalParticipantId": store.global_participant_id, "collectionsCount": store.collections_count, "productsCount": store.products_count, "artistsCount": store.artists_count, "syncedAt": IsISODatetimeOrNone(store.synced_at), } ] def test_refresh_shopify_store( client: TestClient, container: Container, build_model: BuildModel, ) -> None: association = build_model( ShopifyStoreAssociation, status=AppConnectionStatus.CONNECTED, shop_domain="example.myshopify.com", ) handler_mock = mock.MagicMock(spec=RefreshStoreHandler) handler_mock.handle.return_value = association with container.override(RefreshStoreHandler, handler_mock): response = client.put( "/shopify/stores/refresh", json={"fivetranConnectorId": association.fivetran_connector_id}, ) assert response.status_code == 204 def test_delete_shopify_store( client: TestClient, container: Container, fake: FakerTyped, ) -> None: handler_mock = mock.MagicMock(spec=DeleteStoreHandler) handler_mock.handle.return_value = None with container.override(DeleteStoreHandler, handler_mock): response = client.delete( f"/shopify/stores/{fake.uuid4_string()}", ) assert response.status_code == 204 def test_reconnect_shopify_store( client: TestClient, container: Container, build_model: BuildModel, fake: FakerTyped, ) -> None: association = build_model(ShopifyStoreAssociation) connect_card = build_model(ConnectCard) handler_mock = mock.MagicMock(spec=ReconnectStoreHandler) handler_mock.handle.return_value = association, connect_card with container.override(ReconnectStoreHandler, handler_mock): response = client.post( f"/shopify/stores/{association.id}/reconnect", json={"redirectUri": fake.url()}, ) assert response.status_code == 200 assert response.json() == { "storeId": association.id, "connectCardUri": connect_card.uri, } def test_get_shopify_store_collections( client: TestClient, container: Container, build_model: BuildModel, fake: FakerTyped, ) -> None: store = build_model(ShopifyStoreAssociation) collection_global_participant_id = fake.uuid4_string() collection = build_model( ShopifyCollectionWithArtist, global_participant_id=collection_global_participant_id, ) handler_mock = mock.MagicMock(spec=GetCollectionArtistAssociationsHandler) handler_mock.handle.return_value = GetCollectionArtistAssociationsResponse( vendor_id=store.vendor_id, subaccount_id=store.subaccount_id, whole_store_products_count=store.products_count, shop_domain=store.shop_domain, whole_store_global_participant_id=None, collections=[collection], ) with container.override(GetCollectionArtistAssociationsHandler, handler_mock): response = client.get( f"/shopify/stores/{store.id}/collections", ) assert response.status_code == 200 assert response.json() == { "vendorId": store.vendor_id, "subaccountId": store.subaccount_id, "wholeStoreProductsCount": store.products_count, "shopDomain": store.shop_domain, "wholeStoreGlobalParticipantId": None, "collections": [ { "id": collection.id, "title": collection.title, "productsCount": collection.products_count, "globalParticipantId": collection_global_participant_id, } ], } def test_update_shopify_store_collections( client: TestClient, container: Container, build_model: BuildModel, fake: FakerTyped, ) -> None: store = build_model(ShopifyStore, artists_count=1) collection = build_model( ShopifyCollectionWithArtist, global_participant_id=fake.uuid4_string(), ) handler_mock = mock.MagicMock(spec=UpdateCollectionArtistAssociationsHandler) handler_mock.handle.return_value = store with container.override(UpdateCollectionArtistAssociationsHandler, handler_mock): response = client.put( f"/shopify/stores/{store.id}/collections", json={ "wholeStoreGlobalParticipantId": None, "collections": [ { "collectionId": collection.id, "globalParticipantId": collection.global_participant_id, } ], }, ) assert response.status_code == 200 assert response.json() == { "id": store.id, "name": store.name, "shopDomain": store.shop_domain, "customDomain": store.custom_domain, "status": store.status, "vendorId": store.vendor_id, "subaccountId": store.subaccount_id, "globalParticipantId": store.global_participant_id, "collectionsCount": store.collections_count, "productsCount": store.products_count, "artistsCount": store.artists_count, "syncedAt": IsISODatetimeOrNone(store.synced_at), } def test_update_shopify_store_collections_whole_store( client: TestClient, container: Container, build_model: BuildModel, fake: FakerTyped, ) -> None: store = build_model( ShopifyStore, status=AppConnectionStatus.CONNECTED, artists_count=1 ) global_participant_id = fake.uuid4_string() handler_mock = mock.MagicMock(spec=UpdateCollectionArtistAssociationsHandler) handler_mock.handle.return_value = store with container.override(UpdateCollectionArtistAssociationsHandler, handler_mock): response = client.put( f"/shopify/stores/{store.id}/collections", json={ "wholeStoreGlobalParticipantId": global_participant_id, "collections": [], }, ) assert response.status_code == 200 assert response.json() == { "id": store.id, "name": store.name, "shopDomain": store.shop_domain, "customDomain": store.custom_domain, "status": store.status, "vendorId": store.vendor_id, "subaccountId": store.subaccount_id, "globalParticipantId": store.global_participant_id, "collectionsCount": store.collections_count, "productsCount": store.products_count, "artistsCount": store.artists_count, "syncedAt": IsISODatetimeOrNone(store.synced_at), } def test_get_shopify_store_artists( client: TestClient, container: Container, build_model: BuildModel, fake: FakerTyped, ) -> None: store = build_model( ShopifyStoreAssociation, status=AppConnectionStatus.CONNECTED, global_participant_id=None, ) store_artists = [ ShopifyStoreArtist( global_participant_id=fake.uuid4_string(), collections_count=2, products_count=10, ) ] handler_mock = mock.MagicMock(spec=GetStoreArtistsHandler) handler_mock.handle.return_value = store_artists with container.override(GetStoreArtistsHandler, handler_mock): response = client.get(f"/shopify/stores/{store.id}/artists") assert response.status_code == 200 assert response.json() == [ { "globalParticipantId": store_artists[0].global_participant_id, "collectionsCount": store_artists[0].collections_count, "productsCount": store_artists[0].products_count, } ] def test_handle_shopify_transformation_success( client: TestClient, container: Container ) -> None: handler_mock = mock.MagicMock(spec=NotifyStoreSyncCompletedHandler) handler_mock.handle.return_value = None with container.override(NotifyStoreSyncCompletedHandler, handler_mock): response = client.post( "/shopify/handle-transformation-success", ) assert response.status_code == 204 def test_sync_fivetran_tables_state( client: TestClient, fake: FakerTyped, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=SyncShopifyFivetranTablesStateHandler) handler_mock.handle.return_value = [] with container.override(SyncShopifyFivetranTablesStateHandler, handler_mock): response = client.post( "/shopify/sync-fivetran-tables-state", json={ "fivetranConnectorId": fake.pystr(), }, ) assert response.status_code == 200 def test_sync_fivetran_tables_state_all_connections( client: TestClient, container: Container ) -> None: handler_mock = mock.MagicMock(spec=SyncShopifyFivetranTablesStateHandler) handler_mock.handle.return_value = [] with container.override(SyncShopifyFivetranTablesStateHandler, handler_mock): response = client.post( "/shopify/sync-fivetran-tables-state", json={ "fivetranConnectorId": None, }, ) assert response.status_code == 200 @pytest.mark.db def test_get_shopify_max_spend( client: TestClient, fake: FakerTyped, create_reporting_model: CreateReportingModel, ) -> None: vendor_id = fake.integer() subaccount_id = fake.integer() global_participant_id_1 = fake.uuid4_string() global_participant_id_2 = fake.uuid4_string() create_reporting_model( ArtistMaxSpendDbt, global_participant_id=global_participant_id_1, vendor_id=vendor_id, subaccount_id=subaccount_id, max_spend=100.20, ) create_reporting_model( ArtistMaxSpendDbt, global_participant_id=global_participant_id_2, vendor_id=vendor_id, subaccount_id=subaccount_id, max_spend=10.50, ) response = client.get( "/shopify/max-spend", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "globalParticipantIds": [ global_participant_id_1, global_participant_id_2, ], }, ) assert response.status_code == 200 assert response.json() == {"maxSpend": 100.2} def test_get_shopify_stores_v2( client: TestClient, container: Container, build_model: BuildModel, ) -> None: store_1 = build_model( ShopifyStore, status=AppConnectionStatus.CONNECTED, shop_domain="example.myshopify.com", ) store_2 = build_model( ShopifyStore, status=AppConnectionStatus.CONNECTED, shop_domain="other.myshopify.com", ) handler_mock = mock.MagicMock(spec=GetStoresV2Handler) handler_mock.handle.return_value = GetStoresV2Response( total=2, items=[store_1, store_2], ) with container.override(GetStoresV2Handler, handler_mock): response = client.get("/shopify/stores-v2") assert response.status_code == 200 assert response.json() == { "total": 2, "limit": 10, "offset": 0, "items": [ { "id": store_1.id, "name": store_1.name, "shopDomain": store_1.shop_domain, "customDomain": store_1.custom_domain, "status": store_1.status, "vendorId": store_1.vendor_id, "subaccountId": store_1.subaccount_id, "globalParticipantId": store_1.global_participant_id, "collectionsCount": store_1.collections_count, "productsCount": store_1.products_count, "artistsCount": store_1.artists_count, "syncedAt": IsISODatetimeOrNone(store_1.synced_at), }, { "id": store_2.id, "name": store_2.name, "shopDomain": store_2.shop_domain, "customDomain": store_2.custom_domain, "status": store_2.status, "vendorId": store_2.vendor_id, "subaccountId": store_2.subaccount_id, "globalParticipantId": store_2.global_participant_id, "collectionsCount": store_2.collections_count, "productsCount": store_2.products_count, "artistsCount": store_2.artists_count, "syncedAt": IsISODatetimeOrNone(store_2.synced_at), }, ], } def test_get_shopify_stores_v2_with_filters( client: TestClient, container: Container, build_model: BuildModel, fake: FakerTyped, ) -> None: store = build_model( ShopifyStore, status=AppConnectionStatus.CONNECTED, shop_domain="example.myshopify.com", ) vendor_id = fake.integer() subaccount_id = fake.integer() handler_mock = mock.MagicMock(spec=GetStoresV2Handler) handler_mock.handle.return_value = GetStoresV2Response( total=1, items=[store], ) with container.override(GetStoresV2Handler, handler_mock): response = client.get( "/shopify/stores-v2", params={ "vendorId": vendor_id, "subaccountId": subaccount_id, "limit": 5, "offset": 10, }, ) assert response.status_code == 200 body = response.json() assert body["total"] == 1 assert body["limit"] == 5 assert body["offset"] == 10 assert len(body["items"]) == 1 call_request = handler_mock.handle.call_args[0][0] assert call_request.vendor_id == vendor_id assert call_request.subaccount_id == subaccount_id assert call_request.limit == 5 assert call_request.offset == 10 def test_get_shopify_stores_v2_defaults_limit_and_offset( client: TestClient, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=GetStoresV2Handler) handler_mock.handle.return_value = GetStoresV2Response(total=0, items=[]) with container.override(GetStoresV2Handler, handler_mock): response = client.get("/shopify/stores-v2") assert response.status_code == 200 body = response.json() assert body["limit"] == 10 assert body["offset"] == 0 call_request = handler_mock.handle.call_args[0][0] assert call_request.limit == 10 assert call_request.offset == 0 assert call_request.vendor_id is None assert call_request.subaccount_id is None def test_get_shopify_stores_v2_with_order_by( client: TestClient, container: Container, build_model: BuildModel, ) -> None: store = build_model(ShopifyStore) handler_mock = mock.MagicMock(spec=GetStoresV2Handler) handler_mock.handle.return_value = GetStoresV2Response(total=1, items=[store]) with container.override(GetStoresV2Handler, handler_mock): response = client.get( "/shopify/stores-v2", params={"orderBy": ["shopDomain.asc", "name.desc"]}, ) assert response.status_code == 200 call_request = handler_mock.handle.call_args[0][0] assert call_request.order_by == ["shopDomain.asc", "name.desc"] def test_get_shopify_stores_v2_defaults_order_by( client: TestClient, container: Container, ) -> None: handler_mock = mock.MagicMock(spec=GetStoresV2Handler) handler_mock.handle.return_value = GetStoresV2Response(total=0, items=[]) with container.override(GetStoresV2Handler, handler_mock): response = client.get("/shopify/stores-v2") assert response.status_code == 200 call_request = handler_mock.handle.call_args[0][0] assert call_request.order_by == GetStoresV2Request.DEFAULT_ORDER_BY