from collections.abc import Iterator import httpx import pytest import respx from faker import Faker from app.adapters.google import GoogleClient from app.adapters.google.exceptions import GoogleClientError from tests.unit.adapters.google.factories import ( GoogleUploadUsersResponseFactory, OfflineJobOperationFactory, UserDataOfflineJobFactory, ) class TestGoogleClient: @pytest.fixture def client(self, faker: Faker) -> Iterator[GoogleClient]: with GoogleClient(developer_token=faker.pystr()) as client: yield client def test_create_offline_user_data_job_with_login_customer_id( self, client: GoogleClient, respx_mock: respx.MockRouter, faker: Faker, ) -> None: login_customer_id = str(faker.pyint()) customer_id = faker.pystr() user_list_resource = faker.pystr() access_token = faker.pystr() response = UserDataOfflineJobFactory.build() respx_mock.post( f"https://googleads.googleapis.com/v22/customers/{customer_id}/offlineUserDataJobs:create", json={ "job": { "type": "CUSTOMER_MATCH_USER_LIST", "customerMatchUserListMetadata": { "userList": user_list_resource, "consent": { "adUserData": "GRANTED", "adPersonalization": "GRANTED", }, }, } }, headers={ "Authorization": f"Bearer {access_token}", "developer-token": client.developer_token, "Content-Type": "application/json", "login-customer-id": login_customer_id, }, ).mock( return_value=httpx.Response( status_code=200, json=response.model_dump(by_alias=True), ) ) result = client.create_offline_user_data_job( access_token=access_token, customer_id=customer_id, user_list_resource=user_list_resource, login_customer_id=login_customer_id, ) assert result == response def test_create_offline_user_data_job_without_login_customer_id( self, client: GoogleClient, respx_mock: respx.MockRouter, faker: Faker, ) -> None: login_customer_id = None customer_id = faker.pystr() user_list_resource = faker.pystr() access_token = faker.pystr() response = UserDataOfflineJobFactory.build() respx_mock.post( f"https://googleads.googleapis.com/v22/customers/{customer_id}/offlineUserDataJobs:create", json={ "job": { "type": "CUSTOMER_MATCH_USER_LIST", "customerMatchUserListMetadata": { "userList": user_list_resource, "consent": { "adUserData": "GRANTED", "adPersonalization": "GRANTED", }, }, } }, headers={ "Authorization": f"Bearer {access_token}", "developer-token": client.developer_token, "Content-Type": "application/json", }, ).mock( return_value=httpx.Response( status_code=200, json=response.model_dump(by_alias=True), ) ) result = client.create_offline_user_data_job( access_token=access_token, customer_id=customer_id, user_list_resource=user_list_resource, login_customer_id=login_customer_id, ) assert result == response def test_add_to_offline_user_data_job_with_login_customer_id( self, client: GoogleClient, respx_mock: respx.MockRouter, faker: Faker, ) -> None: login_customer_id = str(faker.pyint()) offline_job_resource = faker.pystr() access_token = faker.pystr() payload: list[list[str]] = [ [faker.pystr(), faker.pystr()], [faker.pystr(), ""], ["", faker.pystr()], ] response = GoogleUploadUsersResponseFactory.build() respx_mock.post( f"https://googleads.googleapis.com/v22/{offline_job_resource}:addOperations", json={ "operations": [ { "create": { "userIdentifiers": [ { "hashedEmail": payload[0][0], }, { "hashedPhoneNumber": payload[0][1], }, ] }, }, { "create": { "userIdentifiers": [ { "hashedEmail": payload[1][0], } ] }, }, { "create": { "userIdentifiers": [ { "hashedPhoneNumber": payload[2][1], } ] }, }, ], }, headers={ "Authorization": f"Bearer {access_token}", "developer-token": client.developer_token, "Content-Type": "application/json", "login-customer-id": login_customer_id, }, ).mock( return_value=httpx.Response( status_code=200, json=response.model_dump(by_alias=True), ) ) result = client.add_to_offline_user_data_job( access_token=access_token, offline_job_resource=offline_job_resource, payload=payload, login_customer_id=login_customer_id, ) assert result == response def test_add_to_offline_user_data_job_without_login_customer_id( self, client: GoogleClient, respx_mock: respx.MockRouter, faker: Faker, ) -> None: login_customer_id = None offline_job_resource = faker.pystr() access_token = faker.pystr() payload: list[list[str]] = [ [faker.pystr(), faker.pystr()], [faker.pystr(), ""], ["", faker.pystr()], ] response = GoogleUploadUsersResponseFactory.build() respx_mock.post( f"https://googleads.googleapis.com/v22/{offline_job_resource}:addOperations", json={ "operations": [ { "create": { "userIdentifiers": [ { "hashedEmail": payload[0][0], }, { "hashedPhoneNumber": payload[0][1], }, ] }, }, { "create": { "userIdentifiers": [ { "hashedEmail": payload[1][0], } ] }, }, { "create": { "userIdentifiers": [ { "hashedPhoneNumber": payload[2][1], } ] }, }, ], }, headers={ "Authorization": f"Bearer {access_token}", "developer-token": client.developer_token, "Content-Type": "application/json", }, ).mock( return_value=httpx.Response( status_code=200, json=response.model_dump(by_alias=True), ) ) result = client.add_to_offline_user_data_job( access_token=access_token, offline_job_resource=offline_job_resource, payload=payload, login_customer_id=login_customer_id, ) assert result == response def test_start_offline_user_data_job_with_login_customer_id( self, client: GoogleClient, respx_mock: respx.MockRouter, faker: Faker, ) -> None: login_customer_id = str(faker.pyint()) offline_job_resource = faker.pystr() access_token = faker.pystr() response = OfflineJobOperationFactory.build() respx_mock.post( f"https://googleads.googleapis.com/v22/{offline_job_resource}:run", json={"validateOnly": False}, headers={ "Authorization": f"Bearer {access_token}", "developer-token": client.developer_token, "Content-Type": "application/json", "login-customer-id": login_customer_id, }, ).mock( return_value=httpx.Response( status_code=200, json=response.model_dump(by_alias=True), ) ) result = client.start_offline_user_data_job( access_token=access_token, offline_job_resource=offline_job_resource, login_customer_id=login_customer_id, ) assert result == response def test_start_offline_user_data_job_without_login_customer_id( self, client: GoogleClient, respx_mock: respx.MockRouter, faker: Faker, ) -> None: login_customer_id = None offline_job_resource = faker.pystr() access_token = faker.pystr() response = OfflineJobOperationFactory.build() respx_mock.post( f"https://googleads.googleapis.com/v22/{offline_job_resource}:run", json={"validateOnly": False}, headers={ "Authorization": f"Bearer {access_token}", "developer-token": client.developer_token, "Content-Type": "application/json", }, ).mock( return_value=httpx.Response( status_code=200, json=response.model_dump(by_alias=True), ) ) result = client.start_offline_user_data_job( access_token=access_token, offline_job_resource=offline_job_resource, login_customer_id=login_customer_id, ) assert result == response def test_error_response( self, client: GoogleClient, respx_mock: respx.MockRouter, faker: Faker, ) -> None: offline_job_resource = faker.pystr() access_token = faker.pystr() response_error_data = { "@type": "type.googleapis.com/google.ads.googleads.v22.errors.GoogleAdsFailure", "errors": [ { "errorCode": {"databaseError": "CONCURRENT_MODIFICATION"}, "message": ( "Multiple requests were attempting to modify the same " "resource at once. Retry the request." ), } ], "requestId": "md1QLg-AGENV5NjETAMQZA", } respx_mock.post( f"https://googleads.googleapis.com/v22/{offline_job_resource}:run", json={"validateOnly": False}, headers={ "Authorization": f"Bearer {access_token}", "developer-token": client.developer_token, "Content-Type": "application/json", }, ).mock( return_value=httpx.Response( status_code=200, json=response_error_data, ) ) with pytest.raises(GoogleClientError) as exc_info: client.start_offline_user_data_job( access_token=access_token, offline_job_resource=offline_job_resource, login_customer_id=None, ) assert exc_info.value.log_extra == { "google_client_request_method": "POST", "google_client_request_path": f"/v22/{offline_job_resource}:run", "google_client_response_status_code": 200, "google_client_response_error_data": response_error_data, }