
    iІ                     
   d Z ddlZddlmZ ddlmZmZ ddlZddlm	Z	 ej                  j                  d e ee      j                  j                                ed      5   ed      5  ddlmZ ddd       ddd       d	efd
Zej$                  d	efd       Z G d d      Z G d d      Z G d d      Z G d d      Z G d d      Z G d d      Z G d d      Z G d d      Z G d d      Z G d d      Z G d  d!      Z G d" d#      Z G d$ d%      Z  G d& d'      Z! G d( d)      Z" G d* d+      Z# G d, d-      Z$ G d. d/      Z%e&d0k(  r ejN                  ed1g       yy# 1 sw Y   xY w# 1 sw Y   xY w)2u*  
utils/meta_ads_client.py - MetaAdsClient 단위 테스트

테스트 항목:
- 초기화: 환경변수 정상/누락 시 동작
- 토큰 관리: exchange_token, check_token, update_env_token
- 캠페인 CRUD: list, create, get, update, delete
- 광고세트 CRUD: list, create, update, delete
- 크리에이티브: upload_image, create_creative
- 인사이트: get_insights (campaign/adset/ad), 잘못된 타입 ValueError

모든 외부 호출(requests.get, Facebook SDK 메서드)은 Mock으로 대체하여
실제 Meta API를 호출하지 않는다.
    N)Path)	MagicMockpatch)	HTTPError#utils.meta_ads_client.load_env_keys)utils.meta_ads_client.FacebookAdsApi.init)MetaAdsClientreturnc                 H   | j                  dd       | j                  dd       | j                  dd       | j                  dd       t        d	      5  t        d
      5  t               }ddd       ddd       t               _        |S # 1 sw Y   "xY w# 1 sw Y   &xY w)u   테스트용 환경변수를 주입하고, SDK API 연결 없이 MetaAdsClient를 생성한다.

    _account는 MagicMock으로 교체하여 AdAccount SDK 호출을 모두 차단한다.
    META_APP_IDtest_app_idMETA_APP_SECRETtest_app_secretMETA_ACCESS_TOKENtest_access_tokenMETA_AD_ACCOUNT_IDact_123456789r   utils.meta_ads_client.AdAccountN)setenvr   r	   r   _account)monkeypatchcs     K/home/jay/workspace/.worktrees/task-2116-dev1/tests/test_meta_ads_client.py_make_clientr   &   s    
 }m4(*;<*,?@+_=	:	; UCd=e O  AJH   s$   B B+BB	BB!c                     t        |       S )uL   각 테스트에서 재사용할 MetaAdsClient 인스턴스를 제공한다.)r   )r   s    r   clientr   8   s     $$    c                   "    e Zd ZdZd Zd Zd Zy)TestMetaAdsClientInitu.   MetaAdsClient.__init__ 초기화 동작 검증c                    |j                  dd       |j                  dd       |j                  dd       |j                  dd       t        d	      5 }t        d
      5  t               }ddd       ddd       j                  dk(  sJ |j                  dk(  sJ |j
                  dk(  sJ |j                  dk(  sJ j                  ddd       y# 1 sw Y   ixY w# 1 sw Y   mxY w)uq   필수 환경변수가 모두 설정된 경우 예외 없이 초기화되고 속성값이 정확히 설정된다.r   r   r   r   r   r   r   r   r   r   N)r   r   r	   _app_id_app_secret_access_token_ad_account_idassert_called_once_with)selfr   	mock_initr   s       r   #test_init_success_with_all_env_varsz9TestMetaAdsClientInit.test_init_success_with_all_env_varsF   s    =-8,.?@.0CD/A>? 	 9eTuNv 	 A	  	  yyM)))}} 1111"5555?222))-9JL_`	  	  	  	 s$   C C+CC	CC(c                    |j                  dd       |j                  dd       |j                  dd       |j                  dd	       t        d
      5  t        d      5  t        j                  t
        d      5  t                ddd       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   xY w# 1 sw Y   yxY w)ub   META_ACCESS_TOKEN이 없으면 ValueError가 발생하고 메시지에 키 이름이 포함된다.r   r   r   r   r   Fraisingr   r   r   r   matchN)r   delenvr   pytestraises
ValueErrorr	   )r&   r   s     r   6test_init_raises_value_error_when_access_token_missingzLTestMetaAdsClientInit.test_init_raises_value_error_when_access_token_missingV   s    =-8,.?@.>/A 89 	 5Al;m 	 z1DE   	  	  	    	  	  	  	 s<   B9!B-=B!B-B9!B*&B--B6	2B99Cc                 (   dD ]  }|j                  |d        t        d      5  t        d      5  t        j                  t              5  t                ddd       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   xY w# 1 sw Y   yxY w)uA   여러 필수 키가 모두 없으면 ValueError가 발생한다.)r   r   r   r   Fr*   r   r   N)r.   r   r/   r0   r1   r	   )r&   r   keys      r   7test_init_raises_value_error_when_multiple_keys_missingzMTestMetaAdsClientInit.test_init_raises_value_error_when_multiple_keys_missingb   s    ` 	3CsE2	3 89 	 5Al;m 	 z*   	  	  	    	  	  	  	 s:   BA<A0A<B0A95A<<B	BBN)__name__
__module____qualname____doc__r(   r2   r5    r   r   r   r   C   s    8a 
  r   r   c                       e Zd ZdZd Zd Zy)
TestToDictu'   _to_dict 정적 메서드 동작 검증c                     t               }ddd|j                  _        |j                  |      }|dddk(  sJ |j                  j	                          y)uG   export_all_data()를 가진 객체는 해당 메서드로 변환된다.123testidnameN)r   export_all_datareturn_value_to_dictassert_called_once)r&   r   objresults       r   0test_to_dict_uses_export_all_data_when_availablez;TestToDict.test_to_dict_uses_export_all_data_when_availablet   sO    k27+H(%v6666..0r   c                 <    |j                  ddi      }|ddik(  sJ y)u=   export_all_data()가 없는 객체는 dict()로 변환된다.r4   valueN)rE   )r&   r   rH   s      r   *test_to_dict_falls_back_to_dict_conversionz5TestToDict.test_to_dict_falls_back_to_dict_conversion~   s(    %!12%))))r   N)r6   r7   r8   r9   rI   rL   r:   r   r   r<   r<   q   s    11*r   r<   c                   "    e Zd ZdZd Zd Zd Zy)TestExchangeTokenu=   exchange_token: 단기 → 장기 토큰 교환 동작 검증c                 x   t               }ddd|j                  _        t        d|      5 }t        d      5  |j	                         }ddd       ddd       dk(  sJ |j
                  dk(  sJ |j                  j                          j                  d   d   }d	|v sJ y# 1 sw Y   \xY w# 1 sw Y   `xY w)
uc   Graph API가 access_token을 응답하면 새 토큰을 반환하고 내부 상태를 갱신한다.new_long_lived_tokenbearer)access_token
token_type"utils.meta_ads_client.requests.getrD   r   Nr   zoauth/access_token)	r   jsonrD   r   exchange_tokenr#   raise_for_statusrF   	call_argsr&   r   	mock_respmock_getrH   
called_urls         r   0test_exchange_token_returns_new_token_on_successzBTestExchangeToken.test_exchange_token_returns_new_token_on_success   s    K	2"'
	# 6YO	-S[=>	- **,F		- 	- ////##'====""557''*1-
#z111	- 	- 	- 	-s"   B0B$	B0$B-	)B00B9c                    t               }t        d      |j                  _        t	        d|      5  t        j                  t              5  |j                          ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)uB   HTTP 오류 발생 시 raise_for_status가 예외를 전파한다.z401 UnauthorizedrT   rU   N)r   r   rX   side_effectr   r/   r0   rW   r&   r   r[   s      r   (test_exchange_token_raises_on_http_errorz:TestExchangeToken.test_exchange_token_raises_on_http_error   so    K	1:;M1N	"".7iP 	(y) (%%'(	( 	(( (	( 	(s#   A:A.A:.A7	3A::Bc                    t               }dddii|j                  _        t        d|      5  t	        j
                  t        d      5  |j                          ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)	uH   응답 JSON에 access_token 키가 없으면 ValueError가 발생한다.errormessagezInvalid tokenrT   rU   rR   r,   N)r   rV   rD   r   r/   r0   r1   rW   ra   s      r   Gtest_exchange_token_raises_value_error_when_no_access_token_in_responsezYTestExchangeToken.test_exchange_token_raises_value_error_when_no_access_token_in_response   ss    K	'.O0L&M	#7iP 	(z@ (%%'(	( 	(( (	( 	(s#   A7	A+A7+A4	0A77B N)r6   r7   r8   r9   r^   rb   rf   r:   r   r   rN   rN      s    G2(((r   rN   c                       e Zd ZdZd Zd Zy)TestCheckTokenu=   check_token: debug_token 엔드포인트 호출 동작 검증c                 8   t               }ddddgdi|j                  _        t        d|      5 }|j	                         }ddd       d	   du sJ |d
   dgk(  sJ |j
                  j                          j                  d   d   }d|v sJ y# 1 sw Y   PxY w)u2   debug_token 응답의 data 필드를 반환한다.dataTl   c(	 ads_management)is_valid
expires_atscopesrT   rU   Nrl   rn   r   debug_token)r   rV   rD   r   check_tokenrX   rF   rY   rZ   s         r   #test_check_token_returns_data_fieldz2TestCheckToken.test_check_token_returns_data_field   s    K	 (+,'
	# 7iP 	*T\'')F	* j!T)))h$4#5555""557''*1-

***	* 	*s   BBc                     t               }ddi|j                  _        t        d|      5  |j	                         }ddd       ddik(  sJ y# 1 sw Y   xY w)u>   응답에 data 키가 없으면 응답 전체를 반환한다.rl   FrT   rU   N)r   rV   rD   r   rp   )r&   r   r[   rH   s       r   6test_check_token_returns_raw_response_when_no_data_keyzETestCheckToken.test_check_token_returns_raw_response_when_no_data_key   s]    K	'15&9	#7iP 	*'')F	* *e,,,,	* 	*s   AAN)r6   r7   r8   r9   rq   rs   r:   r   r   rh   rh      s    G+(-r   rh   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestUpdateEnvTokenuN   update_env_token: .env.keys 파일의 토큰 라인 업데이트 동작 검증c                     |dz  }|j                  d       t        |      |_        |j                  d       |j	                         }d|v sJ d|vsJ d|v sJ |j
                  dk(  sJ y)uQ   'export META_ACCESS_TOKEN=...' 형식의 라인을 새 토큰으로 교체한다.	.env.keysz[export META_APP_ID=app_id
export META_ACCESS_TOKEN=old_token
export META_APP_SECRET=secret
brand_new_token	old_tokenzMETA_APP_ID=app_idN)
write_textstr_env_keys_pathupdate_env_token	read_textr#   r&   r   tmp_pathenv_filecontents        r   1test_update_env_token_replaces_export_prefix_linezDTestUpdateEnvToken.test_update_env_token_replaces_export_prefix_line   s    k)l	
 !$H 12$$& G+++')))#w...##'8888r   c                     |dz  }|j                  d       t        |      |_        |j                  d       |j	                         }d|v sJ d|vsJ y)uI   'META_ACCESS_TOKEN=...' (export 없음) 형식의 라인도 교체한다.rw   z"META_ACCESS_TOKEN=old_plain_token
new_plain_tokenold_plain_tokenNrz   r{   r|   r}   r~   r   s        r   0test_update_env_token_replaces_plain_prefix_linezCTestUpdateEnvToken.test_update_env_token_replaces_plain_prefix_line   s`    k)AB #H 12$$& G+++ ///r   c                     |dz  }|j                  d       t        |      |_        |j                  d       |j	                         }d|v sJ y)uK   파일에 META_ACCESS_TOKEN 라인이 없으면 파일 끝에 추가한다.rw   zexport META_APP_ID=app_id
appended_tokenNr   r   s        r   1test_update_env_token_appends_when_line_not_foundzDTestUpdateEnvToken.test_update_env_token_appends_when_line_not_found   sR    k)9: #H 01$$&7***r   c                 L    t        |dz        |_        |j                  d       y)uC   파일이 존재하지 않으면 예외 없이 조기 반환한다.znonexistent.env.keys
some_tokenN)r{   r|   r}   )r&   r   r   s      r   7test_update_env_token_does_nothing_when_file_not_existszJTestUpdateEnvToken.test_update_env_token_does_nothing_when_file_not_exists   s%     #H/E$E F 	-r   N)r6   r7   r8   r9   r   r   r   r   r:   r   r   ru   ru      s    X9 
0	+.r   ru   c                       e Zd ZdZd Zy)TestGetAccountInfouA   get_account_info: AdAccount.api_get 호출 및 dict 변환 검증c                 
   t               }ddddd|j                  _        ||j                  j                  _        |j                         }|d   dk(  sJ |d   dk(  sJ |j                  j                  j                          y)	u4   api_get 결과를 dict로 변환하여 반환한다.r   zTest Account   KRW)rA   rB   account_statuscurrencyrB   r   N)r   rC   rD   r   api_getget_account_inforF   )r&   r   	mock_datarH   s       r   "test_get_account_info_returns_dictz5TestGetAccountInfo.test_get_account_info_returns_dict  s    K	!"	2
	!!. 09,((*f~///j!U***224r   N)r6   r7   r8   r9   r   r:   r   r   r   r     s
    K5r   r   c                   "    e Zd ZdZd Zd Zd Zy)TestListCampaignsuO   list_campaigns: AdAccount.get_campaigns mock을 통한 list[dict] 반환 검증c                 *   t               t               }}ddd|j                  _        ddd|j                  _        ||g|j                  j                  _        |j                         }t        |      dk(  sJ |d   d   dk(  sJ |d	   d
   dk(  sJ y)uD   get_campaigns 결과를 dict 목록으로 변환하여 반환한다.camp_1z
Campaign Ar@   camp_2z
Campaign B   r   rB   r   rA   N)r   rC   rD   r   get_campaignslist_campaignslen)r&   r   camp1camp2rH   s        r   )test_list_campaigns_returns_list_of_dictsz;TestListCampaigns.test_list_campaigns_returns_list_of_dicts*  s     {IKu4<l-S*4<l-S*6;U^%%2&&(6{aay L000ay(***r   c                     g |j                   j                  _        |j                  d       |j                   j                  j                  \  }}|d   d   dk(  sJ y)u;   limit 인자가 params에 포함되어 SDK에 전달된다.
   limitparamsr   Nr   r   rD   r   rY   r&   r   _kwargss       r   &test_list_campaigns_passes_limit_paramz8TestListCampaigns.test_list_campaigns_passes_limit_param7  sV    57%%2B'OO11;;	6h(B...r   c                     g |j                   j                  _        ddg}|j                  |       |j                   j                  j                  \  }}|d   |k(  sJ y)J   fields 인자를 지정하면 해당 필드 목록이 SDK에 전달된다.rA   rB   fieldsr   Nr   )r&   r   custom_fieldsr   r   s        r   &test_list_campaigns_uses_custom_fieldsz8TestListCampaigns.test_list_campaigns_uses_custom_fields@  sZ    57%%2v]3OO11;;	6h=000r   N)r6   r7   r8   r9   r   r   r   r:   r   r   r   r   '  s    Y+/1r   r   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestCreateCampaignuN   create_campaign: AdAccount.create_campaign mock을 통한 생성 동작 검증c                     t               }dddd|j                  _        ||j                  j                  _        |j	                  ddd      }|d   dk(  sJ |d   dk(  sJ y	)
u<   create_campaign 결과를 dict로 변환하여 반환한다.camp_newzNew CampaignPAUSEDrA   rB   statusOUTCOME_TRAFFIC)rB   	objectiver   rA   r   N)r   rC   rD   r   create_campaign)r&   r   mock_campaignrH   s       r   !test_create_campaign_returns_dictz4TestCreateCampaign.test_create_campaign_returns_dictN  s|    !"6
%%2
 8E''4''' ( 
 d|z)))h8+++r   c                    t               }ddi|j                  _        ||j                  j                  _        |j	                  ddd       |j                  j                  j
                  \  }}|d   }|j                  d      dk(  sJ y	)
u4   daily_budget이 지정되면 params에 포함된다.rA   camp_budgetzBudget CampaignOUTCOME_AWARENESSi'  )rB   r   daily_budgetr   r   N)r   rC   rD   r   r   rY   getr&   r   r   r   r   r   s         r   5test_create_campaign_includes_daily_budget_when_givenzHTestCreateCampaign.test_create_campaign_includes_daily_budget_when_givena  s    !6:M5J%%27D''4") 	 	
 OO33==	6!zz.)U222r   c                     t               }ddi|j                  _        ||j                  j                  _        |j	                  dd       |j                  j                  j
                  \  }}|d   }d|vsJ y)	u=   daily_budget=None이면 params에 daily_budget 키가 없다.rA   camp_no_budgetz	No Budgetr   rB   r   r   r   Nr   rC   rD   r   r   rY   r   s         r   1test_create_campaign_omits_daily_budget_when_nonezDTestCreateCampaign.test_create_campaign_omits_daily_budget_when_noneq  st    !6:<L5M%%27D''4K;LMOO33==	6!V+++r   c                     t               }ddi|j                  _        ||j                  j                  _        |j	                  dd       |j                  j                  j
                  \  }}|d   d   g k(  sJ y)	uN   special_ad_categories를 지정하지 않으면 빈 리스트가 전달된다.rA   camp_catzCat Campaignr   r   r   special_ad_categoriesNr   )r&   r   r   r   r   s        r   8test_create_campaign_default_special_ad_categories_emptyzKTestCreateCampaign.test_create_campaign_default_special_ad_categories_empty}  st    !6:J5G%%27D''4N>OPOO33==	6h 78B>>>r   N)r6   r7   r8   r9   r   r   r   r   r:   r   r   r   r   K  s    X,&3 
,	?r   r   c                       e Zd ZdZd Zy)TestGetCampaignuB   get_campaign: Campaign.api_get mock을 통한 단일 조회 검증c                 $   t               }dddd|j                  _        t        d      5 }||j                  j                  _        |j                  d      }ddd       d   dk(  sJ |d   dk(  sJ j                  d       y# 1 sw Y   /xY w)	u6   Campaign(id).api_get() 결과를 dict로 반환한다.camp_123zTarget CampaignACTIVEr   utils.meta_ads_client.CampaignNrA   r   )r   rC   rD   r   r   get_campaignr%   )r&   r   mock_resultMockCampaignrH   s        r   test_get_campaign_returns_dictz.TestGetCampaign.test_get_campaign_returns_dict  s    k%4
##0 34 	5=HL%%--:((4F	5 d|z)))h8+++,,Z8	5 	5s   -BBN)r6   r7   r8   r9   r   r:   r   r   r   r     s
    L9r   r   c                       e Zd ZdZd Zd Zy)TestUpdateCampaignuG   update_campaign: Campaign.api_update mock을 통한 업데이트 검증c                 N   t               }ddi|j                  _        t        d      5 }||j                  j                  _        |j                  ddd      }ddd       d   du sJ j                  j                  j                  \  }}|d	   d
   dk(  sJ y# 1 sw Y   CxY w)*   api_update 결과를 dict로 반환한다.successTr   r   r   Updated)r   rB   Nr   r   r   rC   rD   r   
api_updateupdate_campaignrY   )r&   r   r   r   rH   r   r   s          r   !test_update_campaign_returns_dictz4TestUpdateCampaign.test_update_campaign_returns_dict  s    k4=t3D##034 	Y@KL%%00=++Jxi+XF	Y i D((( --88BB	6h)X555	Y 	Ys   0BB$c                 6   t               }i |j                  _        t        d      5 }||j                  j                  _        |j                  dd       ddd       j                  j                  j                  \  }}|d   d   dk(  sJ y# 1 sw Y   :xY w)uO   **params 키워드 인자가 api_update의 params 딕셔너리로 전달된다.r   camp_456  )r   Nr   r   r   )r&   r   r   r   r   r   s         r   ,test_update_campaign_passes_kwargs_as_paramsz?TestUpdateCampaign.test_update_campaign_passes_kwargs_as_params  s    k35##034 	B@KL%%00="":D"A	B !--88BB	6h/4777	B 	Bs   /BBN)r6   r7   r8   r9   r   r   r:   r   r   r   r     s    Q6
8r   r   c                       e Zd ZdZd Zd Zy)TestDeleteCampaignuA   delete_campaign: Campaign.api_delete mock을 통한 삭제 검증c                    t        d      5 }d|j                  j                  _        |j                  d      }ddd       du sJ j	                  d       |j                  j                  j                          y# 1 sw Y   ExY w)+   api_delete 성공 시 True를 반환한다.r   Ncamp_delT)r   rD   
api_deletedelete_campaignr%   rF   )r&   r   r   rH   s       r   ,test_delete_campaign_returns_true_on_successz?TestDeleteCampaign.test_delete_campaign_returns_true_on_success  sz    34 	8@DL%%00=++J7F	8 ~~,,Z8!!,,??A	8 	8s   -A==Bc           
      &   ddl m}  |dddddi dd	d
di      }t        d      5 }||j                  j                  _        t        j                  |      5  |j                  d       ddd       ddd       y# 1 sw Y   xY w# 1 sw Y   yxY w)uF   SDK가 FacebookRequestError를 발생시키면 그대로 전파된다.r   )FacebookRequestErrorz	API errorDELETEz/camp_error)methodpathi  rd   zCampaign not foundd   )re   code)re   request_contexthttp_statushttp_headersbodyr   
camp_errorN)	facebook_business.exceptionsr   r   rD   r   r`   r/   r0   r   )r&   r   r   excr   s        r   -test_delete_campaign_propagates_sdk_exceptionz@TestDeleteCampaign.test_delete_campaign_propagates_sdk_exception  s    E"'/G';SIJ
 34 	5?BL%%00<34 5&&|45	5 	55 5	5 	5s#   1BA;*B;B	 BBN)r6   r7   r8   r9   r   r   r:   r   r   r   r     s    KB5r   r   c                   "    e Zd ZdZd Zd Zd Zy)TestListAdsetsuM   list_adsets: 계정 전체 및 캠페인별 광고세트 목록 조회 검증c                    t               }ddd|j                  _        |g|j                  j                  _        |j                         }t        |      dk(  sJ |d   d   dk(  sJ |j                  j                  j                          y)uD   campaign_id가 없으면 계정 전체 광고세트를 조회한다.adset_1zAdSet Ar@   r   r   rB   N)r   rC   rD   r   get_ad_setslist_adsetsr   rF   )r&   r   adsetrH   s       r   1test_list_adsets_from_account_when_no_campaign_idz@TestListAdsets.test_list_adsets_from_account_when_no_campaign_id  s}    4=y-Q*497##0##%6{aay I---##668r   c                 `   t               }ddd|j                  _        t        d      5 }|g|j                  j                  _        |j                  d      }ddd       d   d   dk(  sJ j                  d       |j                  j                  j                          y# 1 sw Y   LxY w)	uM   campaign_id가 지정되면 해당 캠페인의 광고세트를 조회한다.
adset_campcamp_xyz)rA   campaign_idr   )r  Nr   r  )r   rC   rD   r   r  r  r%   rF   )r&   r   r  r   rH   s        r   5test_list_adsets_from_campaign_when_campaign_id_givenzDTestListAdsets.test_list_adsets_from_campaign_when_campaign_id_given  s    4@Q[-\*34 	@BGL%%11>''J'?F	@ ay':555,,Z8!!--@@B	@ 	@s   /B$$B-c                     g |j                   j                  _        |j                  d       |j                   j                  j                  \  }}|d   d   dk(  sJ y)u2   limit 인자가 SDK 호출 params에 포함된다.   r   r   r   N)r   r  rD   r  rY   r   s       r   test_list_adsets_passes_limitz,TestListAdsets.test_list_adsets_passes_limit  sV    35##0#OO//99	6h(A---r   N)r6   r7   r8   r9   r  r  r  r:   r   r   r  r    s    W
9C.r   r  c                       e Zd ZdZd Zd Zy)TestCreateAdsetuB   create_adset: AdAccount.create_ad_set mock을 통한 생성 검증c                     t               }dddd|j                  _        ||j                  j                  _        dddgii}|j                  ddd	|d
d      }|d   dk(  sJ |d   dk(  sJ y)u:   create_ad_set 결과를 dict로 변환하여 반환한다.	adset_newz	New AdSetr   r   geo_locations	countriesKRcamp_abcr   REACHIMPRESSIONS)r  rB   r   	targetingoptimization_goalbilling_eventrA   r   N)r   rC   rD   r   create_ad_setcreate_adset)r&   r   
mock_adsetr  rH   s        r   test_create_adset_returns_dictz.TestCreateAdset.test_create_adset_returns_dict	  s    [
3

""/
 6@%%2${TF&;<	$$"%' % 
 d|{***h8+++r   c           	      N   t               }ddi|j                  _        ||j                  j                  _        ddd}|j                  ddd|d	d	d
       |j                  j                  j                  \  }}|d   }|d   dk(  sJ |d   dk(  sJ |d   |k(  sJ |d   d
k(  sJ y)uM   모든 파라미터가 SDK create_ad_set 호출에 올바르게 전달된다.rA   adset_p   A   )age_minage_maxcamp_pzParam AdSeti  LINK_CLICKSr   )r  rB   r   r  r  r  r   r   r  r   r  r   N)r   rC   rD   r   r  r  rY   )r&   r   r   r  r   r   r   s          r   'test_create_adset_passes_correct_paramsz7TestCreateAdset.test_create_adset_passes_correct_params   s    [
372C
""/5?%%2 "r2	 +' 	 	
 OO11;;	6!m$000n%---k"i///h8+++r   N)r6   r7   r8   r9   r!  r*  r:   r   r   r  r    s    L,.,r   r  c                       e Zd ZdZd Zy)TestUpdateAdsetuA   update_adset: AdSet.api_update mock을 통한 업데이트 검증c                 L   t               }ddi|j                  _        t        d      5 }||j                  j                  _        |j                  dd      }ddd       d   du sJ j                  j                  j                  \  }}|d   d	   dk(  sJ y# 1 sw Y   CxY w)
r   updatedTutils.meta_ads_client.AdSet	adset_123r   )r   Nr   r   )r   rC   rD   r   r   update_adsetrY   )r&   r   r   	MockAdSetrH   r   r   s          r   test_update_adset_returns_dictz.TestUpdateAdset.test_update_adset_returns_dict<  s    k4=t3D##001 	GY=HI""--:((X(FF	G i D(((**55??	6h)X555	G 	Gs   /BB#N)r6   r7   r8   r9   r3  r:   r   r   r,  r,  9  s
    K6r   r,  c                       e Zd ZdZd Zy)TestDeleteAdsetu;   delete_adset: AdSet.api_delete mock을 통한 삭제 검증c                     t        d      5 }d|j                  j                  _        |j                  d      }ddd       du sJ j	                  d       y# 1 sw Y   !xY w)r   r/  N	adset_delT)r   rD   r   delete_adsetr%   )r&   r   r2  rH   s       r   test_delete_adset_returns_truez.TestDeleteAdset.test_delete_adset_returns_trueM  sb    01 	6Y=AI""--:((5F	6 ~~))+6	6 	6s   -AA"N)r6   r7   r8   r9   r9  r:   r   r   r5  r5  J  s
    E7r   r5  c                   "    e Zd ZdZd Zd Zd Zy)TestUploadImageu3   upload_image: AdImage 생성 및 hash 반환 검증c                 *   |dz  }|j                  d       t               }ddi|j                  _        ||j                  j
                  _        |j                  t        |            }|dk(  sJ |j                  j
                  j                          y)uL   hash 키가 최상위에 있는 응답에서 hash 문자열을 반환한다.ztest.pngs   PNG_CONTENThash
abc123hashN)	write_bytesr   rC   rD   r   create_ad_imageupload_imager{   rF   r&   r   r   img_file
mock_imagerH   s         r   1test_upload_image_returns_hash_from_flat_responsezATestUploadImage.test_upload_image_returns_hash_from_flat_response_  s~    j(^,[
39<2H
""/7A''4$$S]3%%%''::<r   c                     |dz  }|j                  d       t               }ddddiii|j                  _        ||j                  j
                  _        |j                  t        |            }|dk(  sJ y)uK   images 키 아래에 중첩된 응답 구조에서도 hash를 추출한다.z
nested.jpgs   JPG_CONTENTimagesr=  nested_hash_xyzN)r?  r   rC   rD   r   r@  rA  r{   rB  s         r   :test_upload_image_returns_hash_from_nested_images_responsezJTestUploadImage.test_upload_image_returns_hash_from_nested_images_responsem  st    l*^,[
3;lVUfLg=h2i
""/7A''4$$S]3****r   c                     t        |dz        }t        j                  t        d      5  |j	                  |       ddd       y# 1 sw Y   yxY w)uT   존재하지 않는 파일 경로를 전달하면 FileNotFoundError가 발생한다.zno_image.pngr,   N)r{   r/   r0   FileNotFoundErrorrA  )r&   r   r   nonexistents       r   'test_upload_image_raises_file_not_foundz7TestUploadImage.test_upload_image_raises_file_not_foundz  sC    (^34]],NC 	-,	- 	- 	-s   AAN)r6   r7   r8   r9   rE  rI  rM  r:   r   r   r;  r;  \  s    ==+-r   r;  c                   (    e Zd ZdZd Zd Zd Zd Zy)TestCreateCreativeuJ   create_creative: AdAccount.create_ad_creative mock을 통한 생성 검증c                     t               }ddd|j                  _        ||j                  j                  _        |j                  ddddd      }|d	   dk(  sJ y
)u?   create_ad_creative 결과를 dict로 변환하여 반환한다.
creative_1zTest Creativer@   abc123page_456zBuy now!zhttps://example.comrB   
image_hashpage_idre   linkrA   N)r   rC   rD   r   create_ad_creativecreate_creative)r&   r   mock_creativerH   s       r   !test_create_creative_returns_dictz4TestCreateCreative.test_create_creative_returns_dict  sl    !#6
%%2 ;H**7'' & ( 
 d||+++r   c                    t               }ddi|j                  _        ||j                  j                  _        |j                  ddddd       |j                  j                  j                  \  }}|d	   }|d
   d   }|d   dk(  sJ y)u/   link가 지정되면 link_data에 포함된다.rA   c_linkzLink Creative	hash_link	page_linkzClick mezhttps://landing.example.comrT  r   object_story_spec	link_datarW  Nr   rC   rD   r   rX  rY  rY   r&   r   rZ  r   r   r   ra  s          r   -test_create_creative_includes_link_when_givenz@TestCreateCreative.test_create_creative_includes_link_when_given  s    !6:H5E%%2:G**7 ". 	 	
 OO66@@	6!./<	 $AAAAr   c                 
   t               }ddi|j                  _        ||j                  j                  _        |j                  dddd       |j                  j                  j                  \  }}|d   }|d	   d
   }d|vsJ y)u0   link=None이면 link_data에 link 키가 없다.rA   	c_no_linkzNo Linkhash_nolinkpage_nolinkz
Image onlyrB   rU  rV  re   r   r`  ra  rW  Nrb  rc  s          r   )test_create_creative_omits_link_when_nonez<TestCreateCreative.test_create_creative_omits_link_when_none  s    !6:K5H%%2:G**7$! 	 	 	
 OO66@@	6!./<	Y&&&r   c                 <   t               }ddi|j                  _        ||j                  j                  _        |j                  dddd       |j                  j                  j                  \  }}|d   d	   }|d
   dk(  sJ |d   d   dk(  sJ |d   d   dk(  sJ y)uE   object_story_spec에 page_id와 message가 올바르게 설정된다.rA   c_pagezPage Creative	hash_pagemy_page_789zHello Worldri  r   r`  rV  ra  re   rU  Nrb  )r&   r   rZ  r   r   specs         r   -test_create_creative_sets_page_id_and_messagez@TestCreateCreative.test_create_creative_sets_page_id_and_message  s    !6:H5E%%2:G**7 "!!	 	 	
 OO66@@	6h 34I-///K +}<<<K .+===r   N)r6   r7   r8   r9   r[  rd  rj  rp  r:   r   r   rO  rO    s    T,&B&'$>r   rO  c                   V    e Zd ZdZdedefdZd Zd Zd Z	d Z
d	 Zd
 Zd Zd Zd Zy)TestGetInsightsu6   get_insights: 광고 성과 인사이트 조회 검증rj   r
   c                 <    t               }||j                  _        |S )uC   export_all_data를 가진 인사이트 mock 객체를 생성한다.)r   rC   rD   )r&   rj   ms      r   _make_insightzTestGetInsights._make_insight  s    K)-&r   c                 &   | j                  ddd      }t        d      5 }|g|j                  j                  _        |j                  dd      }ddd       t	              d	k(  sJ |d
   d   dk(  sJ j                  d       y# 1 sw Y   8xY w)uN   object_type='campaign'일 때 Campaign 객체의 get_insights를 호출한다.1000z50.00)impressionsspendr   camp_insightcampaignobject_typeNr   r   rx  ru  r   rD   get_insightsr   r%   r&   r   insightr   rH   s        r   #test_get_insights_for_campaign_typez3TestGetInsights.test_get_insights_for_campaign_type  s    $$Vg%NO34 	QCJ)L%%22?((Z(PF	Q 6{aay'6111,,^<	Q 	Qs   0BBc                    | j                  ddd      }t        d      5 }|g|j                  j                  _        |j                  dd      }ddd       d	   d
   dk(  sJ j	                  d       y# 1 sw Y   (xY w)uH   object_type='adset'일 때 AdSet 객체의 get_insights를 호출한다.200z2.00)clicksctrr/  adset_insightr  r|  Nr   r  ru  r   rD   r  r%   )r&   r   r  r2  rH   s        r    test_get_insights_for_adset_typez0TestGetInsights.test_get_insights_for_adset_type  s    $$f%EF01 	OY@GyI""//<((g(NF	O ay"e+++))/:	O 	Os   0A77B c                    | j                  ddi      }t        d      5 }|g|j                  j                  _        |j                  dd      }ddd       d   d   dk(  sJ j	                  d       y# 1 sw Y   (xY w)	uB   object_type='ad'일 때 Ad 객체의 get_insights를 호출한다.cpmz3.50zutils.meta_ads_client.Ad
ad_insightadr|  Nr   r  )r&   r   r  MockAdrH   s        r   test_get_insights_for_ad_typez-TestGetInsights.test_get_insights_for_ad_type  s    $$eV_5-. 	I&=DIF,,9((4(HF	I ay6)))&&|4	I 	Is   0A66A?c                     t        j                  t        d      5  |j                  dd       ddd       y# 1 sw Y   yxY w)uF   지원하지 않는 object_type 전달 시 ValueError가 발생한다.u   지원하지 않는 object_typer,   some_idinvalid_typer|  N)r/   r0   r1   r  )r&   r   s     r   8test_get_insights_invalid_object_type_raises_value_errorzHTestGetInsights.test_get_insights_invalid_object_type_raises_value_error  s=    ]]:-NO 	G	~F	G 	G 	Gs	   9Ac                 $   | j                  ddi      }t        d      5 }|g|j                  j                  _        |j                  d       ddd       j                  j                  j                  \  }}|d   d   dk(  sJ y# 1 sw Y   :xY w)	uY   date_preset과 time_range 미지정 시 'last_7d' 프리셋이 기본으로 사용된다.ry  z10.00r   camp_defaultNr   date_presetlast_7dru  r   rD   r  rY   r&   r   r  r   r   r   s         r   8test_get_insights_uses_default_date_preset_when_no_rangezHTestGetInsights.test_get_insights_uses_default_date_preset_when_no_range  s    $$gw%7834 	0CJ)L%%22?/	0 !--::DD	6h.);;;	0 	0s   .BBc                 $   | j                  i       }t        d      5 }|g|j                  j                  _        |j                  dd       ddd       j                  j                  j                  \  }}|d   d   dk(  sJ y# 1 sw Y   :xY w)uA   date_preset을 지정하면 해당 값이 params에 포함된다.r   camp_presetlast_30d)r  Nr   r  r  r  s         r   (test_get_insights_uses_given_date_presetz8TestGetInsights.test_get_insights_uses_given_date_preset  s    $$R(34 	GCJ)L%%22?:F	G !--::DD	6h.*<<<	G 	Gs   0BBc                 X   | j                  i       }ddd}t        d      5 }|g|j                  j                  _        |j                  d|       ddd       j                  j                  j                  \  }}|d   j                  d	      |k(  sJ d
|d   vsJ y# 1 sw Y   OxY w)uS   time_range를 지정하면 date_preset 없이 time_range가 params에 포함된다.z
2024-01-01z
2024-01-31)sinceuntilr   
camp_range)
time_rangeNr   r  r  )ru  r   rD   r  rY   r   )r&   r   r  r  r   r   r   s          r   'test_get_insights_uses_given_time_rangez7TestGetInsights.test_get_insights_uses_given_time_range  s    $$R(+lC
34 	ECJ)L%%22?D	E !--::DD	6h##L1Z???F8$4444	E 	Es   0B  B)c                 *   | j                  ddi      }ddg}t        d      5 }|g|j                  j                  _        |j                  d|       ddd       j                  j                  j                  \  }}|d   |k(  sJ y# 1 sw Y   7xY w)	r   rx  500ry  r   camp_fieldsr   Nr   r  )r&   r   r  r   r   r   r   s          r   $test_get_insights_uses_custom_fieldsz4TestGetInsights.test_get_insights_uses_custom_fields)  s    $$mU%;<&034 	ECJ)L%%22?mD	E !--::DD	6h=000	E 	Es   0B		Bc                 
   | j                  ddi      }t        d      5 }|g|j                  j                  _        |j                  dd      }ddd       t	              dk(  sJ j                  d       y# 1 sw Y   +xY w)	uY   object_type은 대소문자를 구분하지 않는다 (Campaign, CAMPAIGN 모두 허용).rx  100r   	camp_caseCAMPAIGNr|  Nr   r~  r  s        r   .test_get_insights_object_type_case_insensitivez>TestGetInsights.test_get_insights_object_type_case_insensitive5  s    $$mU%;<34 	NCJ)L%%22?((*(MF	N 6{a,,[9	N 	Ns   0A99BN)r6   r7   r8   r9   dictr   ru  r  r  r  r  r  r  r  r  r  r:   r   r   rr  rr    sF    @$ 9 
=	;	5G
	<	=5
1	:r   rr  __main__z-v)(r9   syspathlibr   unittest.mockr   r   r/   requests.exceptionsr   r   insertr{   __file__parentutils.meta_ads_clientr	   r   fixturer   r   r<   rN   rh   ru   r   r   r   r   r   r   r  r  r,  r5  r;  rO  rr  r6   mainr:   r   r   <module>r     s     *  ) 3tH~,,334 5 01 459d3e 434 4 $ %= % %&  & \* *2'( '(T- -D/. /.n5 54!1 !1H;? ;?|9 9*8 8:5 5J#. #.L0, 0,f6 6"
7 
7$#- #-LL> L>hh: h:V zFKK4 ! 4 4 4 4s$   #	E9,E-3E9-E6	2E99F