import io import pathlib from hashlib import md5 from unittest import mock import pandas as pd import pytest from freezegun import freeze_time from app.adapters.ows_dmp import AudienceSharePlatform, AudienceShareStatus from app.adapters.tiktok.exceptions import TikTokClientError from app.exceptions import HandlerError from app.handler import ShareAudienceHandler, ShareAudienceRequest from app.strategies import TikTokAudienceShareStrategy from tests.unit.factories import ( AudienceShareFactory, TikTokAudienceFileFactory, TikTokAudienceShareFactory, ) def test_share_tiktok_audience_share_failed( handler: ShareAudienceHandler, ows_dmp_client_mock: mock.MagicMock, mock_download_fileobj_tiktok: None, ) -> None: audience_share = AudienceShareFactory.build(platform=AudienceSharePlatform.TIKTOK) ows_dmp_client_mock.update_audience_share.return_value = audience_share ows_dmp_client_mock.get_tiktok_audience_share.side_effect = Exception("error") with pytest.raises(HandlerError) as exc_info: handler.handle( ShareAudienceRequest( bucket="test", key=f"shares/{audience_share.id}/{audience_share.id}_audience.csv", ) ) assert exc_info.value.code == "GET_TIKTOK_AUDIENCE_SHARE_ERROR" ows_dmp_client_mock.update_audience_share.assert_has_calls( [ mock.call( audience_share.id, status=AudienceShareStatus.FAILED, processed_fans=None, ) ] ) @freeze_time("2023-11-03T12:09:52.902512") def test_share_tiktok_audience_upload_failed( handler: ShareAudienceHandler, ows_dmp_client_mock: mock.MagicMock, s3_client_mock: mock.MagicMock, kms_client_mock: mock.MagicMock, tiktok_client_mock: mock.MagicMock, ) -> None: audience_share = AudienceShareFactory.build( platform=AudienceSharePlatform.TIKTOK, ) share_data = TikTokAudienceShareFactory.build() ows_dmp_client_mock.update_audience_share.return_value = audience_share ows_dmp_client_mock.get_tiktok_audience_share.return_value = share_data kms_client_mock.decrypt.return_value = share_data.user_access_token tiktok_client_mock.upload_audience_file.side_effect = TikTokClientError("error") with pytest.raises(HandlerError) as exc_info: handler.handle( ShareAudienceRequest( bucket="test", key=f"shares/{audience_share.id}/{audience_share.id}_audience.csv", ) ) assert exc_info.value.code == "PROCESS_AUDIENCE_ERROR" ows_dmp_client_mock.update_audience_share.assert_has_calls( [ mock.call( audience_share.id, status=AudienceShareStatus.PROCESSING, processed_fans=None, ), mock.call( audience_share.id, status=AudienceShareStatus.FAILED, processed_fans=None, ), ] ) @freeze_time("2023-11-03T12:09:52.902512") def test_share_tiktok_audience_one_file_upload( handler: ShareAudienceHandler, ows_dmp_client_mock: mock.MagicMock, s3_client_mock: mock.MagicMock, kms_client_mock: mock.MagicMock, tiktok_client_mock: mock.MagicMock, mock_download_fileobj_tiktok: None, audience_file_tiktok: pathlib.Path, ) -> None: audience_share = AudienceShareFactory.build( platform=AudienceSharePlatform.TIKTOK, ) tiktok_audience_share = TikTokAudienceShareFactory.build(fansCount=10) ows_dmp_client_mock.update_audience_share.return_value = audience_share ows_dmp_client_mock.get_tiktok_audience_share.return_value = tiktok_audience_share kms_client_mock.decrypt.return_value = tiktok_audience_share.user_access_token handler.handle( ShareAudienceRequest( bucket="test", key=f"shares/{audience_share.id}/{audience_share.id}_audience.csv", ) ) ows_dmp_client_mock.update_audience_share.assert_has_calls( [ mock.call( audience_share.id, status=AudienceShareStatus.PROCESSING, processed_fans=None, ), mock.call( audience_share.id, status=AudienceShareStatus.COMPLETED, processed_fans=10, ), ], ) md5_file_signature = md5( io.BytesIO(open(audience_file_tiktok, "rb").read()).read() ).hexdigest() tiktok_client_mock.upload_audience_file.assert_has_calls( [ mock.call( access_token=tiktok_audience_share.user_access_token, ad_account_external_id=tiktok_audience_share.ad_account_external_id, file_signature=md5_file_signature, file=open(audience_file_tiktok, "rb").read(), ) ] ) @freeze_time("2023-11-03T12:09:52.902512") def test_share_tiktok_audience_upload_multiple_files( handler: ShareAudienceHandler, ows_dmp_client_mock: mock.MagicMock, s3_client_mock: mock.MagicMock, kms_client_mock: mock.MagicMock, tiktok_client_mock: mock.MagicMock, mock_download_fileobj_tiktok: None, audience_file_tiktok: pathlib.Path, tiktok_strategy: TikTokAudienceShareStrategy, ) -> None: tiktok_strategy.read_chunksize = 2 tiktok_strategy.max_upload_file_size = 600 audience_share = AudienceShareFactory.build( platform=AudienceSharePlatform.TIKTOK, ) tiktok_audience_share = TikTokAudienceShareFactory.build(fansCount=10) tiktok_audience_file1 = TikTokAudienceFileFactory.build() tiktok_audience_file2 = TikTokAudienceFileFactory.build() ows_dmp_client_mock.update_audience_share.return_value = audience_share ows_dmp_client_mock.get_tiktok_audience_share.return_value = tiktok_audience_share kms_client_mock.decrypt.return_value = tiktok_audience_share.user_access_token tiktok_client_mock.upload_audience_file.side_effect = [ tiktok_audience_file1, tiktok_audience_file2, ] handler.handle( ShareAudienceRequest( bucket="test", key=f"shares/{audience_share.id}/{audience_share.id}_audience.csv", ) ) ows_dmp_client_mock.update_audience_share.assert_has_calls( [ mock.call( audience_share.id, status=AudienceShareStatus.PROCESSING, processed_fans=None, ), mock.call( audience_share.id, status=AudienceShareStatus.COMPLETED, processed_fans=10, ), ], ) first_df = pd.DataFrame( { "email_SHA256": [ "hash1ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash2ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash3ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash4ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", None, "hash6ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", ], "phone_SHA256": [ None, "phone_hash28ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash38ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash48ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash58ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", None, ], } ) first_file_buffer = io.BytesIO() first_df.to_csv(first_file_buffer, index=False) first_file_buffer.seek(0) md5_file_signature_first_part = md5(first_file_buffer.getvalue()).hexdigest() first_file_buffer.seek(0) second_df = pd.DataFrame( { "email_SHA256": [ "hash7ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash8ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash9ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", None, ], "phone_SHA256": [ "phone_hash78ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash88ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash98ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash10ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", ], } ) second_file_buffer = io.BytesIO() second_df.to_csv(second_file_buffer, index=False) second_file_buffer.seek(0) md5_file_signature_second_part = md5(second_file_buffer.getvalue()).hexdigest() second_file_buffer.seek(0) tiktok_client_mock.upload_audience_file.assert_has_calls( [ mock.call( access_token=tiktok_audience_share.user_access_token, ad_account_external_id=tiktok_audience_share.ad_account_external_id, file_signature=md5_file_signature_first_part, file=first_file_buffer.getvalue(), ) ], [ mock.call( access_token=tiktok_audience_share.user_access_token, ad_account_external_id=tiktok_audience_share.ad_account_external_id, file_signature=md5_file_signature_second_part, file=second_file_buffer.getvalue(), ) ], ) tiktok_client_mock.create_custom_audience.assert_called_once_with( access_token=tiktok_audience_share.user_access_token, audience_name=tiktok_audience_share.audience_name, ad_account_external_id=tiktok_audience_share.ad_account_external_id, file_paths=[tiktok_audience_file1.file_path, tiktok_audience_file2.file_path], ) @freeze_time("2023-11-03T12:09:52.902512") def test_share_tiktok_audience_upload_file_join_chunks( handler: ShareAudienceHandler, ows_dmp_client_mock: mock.MagicMock, s3_client_mock: mock.MagicMock, kms_client_mock: mock.MagicMock, tiktok_client_mock: mock.MagicMock, mock_download_fileobj_tiktok: None, audience_file_tiktok: pathlib.Path, tiktok_strategy: TikTokAudienceShareStrategy, ) -> None: tiktok_strategy.read_chunksize = 2 audience_share = AudienceShareFactory.build( platform=AudienceSharePlatform.TIKTOK, ) tiktok_audience_share = TikTokAudienceShareFactory.build(fansCount=10) ows_dmp_client_mock.update_audience_share.return_value = audience_share ows_dmp_client_mock.get_tiktok_audience_share.return_value = tiktok_audience_share kms_client_mock.decrypt.return_value = tiktok_audience_share.user_access_token handler.handle( ShareAudienceRequest( bucket="test", key=f"shares/{audience_share.id}/{audience_share.id}_audience.csv", ) ) ows_dmp_client_mock.update_audience_share.assert_has_calls( [ mock.call( audience_share.id, status=AudienceShareStatus.PROCESSING, processed_fans=None, ), mock.call( audience_share.id, status=AudienceShareStatus.COMPLETED, processed_fans=10, ), ], ) first_df = pd.DataFrame( { "email_SHA256": [ "hash1ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash2ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash3ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash4ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", None, "hash6ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash7ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash8ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "hash9ecb91a8ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", None, ], "phone_SHA256": [ None, "phone_hash28ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash38ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash48ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash58ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", None, "phone_hash78ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash88ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash98ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", "phone_hash10ee7ae99210702b2d4758f30cdde3bf61e3d8e787d74681f90a6e", ], } ) first_file_buffer = io.BytesIO() first_df.to_csv(first_file_buffer, index=False) first_file_buffer.seek(0) md5_file_signature_first_part = md5(first_file_buffer.getvalue()).hexdigest() first_file_buffer.seek(0) tiktok_client_mock.upload_audience_file.assert_has_calls( [ mock.call( access_token=tiktok_audience_share.user_access_token, ad_account_external_id=tiktok_audience_share.ad_account_external_id, file_signature=md5_file_signature_first_part, file=first_file_buffer.getvalue(), ) ] )