
    ih                        U d Z ddlZddlZddlZddlZddlmZ ddlmZm	Z	m
Z
 ddlZddlZ eej                  j                  dd            Z ee      ej"                  vr"ej"                  j%                  d ee             dZedz  ed<   	 ddlmZmZmZmZ ej<                  j?                  edude       Z d2dede	fdZ!d3de	dz  de	fdZ"d3de	dz  de	fdZ#d3de	dz  de	fdZ$d3de	dz  de	fdZ%dedefdZ&ejN                  d        Z(ejN                  d        Z)e d        Z*e d        Z+ej<                  jX                  e d               Z-ej<                  jX                  e d               Z.ej<                  jX                  e d                Z/ej<                  jX                  e d!               Z0ej<                  jX                  e d"               Z1ej<                  jX                  e d#               Z2ej<                  jX                  e d$               Z3ej<                  jX                  e d%               Z4ej<                  jX                  e d&               Z5ej<                  jX                  e d'               Z6ej<                  jX                  e d(               Z7ej<                  jX                  e d)               Z8e d*        Z9dZ:edz  ed+<   dZ;dZ<	 dd,l=m>Z>m?Z? e>Z;e?Z<ej<                  j?                  e:dud-e:       ZAeAd.        ZBeAd/        ZCeAd0        ZDej<                  jX                  eAe d1                      ZEy# e$ r0ZeZdZ ed	efi       Z ed
efi       Z edefi       ZY dZ[dZ[ww xY w# e$ rZ@e@Z:Y dZ@[@dZ@[@ww xY w)4u  
test_tistory_publisher.py

TistoryPublisher 단위 테스트 (아르고스 작성)

- Playwright 기반 티스토리 자동화 퍼블리셔의 mock 단위 테스트
- 모든 외부 의존성(Playwright, 브라우저)은 AsyncMock/MagicMock으로 대체
- 실제 브라우저 실행 없음

TistoryPublisher._ensure_browser() 구현 패턴:
  pw = await async_playwright().start()   ← async_playwright()는 동기 호출, .start()가 코루틴
  self._browser = await pw.chromium.launch(...)
  self._context = await self._browser.new_context(...)
  self._page    = await self._context.new_page()

테스트 항목:
  1. test_init_from_env:               환경변수에서 설정 로드
  2. test_init_from_args:              생성자 인자로 설정
  3. test_load_session_file_not_found: 세션 파일 없을 때 FileNotFoundError
  4. test_load_session_success:        유효한 세션 파일 로드
  5. test_check_session_valid_true:    세션 유효 시 True 반환
  6. test_check_session_valid_expired: 세션 만료(로그인 리다이렉트) 시 False 반환
  7. test_publish_post_private:        비공개 글 발행 성공
  8. test_publish_post_with_tags:      태그 포함 발행
  9. test_publish_post_session_expired: 세션 만료 시 SessionExpiredError
 10. test_publish_post_failure:        발행 실패 시 PublishError
 11. test_edit_post_success:           글 수정 성공
 12. test_get_post_status:             글 상태 확인
 13. test_upload_image_success:        이미지 업로드 성공
 14. test_close:                       리소스 정리 확인
 15. test_visibility_default_private:  visibility 기본값 private 확인
 16. test_pipeline_parse_html_title:   HTML에서 제목 추출
 17. test_pipeline_parse_html_tags:    HTML meta에서 태그 추출
 18. test_pipeline_file_not_found:     HTML 파일 없을 때 에러
 19. test_pipeline_publish_flow:       전체 파이프라인 흐름 (mock)
    N)Path)	AsyncMock	MagicMockpatchWORKSPACE_ROOTz/home/jay/workspace_IMPORT_ERROR)LoginRequiredErrorPublishErrorSessionExpiredErrorTistoryPublisherr   r
   r	      TistoryPublisher 미구현: )reasonurlreturnc                    t               }t               |_        t               |_        t               |_        t               |_        t               |_        t               |_        t        d      |_        t               |_	        t               |_
        t        t        t        d                  |_        t               |_        t        | fd      t        |      _        |S )uA   Playwright Page 객체를 흉내내는 MagicMock을 반환한다.Nreturn_value )
inner_textc                     |S N )self_us     M/home/jay/workspace/.worktrees/task-2116-dev1/tests/test_tistory_publisher.py<lambda>z!_make_mock_page.<locals>.<lambda>p   s    2     )r   r   gotofillclickwait_for_selectorwait_for_load_statewait_for_urlevaluate
screenshotcloselocatorexpect_responsepropertytyper   )r   pages     r   _make_mock_pager,   `   s    ;DDIDIDJ&[D({D!D40DMkDODJ)yVX?Y*Z[DL$;D c56DJNKr   r+   c                     t               }| xs
 t               }t        |      |_        t               |_        t        dddg      |_        t        g g d      |_        t               |_        |S )uK   Playwright BrowserContext 객체를 흉내내는 MagicMock을 반환한다.r   sessabc)namevaluecookiesorigins)r   r,   r   new_pageadd_cookiesr3   storage_stater&   )r+   ctx_pages      r   _make_mock_contextr:   u   s_    
+C%O%E%0CLkCO6E*J)KLCK!2"/MNCCIJr   contextc                 v    t               }| xs
 t               }t        |      |_        t               |_        |S )uD   Playwright Browser 객체를 흉내내는 MagicMock을 반환한다.r   )r   r:   r   new_contextr&   )r;   browser_ctxs      r   _make_mock_browserr@      s2    kG*(*D#6GKGMNr   r>   c                     t               }| xs
 t               }t               |_        t        |      |j                  _        t               |_        |S )uP   async_playwright().start() 반환값인 Playwright 인스턴스를 흉내낸다.r   )r   r@   chromiumr   launchstop)r>   pw_inst_browsers      r   _make_pw_instancerG      sB    kG.,.H {G'X>G;GLNr   c                 ^    t        |       }t        t        t        |                  }|S )ue  
    scripts.blog.tistory_publisher.async_playwright 를 대체할 callable을 반환한다.

    _ensure_browser()가 사용하는 패턴:
        pw = await async_playwright().start()

    따라서 async_playwright()는 동기적으로 .start() 메서드를 가진 객체를 반환해야 하고,
    .start()는 코루틴(AsyncMock)이어야 한다.
    r>   r   )start)rG   r   r   )r>   rE   mock_callables      r   _make_async_playwright_callablerL      s)      0G99RY;Z+[\Mr   tmp_pathc                 l    | dz  }ddddgg d}|j                  t        j                  |             |S )Nzsession.json	TSSESSIONzmock-session-valuez.tistory.com)r0   r1   domainr2   )
write_textjsondumps)rM   session_filesession_datas      r   _write_sessionrV      sD    n,L(3GSabcL DJJ|45r   c                      t               S )u2   async_playwright를 대체하는 callable fixture.)rL   r   r   r   mock_playwright_callablerX      s     +,,r   c                   K   t         t        j                  dt                 t        |       }t	        d|      5  t        dt        |      d      }| ddd       y# 1 sw Y   yxY ww)uH   TistoryPublisher 인스턴스를 mock playwright와 함께 생성한다.Nr   /scripts.blog.tistory_publisher.async_playwrighttestblogT	blog_namesession_pathheadless)r   pytestskiprV   r   r   str)rM   rX   rT   pubs       r   	publisherrd      sk       2=/BC!(+L	@BZ	[  \*

 	  s   :A.A"	A."A+'A.c                 (   t        |       }|j                  dd       |j                  dt        |             t        t        j
                  d   t        j
                  d         }|j                  dk(  sJ |j                  t        |      k(  sJ y)uM   환경변수에서 blog_name, session_path를 로드할 수 있어야 한다.TISTORY_BLOG_NAMEzenv-blogTISTORY_SESSION_PATHr]   r^   N)rV   setenvrb   r   osenvironr]   r^   )rM   monkeypatchrT   rc   s       r   test_init_from_envrm      s     "(+L*J7-s</@A
**01ZZ 67C ==J&&&s<0000r   c                     t        |       }t        dt        |      d      }|j                  dk(  sJ |j                  t        |      k(  sJ |j
                  du sJ y)uX   생성자 인자로 blog_name, session_path, headless를 설정할 수 있어야 한다.zmy-blogFr\   N)rV   r   rb   r]   r^   r_   rM   rT   rc   s      r   test_init_from_argsrp      se     "(+L
&C
 ==I%%%s<0000<<5   r   c                    K   t        | dz        }t        d|      }t        j                  t              5  |j                          d{    ddd       y7 # 1 sw Y   yxY ww)uZ   세션 파일이 없으면 _load_session()이 FileNotFoundError를 발생시켜야 한다.znonexistent_session.jsonr[   rh   N)rb   r   r`   raisesFileNotFoundError_load_session)rM   missing_pathrc   s      r    test_load_session_file_not_foundrv      s`      x"<<=L
Zl
KC	(	) "!!!" "!" "s.   5A'AAA	A'AA$ A'c                    K   t        |       }t        dt        |            }|j                          d{    y7 w)uV   유효한 세션 파일이 있으면 _load_session()이 정상 완료되어야 한다.r[   rh   N)rV   r   rb   rt   ro   s      r   test_load_session_successrx     s5      "(+L
Zc,>O
PC



s   5?=?c                 4  K   d}t        |      }t        |      }t        |      }t        |      }t	        |       }t        d|      5  t        dt        |            }|j                          d	{   }d	d	d	       d
u sJ y	7 # 1 sw Y   xY ww)u^   로그인 상태의 페이지 URL이면 _check_session_valid()가 True를 반환해야 한다.z$https://testblog.tistory.com/manage/r   r+   r;   rI   rZ   r[   rh   NT	r,   r:   r@   rL   rV   r   r   rb   _check_session_valid)	rM   	valid_urlr+   r8   r>   rK   rT   rc   results	            r   test_check_session_valid_truer     s      7Iy)D
$
'C -G3GDM!(+L	@-	P 2#lBST//112 T>> 22 20   A
B*B6B
7B;B
BBBc                 4  K   d}t        |      }t        |      }t        |      }t        |      }t	        |       }t        d|      5  t        dt        |            }|j                          d	{   }d	d	d	       d
u sJ y	7 # 1 sw Y   xY ww)ue   로그인 페이지로 리다이렉트 되면 _check_session_valid()가 False를 반환해야 한다.z"https://www.tistory.com/auth/loginrz   r{   r|   rI   rZ   r[   rh   NFr}   )	rM   	login_urlr+   r8   r>   rK   rT   rc   r   s	            r    test_check_session_valid_expiredr   %  s      5Iy)D
$
'C -G3GDM!(+L	@-	P 2#lBST//112 U?? 22 2r   c                 ~  K   d}t        |      }t        |      }t        |      }t        |      }t	        |       }t        d|      5  t        dt        |            }t        d	
      |_	        |j                  ddd       d{   }ddd       J t        |t              sJ y7 ## 1 sw Y   "xY ww)uS   비공개(private) 글 발행이 성공하면 URL 문자열을 반환해야 한다.zhttps://testblog.tistory.com/42rz   r{   r|   rI   rZ   r[   rh   Tr      테스트 제목u   <p>본문 내용</p>private)titlecontent
visibilityNr,   r:   r@   rL   rV   r   r   rb   r   r~   publish_post
isinstance)	rM   published_urlr+   r8   r>   rK   rT   rc   post_urls	            r   test_publish_post_privater   =  s      6M}-D
$
'C -G3GDM!(+L	@-	P 	
#lBST#,$#? ))$*  * 
 
	
 h$$$
	
 	
s0   A
B=?B1B/B1B=/B11B:6B=c                   K   d}t        |      }t        |      }t        |      }t        |      }t	        |       }g d}t        d|      5  t        dt        |      	      }t        d
      |_	        |j                  dd|d       d{   }	ddd       	J t        |	t              sJ y7 ## 1 sw Y   "xY ww)uD   태그 목록을 포함하여 글을 발행할 수 있어야 한다.zhttps://testblog.tistory.com/99rz   r{   r|   rI   )python
playwrighttistoryrZ   r[   rh   Tr   u   태그 테스트u   <p>태그 포함 본문</p>r   )r   r   tagsr   Nr   )
rM   r   r+   r8   r>   rK   rT   r   rc   r   s
             r   test_publish_post_with_tagsr   X  s      6M}-D
$
'C -G3GDM!(+L.D	@-	P 	
#lBST#,$#? ))$1 	 * 
 
		
 h$$$
		
 	
s1   ACA B6B4B6C4B66B?;Cc                 \  K   t        |       }t               }t        d|      5  t        dt	        |            }t        d      |_        t        j                  t              5  |j                  dd       d	{    d	d	d	       d	d	d	       y	7 # 1 sw Y   xY w# 1 sw Y   y	xY ww)
uc   세션이 만료된 상태에서 발행을 시도하면 SessionExpiredError가 발생해야 한다.rZ   r[   rh   Fr   u	   테스트   <p>본문</p>r   r   N)rV   rL   r   r   rb   r   r~   r`   rr   r   r   )rM   rT   rK   rc   s       r   !test_publish_post_session_expiredr   t  s      "(+L35M	@-	P O#lBST#,%#@ ]]./ 	O""o"NNN	O	O O
 O	O 	O	O OsG   "B,AB %B<B=BB 		B,BB	B  B)%B,c                   K   t               }t        t        d            |_        t	        |      }t        |      }t        |      }t        |       }t        d|      5  t        dt        |            }t        d	
      |_        t        j                  t              5  |j                  dd       d{    ddd       ddd       y7 # 1 sw Y   xY w# 1 sw Y   yxY ww)uW   발행 중 예기치 않은 오류가 발생하면 PublishError가 발생해야 한다.zSelector timeoutside_effectr{   r|   rI   rZ   r[   rh   Tr   u   실패 테스트r   r   N)r,   r   	Exceptionr!   r:   r@   rL   rV   r   r   rb   r~   r`   rr   r
   r   rM   r+   r8   r>   rK   rT   rc   s          r   test_publish_post_failurer     s     
 D&9=O3PQD
$
'C -G3GDM!(+L	@-	P V#lBST#,$#? ]]<( 	V"");_"UUU	V	V V
 V	V 	V	V VsI   A C*"AC#C:C;C?C	C*CC	CC'#C*c                 J  K   t               }t        |      }t        |      }t        |      }t	        |       }t        d|      5  t        dt        |            }t        d      |_	        |j                  d	d
d       d{    ddd       y7 # 1 sw Y   yxY ww)uY   post_id와 수정할 필드를 전달하면 edit_post()가 정상 완료되어야 한다.r{   r|   rI   rZ   r[   rh   Tr   42u   수정된 제목u   <p>수정된 본문</p>)post_idr   r   N)r,   r:   r@   rL   rV   r   r   rb   r   r~   	edit_postr   s          r   test_edit_post_successr     s      D
$
'C -G3GDM!(+L	@-	P 	
#lBST#,$#?  mm$-  
 	
 	
	
 	

	
	
 	
s0   AB#?BBB	B#BB B#c                   
K   t               }t               t        d      _        t               t        d      _        d

fd}t               }t        |      |_        t        |      |_        t        |      }t        |      }t        |	      }t        |       }t        d
|      5  t        dt        |            }t        d      |_        |j                  d       d{   }	ddd       t        	t              sJ d|	v sJ |	d   dk(  sJ d|	v sJ |	d   dk(  sJ y7 ?# 1 sw Y   >xY ww)uP  get_post_status()가 post_id와 상태 정보를 포함한 dict를 반환해야 한다.

    구현 패턴:
        row = page.locator(row_selector)          ← MagicMock
        title = await row.locator(child).inner_text(timeout=...)   ← chained AsyncMock
        status_text = await row.locator(child).inner_text(timeout=...)
    r   r   u	   비공개r   c                      dz  dk(  rS S )N   r   )selector_child_call_countmock_child_statusmock_child_titles    r   _child_locatorz,test_get_post_status.<locals>._child_locator  s"    Q!##  r   r   r{   r|   rI   rZ   r[   rh   Tr   )r   Nr   statusr   )r,   r   r   r   r'   r:   r@   rL   rV   r   r   rb   r~   get_post_statusr   dict)rM   r+   r   mock_rowr8   r>   rK   rT   rc   r   r   r   r   s             @@@r   test_get_post_statusr     sD     D !{"+9K"L!#,+#F  ! {H ^<H(3DL
$
'C -G3GDM!(+L	@-	P 9#lBST#,$#? **4*88	9 fd###)$$$v(y((( 9	9 9s0   B1E6=D53D34D58;E3D55D>:Ec                 f  K   ddl }d}| dz  }|j                  d       t        |       }t               }t	        d|i      |_        |j                         }|j                         }|j                  |       t               }||_	        t               }	t	        |      |	_
        t	        d      |	_        t               }
t        |	      |
_        t               }t	               |_        t        |      |
_        t!        |
	      }t#        |
      }t%        |      }t'        d|      5  t)        dt+        |            }t	        d      |_        |j/                  t+        |             d{   }ddd       t1        t*              sJ |j3                  d      sJ y7 2# 1 sw Y   1xY ww)u%  이미지 파일 경로를 전달하면 upload_image()가 업로드된 URL을 반환해야 한다.

    구현 패턴:
        async with page.expect_response(...) as response_info:
            await file_input.set_input_files(image_path)
        response = await response_info.value          ← value가 직접 awaitable
        response_json = await response.json()

    `await response_info.value` 는 `.value` 속성이 코루틴/Future여야 한다.
    asyncio.Future를 사용하여 mock_response를 즉시 반환하도록 설정한다.
    r   Nz-https://img1.daumcdn.net/thumb/test_image.pngztest_image.pngsl   PNG

                                                                                                    r   r   Fr{   r|   rI   rZ   r[   rh   T)
image_pathzhttps://)asynciowrite_bytesrV   r   r   rR   get_event_loopcreate_future
set_resultr1   
__aenter__	__aexit__r,   r(   set_input_filesr'   r:   r@   rL   r   r   rb   r~   upload_imager   
startswith)rM   r   expected_url
image_filerT   mock_responseloopresponse_futuremock_response_infomock_cmr+   mock_file_inputr8   r>   rK   rc   r   s                    r   test_upload_image_successr     s     BL ,,J?@!(+L KM"0EFM !!#D&*&8&8&:O}-". kG"0BCG!u5GD$':D  kO&/kO#/:DL
$
'C -G3GDM	@-	P A#lBST#,$#? $$J$@@	A c3>>*%%% A	A As1   D(F1*AF%0F#1F%5.F1#F%%F.*F1c                 :  K   t               }t        |      }t        |      }t        |      }t	        |       }t        d|      5  t        dt        |            }||_        ||_	        ||_
        |j                          d{    ddd       |j                  j                          |j                  j                          |j                  j                          j                  J |j                  J |j                  J y7 # 1 sw Y   xY ww)uQ   close()를 호출하면 page/context/browser 리소스가 정리되어야 한다.r{   r|   rI   rZ   r[   rh   N)r,   r:   r@   rL   rV   r   r   rb   r9   _contextrF   r&   assert_awaited_oncer   s          r   
test_closer   ;  s      D
$
'C -G3GDM!(+L	@-	P #lBST 	iik 	JJ""$II!!#MM%%' 99<<<< 	 s1   AD?DDDBDDDDc                      t        j                  t        j                        } | j                  }d|v sJ d       |d   j
                  dk(  sJ d|d   j
                         y)uD   publish_post()의 visibility 기본값은 'private'이어야 한다.r   u1   publish_post에 visibility 파라미터가 없음r   u-   visibility 기본값이 'private'이 아님: N)inspect	signaturer   r   
parametersdefault)sigparamss     r   test_visibility_default_privater   `  sp     

,99
:C^^F6!V#VV!|$$	1X	6vl7K7S7S6VWX1r   _PIPELINE_IMPORT_ERROR)parse_html_filerun_pipelineu   publish_pipeline 미구현: c                 l    | dz  }|j                  dd       t        |      }d|d   v s
d|d   v sJ yy)	uN   HTML 파일에서 <title> 또는 <h1> 태그의 제목을 추출해야 한다.	post.htmlu   <!DOCTYPE html>
<html>
<head><title>파이썬으로 배우는 Playwright</title></head>
<body><h1>파이썬으로 배우는 Playwright</h1></body>
</html>utf-8encoding
Playwrightr   u	   파이썬N)rQ   _parse_html_filerM   	html_filer   s      r   test_pipeline_parse_html_titler     sZ     ;&I	
    i(F6'?*kVG_.LLL.L*r   c                     | dz  }|j                  dd       t        |      }|d   }t        |t              sJ d|j	                         v sd|j	                         v sJ yy)	u>   HTML meta keywords에서 태그 목록을 추출해야 한다.r   u   <!DOCTYPE html>
<html>
<head>
  <title>테스트</title>
  <meta name="keywords" content="python, playwright, automation">
</head>
<body><p>본문</p></body>
</html>r   r   keywordsr   r   N)rQ   r   r   rb   lower)rM   r   r   r   s       r   test_pipeline_parse_html_tagsr     sz     ;&I	   
 i(Fj!Hh$$$x~~''<8>>;K+KKK+K'r   c                      t        d      } t        j                  t              5  t	        |        ddd       y# 1 sw Y   yxY w)uY   존재하지 않는 HTML 파일을 파싱하면 FileNotFoundError가 발생해야 한다.z(/tmp/nonexistent_tistory_post_12345.htmlN)r   r`   rr   rs   r   )missings    r   test_pipeline_file_not_foundr     s9     =>G	(	) "!" " "s	   :Ac                    K   | dz  }|j                  dd       t        |      }|d   dk(  sJ d|d   v sJ |d	   t        |d	         dkD  sJ y
w)u_   전체 파이프라인 흐름: parse_html_file → run_pipeline → publish_post 호출 확인.r   u   <!DOCTYPE html>
<html>
<head>
  <title>파이프라인 통합 테스트</title>
  <meta name="keywords" content="test, pipeline">
</head>
<body><p>파이프라인 본문입니다.</p></body>
</html>r   r   r   u    파이프라인 통합 테스트testr   bodyNr   )rQ   r   lenr   s      r   test_pipeline_publish_flowr     s     
 ;&I	   
 i(F'?@@@@VJ''''&>%#fVn*=*AAA*As   AA)z+https://testblog.tistory.com/manage/post/42r   )F__doc__r   rR   rj   syspathlibr   unittest.mockr   r   r   r`   pytest_asynciork   get
_WORKSPACErb   pathinsertr   ImportError__annotations__scripts.blog.tistory_publisherr	   r
   r   r   _er*   r   markskipif_SKIP_IF_NO_IMPLr,   r:   r@   rG   rL   rV   fixturerX   rd   rm   rp   r   rv   rx   r   r   r   r   r   r   r   r   r   r   r   r   r   _run_pipelinescripts.blog.publish_pipeliner   r   _pe_SKIP_IF_NO_PIPELINEr   r   r   r   r   r   r   <module>r     s  #J   	 
  5 5   "**..!13HIJ
z?#(("HHOOAs:' %){T! (F  ;;%%)-9 &  & QZ *	Y- 	 		D 0 I y4/ 9 Y-=  (T d  - -
  * 1 1 ! !& "  "      "   , %  %2 %  %4 
O  
O V  V0 
  
4 3)  3)v ;&  ;&F     F X X .2 d* 1 !K& M {{))$&)*@)AB *   M M  L L* " " B   B[  FM4ylBGb9L2YL"EFd  ! !s0   M M: M7%M22M7:N?NN