
    (<i                     T   d Z ddlZddlZddlmZ ddlmZmZ ddlZej                  j                  d e ee      j                  j                               dedej                  fdZdD ]
  Z ee         e       Zeej$                  d	   _         ed
efi       Zeej$                  d   _         ei       ej$                  d   _         e       ej$                  d   _         ed      5   ed      5 Z e       ej6                  _        ddlmZ ddd       ddd       ej<                  d        Zej<                  d        Z ej<                  d        Z!defdZ"de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 d0 d1      Z0 G d2 d3      Z1e2d4k(  r ejf                  ed5g       yy# 1 sw Y   xY w# 1 sw Y   xY w)6u  
Google Ads API 클라이언트 단위 테스트 (Mock 기반)

테스트 대상: GoogleAdsClient (utils.google_ads_client)

테스트 항목:
- 초기화: 환경변수 정상/누락 시 동작
- 캠페인 CRUD: list, get, create, update, delete
- 광고그룹 CRUD: list, create, update, delete
- 키워드 관리: list, add, update_status
- 인사이트: get_insights (campaign/ad_group/keyword), 잘못된 entity_type ValueError
- RSA 광고 생성: create_responsive_search_ad (유효성 검증 포함)
- 인터페이스 일관성: MetaAdsClient와 공통 메서드명 확인

모든 외부 호출(Google Ads SDK)은 Mock으로 대체하여
실제 Google Ads API를 호출하지 않는다.
    N)Path)	MagicMockpatchdotted_namereturnc                    | j                  d      }t        dt        |      dz         D ]  }dj                  |d|       }|t        j
                  vs*t        j                  |      }|dkD  r;dj                  |d|dz
         }t        t        j
                  |   ||dz
     |       |t        j
                  |<    t        j
                  |    S )u_   dotted_name 경로의 가짜 모듈을 sys.modules에 등록(없을 때만)하고 반환한다..   N)	splitrangelenjoinsysmodulestypes
ModuleTypesetattr)r   partsikeymod
parent_keys         M/home/jay/workspace/.worktrees/task-2057-dev2/tests/test_google_ads_client.py_ensure_fake_moduler   &   s    c"E1c%j1n% #hhuRay!ckk!""3'C1u XXeGa!en5
J/q1usC"CKK# ;;{##    )googlez
google.adszgoogle.ads.googleadsgoogle.ads.googleads.clientgoogle.ads.googleads.errorszgoogle.protobufgoogle.protobuf.json_formatgoogle.protobuf.field_mask_pb2r   GoogleAdsExceptionr   )return_valuer   r    %utils.google_ads_client.load_env_keys(utils.google_ads_client._GoogleAdsClient)GoogleAdsClientc                     | j                  dd       | j                  dd       | j                  dd       | j                  dd       | j                  d	d
       y)u?   5개의 필수 Google Ads 환경변수를 세팅하는 fixture.GOOGLE_ADS_DEVELOPER_TOKENztest-dev-tokenGOOGLE_ADS_CLIENT_IDztest-client-idGOOGLE_ADS_CLIENT_SECRETztest-client-secretGOOGLE_ADS_REFRESH_TOKENztest-refresh-tokenGOOGLE_ADS_CUSTOMER_ID
1234567890N)setenv)monkeypatchs    r   mock_envr/   \   s]     35EF-/?@13GH13GH/>r   c               #      K   t        d      5 } t               }|| j                  _        | ddd       y# 1 sw Y   yxY ww)uI   Google Ads SDK 클라이언트 인스턴스 mock을 반환하는 fixture.r$   N)r   r   load_from_dictr"   )mock_clsmock_instances     r   mock_sdk_clientr4   f   sB      
9	: h!/<,  s   A 7	AA Ac                 X    t        d      5  t               cddd       S # 1 sw Y   yxY w)uB   테스트용 GoogleAdsClient 인스턴스를 제공하는 fixture.r#   N)r   r%   )r/   r4   s     r   clientr6   o   s(     
6	7 ! ! ! !s   
 )c                      t               }| j                         D ]>  \  }}|j                  d      }|}|dd D ]  }t        ||      } t	        ||d   |       @ |S )uC   GoogleAdsService.search() 한 행(row) mock을 생성하는 헬퍼.r	   N)r   itemsr   getattrr   )kwargsrow	attr_pathvaluer   objparts          r   _make_search_rowrA   v   sn    
+C"LLN '	5$#2J 	%D#t$C	%U2Y&' Jr   resource_namesc                      t               }| D cg c]  }t                c}|_        t        |j                  |       D ]  \  }}||_         |S c c}w )u0   mutate_xxx() 응답 mock을 생성하는 헬퍼.)r   resultszipresource_name)rB   result_mock_mock_rrns        r   _make_mutate_resultrK      sR    +K0>?19;?K+--~> "
!" @s   Ac                   "    e Zd ZdZd Zd Zd Zy)TestGoogleAdsClientInitu0   GoogleAdsClient.__init__ 초기화 동작 검증c                     t        d      5  t               }ddd       J |j                  |u sJ y# 1 sw Y   xY w)uM   필수 환경변수가 모두 설정된 경우 예외 없이 초기화된다.r#   N)r   r%   _client)selfr/   r4   cs       r   test_init_successz)TestGoogleAdsClientInit.test_init_success   sF    :; 	"!A	" }}yyO+++	" 	"s   4=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)uJ   모든 필수 환경변수가 누락된 경우 ValueError가 발생한다.)r'   r(   r)   r*   r+   Fraisingr#   r$   Ndelenvr   pytestraises
ValueErrorr%   )rP   r.   r   s      r   test_init_missing_envz-TestGoogleAdsClientInit.test_init_missing_env   s    
 	3C sE2	3 :; 	"UCm=n 	"z* "!"	" 	" 	"" "	" 	" 	" 	"s:   BA<A0A<B0A95A<<B	BBc                    |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   단일 필수 환경변수(GOOGLE_ADS_DEVELOPER_TOKEN)가 누락되면 ValueError가 발생한다.r'   FrT   r#   r$   matchNrV   )rP   r/   r.   s      r   /test_init_missing_single_env_raises_value_errorzGTestGoogleAdsClientInit.test_init_missing_single_env_raises_value_error   s    7G:; 	"UCm=n 	"z1MN "!"	" 	" 	"" "	" 	" 	" 	"s:   BA7A+A7B+A40A77B 	<BBN)__name__
__module____qualname____doc__rR   r[   r_    r   r   rM   rM      s    :,""r   rM   c                       e Zd ZdZd Zd Zy)TestGetAccountInfou2   get_account_info: 계정 정보 dict 반환 검증c                    t               }||j                  _        t               }d|j                  _        d|j                  _        d|j                  _        d|j                  _        d|j                  _        d|j                  j                  _
        |g|j                  _        |j                         }t        |t              sJ |j                  j                          y)u+   get_account_info()는 dict를 반환한다.iIu   테스트 계정KRWz
Asia/SeoulTENABLEDN)r   get_servicer"   customeriddescriptive_namecurrency_code	time_zoneauto_tagging_enabledstatusnamesearchget_account_info
isinstancedictassert_called_oncerP   r6   r4   mock_ga_servicer<   results         r   "test_get_account_info_returns_dictz5TestGetAccountInfo.test_get_account_info_returns_dict   s    #+3B##0k$(:%%*"!-,0)#, /2e+((*&$'''113r   c                     t               }||j                  _        g |j                  _        |j	                         }|i k(  sJ y)u8   API 응답이 비어 있으면 빈 dict를 반환한다.N)r   rj   r"   rs   rt   rP   r6   r4   ry   rz   s        r   7test_get_account_info_empty_response_returns_empty_dictzJTestGetAccountInfo.test_get_account_info_empty_response_returns_empty_dict   s?    #+3B##0.0+((*||r   N)r`   ra   rb   rc   r{   r~   rd   r   r   rf   rf      s    <4&r   rf   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestListCampaignsu(   list_campaigns: list[dict] 반환 검증c                 @   t               }||j                  _        t               }d|j                  _        d|j                  _        d|j                  j                  _        d|j                  j                  _        d|j                  _        d|j                  _	        d|j                  _        t               }d|j                  _        d	|j                  _        d
|j                  j                  _        d|j                  j                  _        d|j                  _        d|j                  _	        d|j                  _        ||g|j                  _        |j                         }t        |t              sJ t!        |      dk(  sJ t#        d |D              sJ y)u7   list_campaigns() 호출 시 list[dict]를 반환한다.o      캠페인 알파ri   SEARCH
2024-01-01 @B    u   캠페인 베타PAUSEDz
2024-02-01逄    c              3   <   K   | ]  }t        |t                y wNru   rv   .0items     r   	<genexpr>z8TestListCampaigns.test_list_campaigns.<locals>.<genexpr>        =d:dD)=   N)r   rj   r"   campaignrl   rr   rq   advertising_channel_type
start_dateend_datecampaign_budgetamount_microsrs   list_campaignsru   listr   all)rP   r6   r4   ry   row1row2rz   s          r   test_list_campaignsz%TestListCampaigns.test_list_campaigns   s?   #+3B##0{/$-!6>..3#/ !#-4*{/$,!6>..3#/ !#-4*/3Tl+&&(&$'''6{a=f====r   c                     t               }||j                  _        g |j                  _        |j	                          |j                  j
                  \  }}d|j                  dd      v sJ y)u0   기본 limit(25)이 GAQL 쿼리에 포함된다.25queryr   Nr   rj   r"   rs   r   	call_argsgetrP   r6   r4   ry   rH   r;   s         r   *test_list_campaigns_default_limit_in_queryz<TestListCampaigns.test_list_campaigns_default_limit_in_query  s_    #+3B##0.0+#**44	6vzz'2....r   c                     t               }||j                  _        g |j                  _        |j	                  d       |j                  j
                  \  }}d|j                  dd      v sJ y)u:   사용자 지정 limit(10)이 GAQL 쿼리에 포함된다.
   )limit10r   r   Nr   r   s         r   )test_list_campaigns_custom_limit_in_queryz;TestListCampaigns.test_list_campaigns_custom_limit_in_query  sd    #+3B##0.0+B'#**44	6vzz'2....r   c                     t               }||j                  _        g |j                  _        |j	                         }|g k(  sJ y)u5   캠페인이 없으면 빈 리스트를 반환한다.N)r   rj   r"   rs   r   r}   s        r   ,test_list_campaigns_empty_returns_empty_listz>TestListCampaigns.test_list_campaigns_empty_returns_empty_list  s?    #+3B##0.0+&&(||r   N)r`   ra   rb   rc   r   r   r   r   rd   r   r   r   r      s    2>>	/	/r   r   c                   "    e Zd ZdZd Zd Zd Zy)TestGetCampaignu    get_campaign: dict 반환 검증c                    t               }||j                  _        t               }d|j                  _        d|j                  _        d|j                  j                  _        d|j                  j                  _        d|j                  _        d|j                  _	        d|j                  _        |g|j                  _        |j                  d      }t        |t              sJ y	)
u/   get_campaign() 호출 시 dict를 반환한다.r   u   대상 캠페인ri   r   r   r   i- 111N)r   rj   r"   r   rl   rr   rq   r   r   r   r   r   rs   get_campaignru   rv   rx   s         r   test_get_campaignz!TestGetCampaign.test_get_campaign&  s    #+3B##0k.#, 5=--2". ",3)/2e+$$U+&$'''r   c                     t               }||j                  _        g |j                  _        t	        j
                  t        d      5  |j                  d       ddd       y# 1 sw Y   yxY w)uF   존재하지 않는 campaign_id 조회 시 ValueError가 발생한다.u$   캠페인을 찾을 수 없습니다r]   999999N)r   rj   r"   rs   rX   rY   rZ   r   )rP   r6   r4   ry   s       r   .test_get_campaign_not_found_raises_value_errorz>TestGetCampaign.test_get_campaign_not_found_raises_value_error9  sW    #+3B##0.0+]]:-ST 	*)	* 	* 	*s   A##A,c                    t               }||j                  _        t               }d|j                  _        d|j                  _        d|j                  j                  _        d|j                  j                  _        d|j                  _        d|j                  _	        d|j                  _        |g|j                  _        |j                  d       |j                  j                  \  }}d|j                  d	d      v sJ y
)uH   get_campaign()이 campaign_id를 WHERE 절에 포함하여 조회한다.i+  u   ID 포함 쿼리 확인r   r   r   r     555r   N)r   rj   r"   r   rl   rr   rq   r   r   r   r   r   rs   r   r   r   )rP   r6   r4   ry   r<   rH   r;   s          r   &test_get_campaign_includes_id_in_queryz6TestGetCampaign.test_get_campaign_includes_id_in_queryB  s    #+3B##0k5#+ 5=--2". ",2)/2e+E"#**44	6

7B////r   N)r`   ra   rb   rc   r   r   r   rd   r   r   r   r   #  s    *(&*0r   r   c                   .    e Zd ZdZd Zd Zd Zd Zd Zy)TestCreateCampaignu#   create_campaign: dict 반환 검증c                    t               t               t        d      }|j                  _        t        d      }|j                  _        fd}||j
                  _        t               |j                  _        fS )uX   create_campaign에 필요한 CampaignBudgetService/CampaignService mock을 설정한다.z!customers/123/campaignBudgets/456zcustomers/123/campaigns/789c                     | dk(  rS S )NCampaignBudgetServicerd   )service_namemock_budget_servicemock_campaign_services    r   get_service_side_effectzGTestCreateCampaign._setup_create_mocks.<locals>.get_service_side_effecte  s    66**((r   )r   rK   mutate_campaign_budgetsr"   mutate_campaignsrj   side_effectget_type)rP   r4   mock_budget_resultmock_campaign_resultr   r   r   s        @@r   _setup_create_mocksz&TestCreateCampaign._setup_create_mocksZ  s{    'k )01TUCU33@23PQ>R..;	)
 3J##/09  -"$999r   c                 r    | j                  |       |j                  ddd      }t        |t              sJ y)u2   create_campaign() 호출 시 dict를 반환한다.u   새 캠페인r   r   )rr   budget_amountrq   N)r   create_campaignru   rv   rP   r6   r4   rz   s       r   test_create_campaignz'TestCreateCampaign.test_create_campaignn  s@      1'' ! ( 
 &$'''r   c                 |    | j                  |       |j                  dd      }d|v sJ d|v sJ d|v sJ d|v sJ y)	uQ   create_campaign() 반환 dict에 id, resource_name, name, status가 포함된다.u   키 확인 캠페인r   rr   r   rl   rF   rr   rq   N)r   r   r   s       r   2test_create_campaign_result_contains_expected_keyszETestCreateCampaign.test_create_campaign_result_contains_expected_keysz  s]      1''-CSZ'[v~~&(((6!!!r   c                 x    | j                  |       |j                  dd      }|j                  d      dk(  sJ y)N   status 인자를 생략하면 기본값 PAUSED가 반환 dict에 포함된다.u   기본상태 캠페인r   r   rq   r   N)r   r   r   r   s       r   *test_create_campaign_default_status_pausedz=TestCreateCampaign.test_create_campaign_default_status_paused  s>      1''-EU['\zz(#x///r   c                     | j                  |      \  }}|j                  dd       |j                  j                          |j                  j                          y)uU   create_campaign()이 예산 생성 후 캠페인 생성을 순서대로 호출한다.u   순서 확인 캠페인r   r   N)r   r   r   rw   r   )rP   r6   r4   mock_budget_svcmock_campaign_svcs        r   7test_create_campaign_calls_budget_and_campaign_serviceszJTestCreateCampaign.test_create_campaign_calls_budget_and_campaign_services  sP    -1-E-Eo-V**$=WU//BBD**==?r   N)	r`   ra   rb   rc   r   r   r   r   r   rd   r   r   r   r   W  s    -:(
(	"0@r   r   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestUpdateCampaignu#   update_campaign: dict 반환 검증c                     t               }||j                  _        t               |j                  _        t	        d      }||j
                  _        |j                  ddd      }t        |t              sJ y)u2   update_campaign() 호출 시 dict를 반환한다.zcustomers/123/campaigns/111r   u   업데이트된 캠페인ri   rr   rq   N)	r   rj   r"   r   rK   r   update_campaignru   rv   )rP   r6   r4   r   mock_responserz   s         r   test_update_campaignz'TestUpdateCampaign.test_update_campaign  sk     )3H##009  -+,IJ>K..;''4OXa'b&$'''r   c                     t               }||j                  _        t               |j                  _        t	        d      |j
                  _        |j                  ddd      }d|v sJ d|v sJ y)	uF   update_campaign() 반환 dict에 id와 updated_fields가 포함된다.zcustomers/123/campaigns/222222u   필드 확인r   r   rl   updated_fieldsN)r   rj   r"   r   rK   r   r   rP   r6   r4   mock_svcrz   s        r   :test_update_campaign_result_contains_id_and_updated_fieldszMTestUpdateCampaign.test_update_campaign_result_contains_id_and_updated_fields  sl    ;3;##009  -1DEb1c!!.''OH'Uv~~6)))r   c                     t               }||j                  _        t               |j                  _        t	        d      |j
                  _        |j                  d      }|du sJ y)u2   delete_campaign() 호출 시 True를 반환한다.zcustomers/123/campaigns/333333TN)r   rj   r"   r   rK   r   delete_campaignr   s        r   test_delete_campaignz'TestUpdateCampaign.test_delete_campaign  sW    ;3;##009  -1DEb1c!!.''.~~r   c                     t               }||j                  _        t               |j                  _        t	        d      |j
                  _        |j                  d       |j
                  j                          y)uF   delete_campaign()이 CampaignService.mutate_campaigns를 호출한다.zcustomers/123/campaigns/444444N)r   rj   r"   r   rK   r   r   rw   )rP   r6   r4   r   s       r   +test_delete_campaign_calls_mutate_campaignsz>TestUpdateCampaign.test_delete_campaign_calls_mutate_campaigns  s[    ;3;##009  -1DEb1c!!.u%!!446r   N)r`   ra   rb   rc   r   r   r   r   rd   r   r   r   r     s    -(
*		7r   r   c                   "    e Zd ZdZd Zd Zd Zy)TestListAdGroupsu(   list_ad_groups: list[dict] 반환 검증c                    t               }||j                  _        t               }d|j                  _        d|j                  _        d|j                  j                  _        d|j                  j                  _        d|j                  _        d|j                  _        d|j                  _        |g|j                  _        |j                         }t        |t              sJ t        d |D              sJ y	)
u7   list_ad_groups() 호출 시 list[dict]를 반환한다.r   u   광고그룹 Ari   SEARCH_STANDARDr   r   r   c              3   <   K   | ]  }t        |t                y wr   r   r   s     r   r   z7TestListAdGroups.test_list_ad_groups.<locals>.<genexpr>  r   r   N)r   rj   r"   ad_grouprl   rr   rq   type_cpc_bid_microsr   rs   list_ad_groupsru   r   r   rx   s         r   test_list_ad_groupsz$TestListAdGroups.test_list_ad_groups  s    #+3B##0k,#, "3&-#./2e+&&(&$'''=f====r   c                     t               }||j                  _        g |j                  _        |j	                  d       |j                  j
                  \  }}d|j                  dd      v sJ y)uT   campaign_id가 지정되면 GAQL 쿼리에 WHERE campaign.id 조건이 포함된다.999campaign_idr   r   Nr   rj   r"   rs   r   r   r   r   s         r   -test_list_ad_groups_with_campaign_id_in_queryz>TestListAdGroups.test_list_ad_groups_with_campaign_id_in_query  sd    #+3B##0.0+%0#**44	6

7B////r   c                     t               }||j                  _        g |j                  _        |j	                  d       |j                  j
                  \  }}d|j                  dd      vsJ y)uO   campaign_id가 None이면 WHERE 절 없이 전체 광고그룹을 조회한다.Nr   zWHERE campaign.idr   r   r   r   s         r   7test_list_ad_groups_without_campaign_id_no_where_clausezHTestListAdGroups.test_list_ad_groups_without_campaign_id_no_where_clause  sd    #+3B##0.0+$/#**44	6"&**Wb*AAAAr   N)r`   ra   rb   rc   r   r   r  rd   r   r   r   r     s    2>(	0
Br   r   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestCreateAdGroupu#   create_ad_group: dict 반환 검증c                    t               t               fd}||j                  _        t               |j                  _        t        d      j                  _        |j                  dddd      }t        |t              sJ y)	u2   create_ad_group() 호출 시 dict를 반환한다.c                 4    | dk(  rS | dk(  rS t               S NCampaignServiceAdGroupServicer   rr   mock_ad_group_svcr   s    r   r   zGTestCreateAdGroup.test_create_ad_group.<locals>.get_service_side_effect  )    ((((''((;r   customers/123/adGroups/456r   u   새 광고그룹r   r   )r   rr   cpc_bidrq   N)
r   rj   r   r   r"   rK   mutate_ad_groupscreate_ad_groupru   rv   rP   r6   r4   r   rz   r  r   s        @@r   test_create_ad_groupz&TestCreateAdGroup.test_create_ad_group  s    %K%K	 3J##/09  -:MNj:k**7''#	 ( 
 &$'''r   c                    t               t               fd}||j                  _        t               |j                  _        t        d      j                  _        |j                  ddd      }|j                  d      dk(  sJ y	)
r   c                 4    | dk(  rS | dk(  rS t               S r  r	  r
  s    r   r   z]TestCreateAdGroup.test_create_ad_group_default_status_paused.<locals>.get_service_side_effect   r  r   zcustomers/123/adGroups/789r   u   기본상태 광고그룹r   )r   rr   r  rq   r   N)	r   rj   r   r   r"   rK   r  r  r   r  s        @@r   *test_create_ad_group_default_status_pausedz<TestCreateAdGroup.test_create_ad_group_default_status_paused  s    %K%K	 3J##/09  -:MNj:k**7'', ( 
 zz(#x///r   c                     t               }||j                  _        t               |j                  _        t	        d      |j
                  _        |j                  d      }|du sJ y)u2   delete_ad_group() 호출 시 True를 반환한다.r  456TN)r   rj   r"   r   rK   r  delete_ad_grouprP   r6   r4   r  rz   s        r   test_delete_ad_groupz&TestCreateAdGroup.test_delete_ad_group4  sY    %K3D##009  -:MNj:k**7''.~~r   c                     t               }||j                  _        t               |j                  _        t	        d      |j
                  _        |j                  dd      }t        |t              sJ y)u2   update_ad_group() 호출 시 dict를 반환한다.r  r  u   업데이트된 광고그룹)rr   N)	r   rj   r"   r   rK   r  update_ad_groupru   rv   r  s        r   test_update_ad_groupz&TestCreateAdGroup.test_update_ad_group?  sc    %K3D##009  -:MNj:k**7''4R'S&$'''r   N)r`   ra   rb   rc   r  r  r  r  rd   r   r   r  r    s    -(402		(r   r  c                   "    e Zd ZdZd Zd Zd Zy)TestListKeywordsu'   list_keywords: list[dict] 반환 검증c                    t               }||j                  _        t               }d|j                  _        d|j                  j
                  _        d|j                  j
                  j                  _        d|j                  j                  _        d|j                  _
        d|j                  _        |g|j                  _        |j                  d      }t        |t               sJ t#        d	 |D              sJ y
)u6   list_keywords() 호출 시 list[dict]를 반환한다.i     파이썬 개발BROADri   i 5   r  ad_group_idc              3   <   K   | ]  }t        |t                y wr   r   r   s     r   r   z6TestListKeywords.test_list_keywords.<locals>.<genexpr>d  r   r   N)r   rj   r"   ad_group_criterioncriterion_idkeywordtext
match_typerr   rq   r   r   rl   rs   list_keywordsru   r   r   rx   s         r   test_list_keywordsz#TestListKeywords.test_list_keywordsS  s    #+3B##0k.2+.@&&+9@&&116-6%%*06-/2e+%%%%8&$'''=f====r   c                     t               }||j                  _        g |j                  _        |j	                  d       |j                  j
                  \  }}d|j                  dd      v sJ y)u<   list_keywords()가 ad_group_id를 WHERE 절에 포함한다.r  r$  r   r   Nr   rj   r"   rs   r,  r   r   r   s         r   0test_list_keywords_includes_ad_group_id_in_queryzATestListKeywords.test_list_keywords_includes_ad_group_id_in_queryf  sd    #+3B##0.0+/#**44	6

7B////r   c                     t               }||j                  _        g |j                  _        |j	                  dd       |j                  j
                  \  }}d|j                  dd      v sJ y)u<   list_keywords()에 limit 값이 GAQL 쿼리에 반영된다.r     )r%  r   5r   r   Nr/  r   s         r   !test_list_keywords_limit_in_queryz2TestListKeywords.test_list_keywords_limit_in_queryq  sf    #+3B##0.0+a8#**44	6fjj"----r   N)r`   ra   rb   rc   r-  r0  r4  rd   r   r   r  r  P  s    1>&	0	.r   r  c                   "    e Zd ZdZd Zd Zd Zy)TestAddKeywordsu&   add_keywords: list[dict] 반환 검증c                 L   t               t               fd}||j                  _        t               |j                  _        t        dd      j                  _        ddddddg}|j                  d	|
      }t        |t              sJ t        d |D              sJ y)u5   add_keywords() 호출 시 list[dict]를 반환한다.c                 4    | dk(  rS | dk(  rS t               S Nr  AdGroupCriterionServicer	  rr   r  mock_criterion_svcs    r   r   zBTestAddKeywords.test_add_keywords.<locals>.get_service_side_effect  )    ''((00));r   &customers/123/adGroupCriteria/456~1001&customers/123/adGroupCriteria/456~1002r!  r"  r*  r+  u   구글 광고EXACTr  r%  keywordsc              3   <   K   | ]  }t        |t                y wr   r   r   s     r   r   z4TestAddKeywords.test_add_keywords.<locals>.<genexpr>  r   r   N)r   rj   r   r   r"   rK   mutate_ad_group_criteriaadd_keywordsru   r   r   )rP   r6   r4   r   rC  rz   r  r<  s         @@r   test_add_keywordsz!TestAddKeywords.test_add_keywords  s    %K&[	 3J##/09  -CV44D
33@ (w?$G<
 $$$J&$'''=f====r   c                 >   t               t               fd}||j                  _        t               |j                  _        t        ddd      j                  _        ddddd	dd
ddg}|j                  d|       j                  j                          y)uO   add_keywords()는 여러 키워드를 한 번의 mutate 호출로 추가한다.c                 4    | dk(  rS | dk(  rS t               S r9  r	  r;  s    r   r   z]TestAddKeywords.test_add_keywords_calls_mutate_criteria_once.<locals>.get_service_side_effect  r=  r   r>  r?  z&customers/123/adGroupCriteria/456~1003u
   키워드1r"  r@  u
   키워드2PHRASEu
   키워드3rA  r  rB  N)	r   rj   r   r   r"   rK   rE  rF  rw   )rP   r6   r4   r   rC  r  r<  s        @@r   ,test_add_keywords_calls_mutate_criteria_oncez<TestAddKeywords.test_add_keywords_calls_mutate_criteria_once  s    %K&[	 3J##/09  -CV444D
33@ "9!:!9

 	A33FFHr   c                 $   t               t               fd}||j                  _        t               |j                  _        t        d      j                  _        |j                  ddddg      }t        |      dk(  sJ d	|d
   v sJ y)u@   add_keywords() 반환 각 항목에 resource_name 키가 있다.c                 4    | dk(  rS | dk(  rS t               S r9  r	  r;  s    r   r   z`TestAddKeywords.test_add_keywords_result_contains_resource_name.<locals>.get_service_side_effect  r=  r   z&customers/123/adGroupCriteria/456~9999r  u   단일 키워드r"  r@  rB  r
   rF   r   N)	r   rj   r   r   r"   rK   rE  rF  r   )rP   r6   r4   r   rz   r  r<  s        @@r   /test_add_keywords_result_contains_resource_namez?TestAddKeywords.test_add_keywords_result_contains_resource_name  s    %K&[	 3J##/09  -CV4D
33@ $$1IJ % 

 6{a&)+++r   N)r`   ra   rb   rc   rG  rK  rN  rd   r   r   r6  r6  }  s    0>:I<,r   r6  c                       e Zd ZdZd Zd Zy)TestUpdateKeywordStatusu)   update_keyword_status: dict 반환 검증c                     t               }||j                  _        t               |j                  _        t	        d      |j
                  _        |j                  ddd      }t        |t              sJ y)u8   update_keyword_status() 호출 시 dict를 반환한다.r>  1001r  r   keyword_criterion_idr%  rq   N)	r   rj   r"   r   rK   rE  update_keyword_statusru   rv   rP   r6   r4   r<  rz   s        r   test_update_keyword_statusz2TestUpdateKeywordStatus.test_update_keyword_status  sr    &[3E##009  -CV4D
33@ --!' . 
 &$'''r   c                 L   t               }||j                  _        t               |j                  _        t	        d      |j
                  _        |j                  ddd      }|j                  d      dk(  sJ |j                  d      dk(  sJ |j                  d      dk(  sJ y	)
uS   update_keyword_status() 반환 dict에 criterion_id, ad_group_id, status가 있다.r>  rR  r  ri   rS  r(  r%  rq   N)r   rj   r"   r   rK   rE  rU  r   rV  s        r   1test_update_keyword_status_contains_expected_keyszITestUpdateKeywordStatus.test_update_keyword_status_contains_expected_keys  s    &[3E##009  -CV4D
33@ --!' . 
 zz.)V333zz-(E111zz(#y000r   N)r`   ra   rb   rc   rW  rY  rd   r   r   rP  rP    s    3($1r   rP  c                   F    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
d	 Zd
 Zy)TestGetInsightsuG   get_insights: list[dict] 반환 및 잘못된 entity_type 예외 검증c                    t               }||j                  _        t               }d|j                  _        d|j                  _        d|j                  _        d|j                  _	        d|j                  _
        d|j                  _        d|j                  _        d|j                  _        d	|j                  _        |g|j                  _        |j!                  d
d      }t#        |t$              sJ t'        d |D              sJ y)u5   get_insights() 호출 시 list[dict]를 반환한다.r   u   분석 캠페인
2024-01-15i     i` 皙?p  r   r   r   entity_typec              3   <   K   | ]  }t        |t                y wr   r   r   s     r   r   z4TestGetInsights.test_get_insights.<locals>.<genexpr>  r   r   N)r   rj   r"   r   rl   rr   segmentsdatemetricsimpressionsclickscost_microsctraverage_cpcconversionsrs   get_insightsru   r   r   rx   s         r   test_get_insightsz!TestGetInsights.test_get_insights	  s    #+3B##0k.("& ")"&"$/2e+$$U
$C&$'''=f====r   c                     t               }||j                  _        g |j                  _        |j	                  d       |j                  j
                  \  }}d|j                  dd      j                         v sJ y)uV   entity_type 기본값은 'campaign'이며, campaign 테이블을 FROM에 사용한다.r   r   r   r   N)r   rj   r"   rs   rm  r   r   lowerr   s         r   .test_get_insights_default_entity_type_campaignz>TestGetInsights.test_get_insights_default_entity_type_campaign  sj    #+3B##0.0+E"#**44	6VZZ4::<<<<r   c                    t               }||j                  _        t               }d|j                  _        d|j                  _        d|j                  _        d|j                  _	        d|j                  _
        d|j                  _        d|j                  _        d|j                  _        d	|j                  _        |g|j                  _        |j!                  d
d      }t#        |t$              sJ y)uG   entity_type='ad_group'으로 광고그룹 인사이트를 조회한다.r#  u   광고그룹 인사이트r]  i  2   i r_  r`     r  r   ra  N)r   rj   r"   r   rl   rr   rd  re  rf  rg  rh  ri  rj  rk  rl  rs   rm  ru   r   rx   s         r   test_get_insights_ad_group_typez/TestGetInsights.test_get_insights_ad_group_type*  s    #+3B##0k7("&"("&"#/2e+$$U
$C&$'''r   c                    t               }||j                  _        g |j                  _        |j	                  dd      }t        |t              sJ |j                  j                  \  }}d|j                  dd      v sJ y)u@   entity_type='keyword'로 키워드 인사이트를 조회한다.rR  r)  ra  keyword_viewr   r   N)	r   rj   r"   rs   rm  ru   r   r   r   )rP   r6   r4   ry   rz   rH   r;   s          r   test_get_insights_keyword_typez.TestGetInsights.test_get_insights_keyword_type?  sx    #+3B##0.0+$$V$C&$'''#**44	6GR!8888r   c                     t        j                  t              5  |j                  dd       ddd       y# 1 sw Y   yxY w)uF   지원하지 않는 entity_type 전달 시 ValueError가 발생한다.r   invalid_typera  NrX   rY   rZ   rm  rP   r6   r4   s      r   %test_get_insights_invalid_entity_typez5TestGetInsights.test_get_insights_invalid_entity_typeK  s:    ]]:& 	C>B	C 	C 	Cs	   7A c                     t        j                  t        d      5  |j                  dd       ddd       y# 1 sw Y   yxY w)uB   ValueError 메시지에 잘못된 entity_type 값이 포함된다.unsupported_entityr]   r   ra  Nr{  r|  s      r   -test_get_insights_invalid_entity_type_messagez=TestGetInsights.test_get_insights_invalid_entity_type_messageP  s>    ]]:-AB 	I3GH	I 	I 	Is	   9Ac                     t               }||j                  _        g |j                  _        |j	                  d       |j                  j
                  \  }}d|j                  dd      v sJ y)uB   date_range 기본값 'LAST_7_DAYS'가 GAQL 쿼리에 포함된다.r   LAST_7_DAYSr   r   Nr   rj   r"   rs   rm  r   r   r   s         r   0test_get_insights_default_date_range_last_7_daysz@TestGetInsights.test_get_insights_default_date_range_last_7_daysU  sa    #+3B##0.0+E"#**44	6

7B 7777r   c                     t               }||j                  _        g |j                  _        |j	                  ddd       |j                  j
                  \  }}d|j                  dd      v sJ y)u;   사용자 지정 date_range가 GAQL 쿼리에 포함된다.r   r   LAST_30_DAYS)rb  
date_ranger   r   Nr  r   s         r   #test_get_insights_custom_date_rangez3TestGetInsights.test_get_insights_custom_date_range`  sh    #+3B##0.0+EznU#**44	6GR!8888r   c                 N   t               }||j                  _        t               }d|j                  _        d|j                  _        d|j                  _        d|j                  _	        d|j                  _
        d|j                  _        d|j                  _        d|j                  _        d	|j                  _        |g|j                  _        |j!                  d
d      }t#        |      dk(  sJ |d   j%                  d      d
k(  sJ |d   j%                  d      dk(  sJ y)uR   get_insights() 반환 dict 각 항목에 entity_id와 entity_type이 포함된다.i	  u   리포트 캠페인z
2024-01-20i  d   i'	 r_  r`  r2  777r   ra  r
   r   	entity_idrb  N)r   rj   r"   r   rl   rr   rd  re  rf  rg  rh  ri  rj  rk  rl  rs   rm  r   r   rx   s         r   2test_get_insights_returns_records_with_entity_infozBTestGetInsights.test_get_insights_returns_records_with_entity_infok  s    #+3B##0k1("& "("&"#/2e+$$U
$C6{aay}}[)U222ay}}]+z999r   N)r`   ra   rb   rc   rn  rq  ru  rx  r}  r  r  r  r  rd   r   r   r[  r[    s4    Q>,	=(*
9C
I
	8	9:r   r[  c                   @    e Zd ZdZd Zd Zd Zd Zd Zd Z	d Z
d	 Zy
)TestCreateResponsiveSearchAdu=   create_responsive_search_ad: dict 반환 및 유효성 검증c                     t               t               fd}||j                  _        t               |j                  _        t        d      j                  _        fS )uH   create_responsive_search_ad에 필요한 서비스 mock을 설정한다.c                 4    | dk(  rS | dk(  rS t               S )Nr  AdGroupAdServicer	  )rr   mock_ad_group_ad_svcr  s    r   r   zNTestCreateResponsiveSearchAd._setup_rsa_mocks.<locals>.get_service_side_effect  s)    ''(())++;r   z customers/123/adGroupAds/456~999)r   rj   r   r   r"   rK   mutate_ad_group_ads)rP   r4   r   r  r  s      @@r   _setup_rsa_mocksz-TestCreateResponsiveSearchAd._setup_rsa_mocks  s\    %K({	 3J##/ 1:  -@STv@w00= "666r   c                 |    | j                  |       |j                  dg dddgd      }t        |t              sJ y)u>   create_responsive_search_ad() 호출 시 dict를 반환한다.r  )u   헤드라인1u   헤드라인2u   헤드라인3u   설명1u   설명2https://example.comr%  	headlinesdescriptions	final_urlN)r  create_responsive_search_adru   rv   r   s       r    test_create_responsive_search_adz=TestCreateResponsiveSearchAd.test_create_responsive_search_ad  sH    o.33I#Y/+	 4 
 &$'''r   c                 |    | j                  |       |j                  dg dddgd      }d|v sJ d|v sJ d	|v sJ y
)uE   반환 dict에 resource_name, ad_group_id, final_url이 포함된다.r  H1H2H3D1D2zhttps://example.com/landingr  rF   r%  r  N)r  r  r   s       r   >test_create_responsive_search_ad_result_contains_expected_keysz[TestCreateResponsiveSearchAd.test_create_responsive_search_ad_result_contains_expected_keys  sa    o.33(3	 4 
 &(((&&&f$$$r   c                     | j                  |       t        j                  t        d      5  |j	                  dddgddgd	       d
d
d
       y
# 1 sw Y   y
xY w)u=   헤드라인이 3개 미만이면 ValueError가 발생한다.   헤드라인r]   r  r  r  r  r  r  r  Nr  rX   rY   rZ   r  r|  s      r   9test_create_responsive_search_ad_too_few_headlines_raiseszVTestCreateResponsiveSearchAd.test_create_responsive_search_ad_too_few_headlines_raises  sZ    o.]]:^< 	..!,"D\/	 / 	 	 	   AAc           	          | j                  |       t        j                  t        d      5  |j	                  dt        d      D cg c]  }d| 	 c}ddgd	       d
d
d
       y
c c}w # 1 sw Y   y
xY w)uA   헤드라인이 15개를 초과하면 ValueError가 발생한다.r  r]   r     Hr  r  r  r  N)r  rX   rY   rZ   r  r   )rP   r6   r4   r   s       r   :test_create_responsive_search_ad_too_many_headlines_raiseszWTestCreateResponsiveSearchAd.test_create_responsive_search_ad_too_many_headlines_raises  su    o.]]:^< 	..!,1"I6qQqc76"D\/	 / 	 	 7	 	s   A-A(A-(A--A6c                     | j                  |       t        j                  t        d      5  |j	                  dg ddgd       ddd       y# 1 sw Y   yxY w)	u7   설명이 2개 미만이면 ValueError가 발생한다.   설명r]   r  r  r  r  r  Nr  r|  s      r   <test_create_responsive_search_ad_too_few_descriptions_raiseszYTestCreateResponsiveSearchAd.test_create_responsive_search_ad_too_few_descriptions_raises  sT    o.]]:X6 	..!,"V/	 / 	 	 	s   AAc                     | j                  |       t        j                  t        d      5  |j	                  dg dg dd       ddd       y# 1 sw Y   yxY w)	u:   설명이 4개를 초과하면 ValueError가 발생한다.r  r]   r  r  )r  r  D3D4D5r  r  Nr  r|  s      r   =test_create_responsive_search_ad_too_many_descriptions_raiseszZTestCreateResponsiveSearchAd.test_create_responsive_search_ad_too_many_descriptions_raises  sR    o.]]:X6 	..!,;/	 / 	 	 	r  c           	          | j                  |       |j                  dt        dd      D cg c]  }d| 	 c}t        dd      D cg c]  }d| 	 c}d      }t        |t              sJ y	c c}w c c}w )
uM   최대 허용 개수(헤드라인 15개, 설명 4개)로 정상 생성된다.r  r
   r  r  r2  r  zhttps://example.com/maxr  N)r  r  r   ru   rv   )rP   r6   r4   r   rz   s        r   ?test_create_responsive_search_ad_max_headlines_and_descriptionsz\TestCreateResponsiveSearchAd.test_create_responsive_search_ad_max_headlines_and_descriptions  s{    o.3338B<@aaS)@05a<1F1#,</	 4 
 &$''' A<s   A0
A5N)r`   ra   rb   rc   r  r  r  r  r  r  r  r  rd   r   r   r  r    s-    G7((%



(r   r  c                   "    e Zd ZdZd Zd Zd Zy)TestInterfaceConsistencyuA   GoogleAdsClient와 MetaAdsClient의 공통 인터페이스 검증c                 n    g d}|D cg c]  }t        t        |      r| }}|r
J d|        yc c}w )uL   GoogleAdsClient가 MetaAdsClient와 공통 메서드를 모두 구현한다.r   r   r   r   r   rm  rt   uE   GoogleAdsClient에 다음 공통 메서드가 누락되었습니다: Nhasattrr%   )rP   common_methodsmmissings       r   test_interface_consistencyz3TestInterfaceConsistency.test_interface_consistency  sF    
 -PGOQ4O1PPmcdkclmm{7 Q   22c           
          g d}|D cg c]  }t        t        t        |d            r|! }}|r
J d|        yc c}w )u9   GoogleAdsClient의 공통 메서드들이 callable이다.r  Nu4   GoogleAdsClient에서 callable이 아닌 메서드: )callabler:   r%   )rP   r  r  not_callables       r   $test_all_common_methods_are_callablez=TestInterfaceConsistency.test_all_common_methods_are_callable  sP    
 $2ea'/[\^bBc9deef#WXdWe!ff< fs   <<c                 n    g d}|D cg c]  }t        t        |      r| }}|r
J d|        yc c}w )uO   Google Ads 전용 메서드(키워드/광고그룹/RSA 관련)가 존재한다.)r   r  r  r  r,  rF  rU  r  uE   GoogleAdsClient에 다음 전용 메서드가 누락되었습니다: Nr  )rP   google_specific_methodsr  r  s       r   &test_google_ads_specific_methods_existz?TestInterfaceConsistency.test_google_ads_specific_methods_exist!  sH    	#
 6YW_VW=X1YYmcdkclmm{7 Zr  N)r`   ra   rb   rc   r  r  r  rd   r   r   r  r    s    Kn g nr   r  __main__z-v)4rc   r   r   pathlibr   unittest.mockr   r   rX   pathinsertstr__file__parentr   r   _pkg_MockSDKClientClsr   r%   type	Exception_MockGoogleAdsExceptionr!   MessageToDict	FieldMask_patched_sdk_clsr1   r"   utils.google_ads_clientfixturer/   r4   r6   rA   rK   rM   rf   r   r   r   r   r   r  r  r6  rP  r[  r  r  r`   mainrd   r   r   <module>r     sk  $    *  3tH~,,334 5$S $U-=-= $	 
D 
 K =N) * :3i\2F @W) * =;DRT;U) * 8:C+, - 7 

128	
4589I3<;##078 8 ? ?   ! !
) 
  !" !"R L@ @F10 10h=@ =@@07 07p,B ,B^J( J(d*. *.ZW, W,t'1 '1^z: z:Dn( n(l2n 2nj zFKK4 ! I8 8 8 8s$   0	H9HHH	HH'