
     jr2                       d Z ddlmZ ddlZddlmc mZ ddl	Z
ddlZddlZddlmZ ddlZ ee      j#                         j$                  d   Zej(                  j+                  d ee             edz  dz  Ze
j0                  j3                  de      Zeej6                   ej8                  e d	d
       e
j0                  j;                  e      Zej6                  j?                  e       d Z d Z!d Z"d Z#d Z$d Z%y)u   tests/regression/test_refresh_bot_token.py — task-2483 회귀 테스트.

모리건(개발3팀 테스터) 작성.
scripts/refresh_bot_token.py 인터페이스 명세 기반 6항목.
루(Lugh)의 구현이 없으면 전체 SKIP (ImportError guard).
    )annotationsN)Path   scriptszrefresh_bot_token.pyrefresh_bot_tokenu&    미작성 — 루(Lugh) 구현 대기T)allow_module_levelc                 ~   ddl m}  ddlm} |j	                  dd      }|j                  | j                  j                  | j                  j                  | j                               }d}t        j                  d	||
      }ddl}|j                  |      }|j                  |ddi      }|d   }	d}
|	|
k(  }|slt!        j"                  d|fd|	|
f      t!        j$                  |	      t!        j$                  |
      dz  }dd|iz  }t'        t!        j(                  |            dx}	x}}
|d   }	d	}
|	|
k(  }|slt!        j"                  d|fd|	|
f      t!        j$                  |	      t!        j$                  |
      dz  }dd|iz  }t'        t!        j(                  |            dx}	x}}
|d   }	d}||z
  }|	|k(  }|st!        j"                  d|fd|	|f      t!        j$                  |	      dt+        j,                         v st!        j.                  |      rt!        j$                  |      ndt!        j$                  |      dz  }dd|iz  }t'        t!        j(                  |            dx}	x}x}}|d   }	d}||z   }|	|k(  }|st!        j"                  d|fd |	|f      t!        j$                  |	      dt+        j,                         v st!        j.                  |      rt!        j$                  |      ndt!        j$                  |      dz  }dd|iz  }t'        t!        j(                  |            dx}	x}x}}y)!uU   JWT는 RS256으로 발급되며, iat=now-60 (시계 skew 흡수), exp=now+540 (9분).r   serializationrsa     )public_exponentkey_size)encodingformatencryption_algorithmi Se3616524)nowNverify_signatureF)optionsalgRS256==z%(py1)s == %(py4)spy1py4assert %(py6)spy6issiat<   )z%(py1)s == (%(py3)s - %(py5)s)	fixed_now)r   py3py5assert %(py8)spy8expi  )z%(py1)s == (%(py3)s + %(py5)s))cryptography.hazmat.primitivesr   )cryptography.hazmat.primitives.asymmetricr   generate_private_keyprivate_bytesEncodingPEMPrivateFormatPKCS8NoEncryptionrbtgenerate_jwtjwtget_unverified_headerdecode
@pytest_ar_call_reprcompare	_safereprAssertionError_format_explanation@py_builtinslocals_should_repr_global_name)r   r   private_key	pem_bytesr&   tokenpyjwtheaderpayload@py_assert0@py_assert3@py_assert2@py_format5@py_format7@py_assert4@py_assert6@py_format9s                    >/home/jay/workspace/tests/regression/test_refresh_bot_token.py#test_jwt_generation_rs256_with_skewrQ      s-   <=**54*PK))''++**00*779 * I IY	yAE((/Fll5+=u*ElFG%=#G#=G####=G###=###G#######5>&Y&>Y&&&&>Y&&&>&&&Y&&&&&&&5>++Y^+>^++++>^+++>++++++Y+++Y++++++++++5>,,Y_,>_,,,,>_,,,>,,,,,,Y,,,Y,,,,,,,,,,    c                X   | dz  }|j                  d       t        j                  t        |      | dz        }||k(  }|st	        j
                  d|fd||f      dt        j                         v st	        j                  |      rt	        j                  |      nddt        j                         v st	        j                  |      rt	        j                  |      nddz  }t	        j                  d	      d
z   d|iz  }t        t	        j                  |            d}|j                  } |       }|sddt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      t	        j                  |      dz  }t        t	        j                  |            dx}}| dz  }|j                  d       t        j                  t        | dz        |      }|j                  } |       }|st	        j                  d      dz   dt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      t	        j                  |      dz  }t        t	        j                  |            dx}}| dz  }	|	j                  d       |	j                          | dz  }
t        d      }|j                         sCt!        j"                  t$              5  t        j                  t        |	      |
       ddd       yt        j                  t        |	      |
      }|j                  } |       }|st	        j                  d      dz   dt        j                         v st	        j                  |      rt	        j                  |      ndt	        j                  |      t	        j                  |      dz  }t        t	        j                  |            dx}}y# 1 sw Y   yxY w)u  PEM 경로 우선순위 및 fallback 동작 검증.

    인터페이스 명세: env_path 존재 → 그 경로 반환. env_path None/미존재 → fallback.
    둘 다 없으면 FileNotFoundError.

    구현 align 주석: resolve_pem_path() 내부에 하드코딩된
    /home/jay/.secrets/... 경로가 candidates 중간에 삽입된다.
    이 파일이 실서버에서 실제 존재하므로, env_path=None 시 항상 존재하는 경로가 반환된다.
    FileNotFoundError case는 CI(실서버 .secrets 없음) 환경에서만 재현 가능.
    → 환경 독립적으로 검증 가능한 case만 PASS 기준으로 설정.
    zprimary.pemprimaryzfallback.pem)fallback_pathr   )z%(py0)s == %(py2)sresultpy0py2u(   env_path 존재 시 해당 경로 반환
>assert %(py4)sr    NzAassert %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.exists
}()
}rX   rY   r    
backup.pemdummyzmissing.pemu%   후보 중 존재하는 경로 반환zC
>assert %(py4)s
{%(py4)s = %(py2)s
{%(py2)s = %(py0)s.exists
}()
}returnedz	ghost.pemxznone_fallback.pemzG/home/jay/.secrets/jeon-jonghyuk-taskctl-bot.2026-05-05.private-key.pemu5   hardcoded fallback 경로 반환 시 존재해야 함returned_hardcoded)
write_textr5   resolve_pem_pathstrr:   r;   r?   r@   rA   r<   _format_assertmsgr=   r>   existsunlinkr   pytestraisesFileNotFoundError)tmp_pathrT   rV   @py_assert1@py_format3rK   rI   fallbackr^   ghostnone_fallback	hardcodedr`   s                rP   #test_pem_fallback_when_main_missingrq   <   s    &Gy!!!#g,h>W!XFWHHH6WHHHHHH6HHH6HHHHHHWHHHWHHHHHHHHHHH ===??66=? ,&H ##C=(@$AQY#ZH??E?EEEEEEEEEE8EEE8EEE?EEEEEEEEE {"E	S	LLN22M ^_I]],- 	J  U= I	J 	J !11#e*M1Z!((c(*c*cc,ccccccc!ccc!ccc(ccc*cccccc	J 	Js   !P  P)c                   | dz  }d}|j                  d| d       | dz  }ddlm} ddlm} |j                  d	d
      j                  |j                  j                  |j                  j                  |j                               }| dz  }|j                  |       d }	|j                  t        d|	       |j                  t        d|       t        j                  dt!        |      dt!        |      g      }
d}|
|k(  }|st#        j$                  d|fd|
|f      dt'        j(                         v st#        j*                  |
      rt#        j,                  |
      ndt#        j,                  |      dz  }t#        j.                  d      dz   d|iz  }t1        t#        j2                  |            dx}}|j4                  } |       }||v }|st#        j$                  d|fd||f      dt'        j(                         v st#        j*                  |      rt#        j,                  |      nddt'        j(                         v st#        j*                  |      rt#        j,                  |      ndt#        j,                  |      t#        j,                  |      dz  }dd |iz  }t1        t#        j2                  |            dx}x}}|j5                         j7                         D cg c](  }|j9                         st;        j<                  |      * }}d! |D        }t?        |      }|sd"d#t'        j(                         v st#        j*                  t>              rt#        j,                  t>              nd#t#        j,                  |      t#        j,                  |      d$z  }t1        t#        j2                  |            dx}}yc c}w )%uI   API 401 → main() exit 1 + .env.keys 미수정 + audit refresh_rejected.	.env.keysghs_OLDoldoldoldoldOLDOLDzyBOT_GITHUB_APP_ID=3616524
BOT_GITHUB_INSTALLATION_ID=129882070
BOT_GITHUB_PRIVATE_KEY_PATH=/nonexistent
BOT_GITHUB_TOKEN=
audit.jsonlr   r
   r   r   r   r\   c                    t        dd      )Ni  zBad credentials)RuntimeError
_jwt_token_installation_id_kwargss      rP   fake_requestzBtest_api_401_fail_closed_preserves_old_token.<locals>.fake_request   s    3 122rR   request_installation_tokenDEFAULT_PEM_BACKUP
--env-keys--audit-path   r   z%(py0)s == %(py3)srcrX   r'   u   fail-closed 시 exit 1z
>assert %(py5)sr(   NinzH%(py0)s in %(py6)s
{%(py6)s = %(py4)s
{%(py4)s = %(py2)s.read_text
}()
}	old_tokenenv_keysrX   rY   r    r"   r)   r*   c              3  *   K   | ]  }|d    dv   yw)status)refresh_rejected	api_errorN ).0lines     rP   	<genexpr>z?test_api_401_fail_closed_preserves_old_token.<locals>.<genexpr>   s     [ttH~!BB[s   z,assert %(py4)s
{%(py4)s = %(py0)s(%(py2)s)
}anyr[   ) ra   r,   r   r-   r   r.   r/   r0   r1   r2   r3   r4   write_bytessetattrr5   mainrc   r:   r;   r?   r@   rA   r<   rd   r=   r>   	read_text
splitlinesstripjsonloadsr   )rj   monkeypatchr   r   
audit_pathr   r   pempem_pathr}   r   rJ   rk   @py_format4@py_format6rI   @py_assert5rL   rO   laudit_linesrK   s                         rP   ,test_api_401_fail_closed_preserves_old_tokenr   q   s   +%H+I &;b	* M)J<=

"
"5$
/
=
=""##))""$C
 ,&H3 9<H18<	c(mJ 
B
 ,27,,,2,,,,,,2,,,2,,,,,,,,,,,,, **,*,,9,,,,,9,,,,,,,9,,,9,,,,,,,,,,,,*,,,,,,,,,,,*4*>*>*@*K*K*M[QQRQXQXQZ4::a=[K[[{[[3[[[[[[[[[3[[[3[[[[[[[[[[[[[[ \s   "O8Oc                   | dz  }t         j                  |ddd       t         j                  |dddd	       t         j                  |d
dd       |j                         j                         D cg c](  }|j	                         st        j                  |      * }}t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t              rt        j                  t              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}}|d   d   }	d}
|	|
k(  }|slt        j                  d|fd|	|
f      t        j                  |	      t        j                  |
      dz  }dd|iz  }t        t        j                  |            dx}	x}}
|d   d   }	d}
|	|
k(  }|slt        j                  d|fd|	|
f      t        j                  |	      t        j                  |
      dz  }dd|iz  }t        t        j                  |            dx}	x}}
|d   d   }	d
}
|	|
k(  }|slt        j                  d|fd|	|
f      t        j                  |	      t        j                  |
      dz  }dd|iz  }t        t        j                  |            dx}	x}}
yc c}w )u0   append_audit 호출 N번 → N개 라인 누적.rv   	refreshedabc12345z2026-05-07T23:00:00Z)r   sha256_prefix
expires_atr   NzHTTP 401)r   r   r   errordry_rundef67890z2026-05-07T23:30:00Z   r   )z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenlines)rX   r   r'   r"   r)   r*   r   r   r   r   r!   r"   r   r   )r5   append_auditr   r   r   r   r   r   r:   r;   r?   r@   rA   r<   r=   r>   )rj   r   r   r   rJ   r   rM   rL   rO   rH   rI   rK   s               rP   test_audit_append_onlyr      s$   M)JZ:ZpqZ(:$[_gqrZ	Xno$.$8$8$:$E$E$GUq1779TZZ]UEUu::?:33uu:8H,,,,,,,,,,,,,,,,,,,8H3!33!33333!3333333!333333338H*******************	 Vs   2L;L;c                	   | dz  }|j                  d       | dz  }ddlm} ddlm} |j                  dd      j                  |j                  j                  |j                  j                  |j                               }| d	z  }|j                  |       d
fd}	|j                  t        d|	       |j                  t        d|       t        j                  dt!        |      dt!        |      g      }
d}|
|k(  }|st#        j$                  d|fd|
|f      dt'        j(                         v st#        j*                  |
      rt#        j,                  |
      ndt#        j,                  |      dz  }dd|iz  }t/        t#        j0                  |            dx}}|j3                         }|j4                  |j6                  z   }|v}|st#        j$                  d|fd|f      dt'        j(                         v st#        j*                        rt#        j,                        nddt'        j(                         v st#        j*                  |      rt#        j,                  |      nddz  }t#        j8                  d      dz   d|iz  }t/        t#        j0                  |            d}|j:                  } |       }|v}|st#        j$                  d|fd|f      dt'        j(                         v st#        j*                        rt#        j,                        ndd t'        j(                         v st#        j*                  |      rt#        j,                  |      nd t#        j,                  |      t#        j,                  |      d!z  }t#        j8                  d"      d#z   d$|iz  }t/        t#        j0                  |            dx}x}}|j:                  } |       }|v }|st#        j$                  d%|fd&|f      dt'        j(                         v st#        j*                        rt#        j,                        ndd't'        j(                         v st#        j*                  |      rt#        j,                  |      nd't#        j,                  |      t#        j,                  |      d!z  }d(d$|iz  }t/        t#        j0                  |            dx}x}}y))uM   성공 시 stdout/audit 어디에도 토큰 원문이 등장하지 않는다.rs   zBOT_GITHUB_APP_ID=3616524
BOT_GITHUB_INSTALLATION_ID=129882070
BOT_GITHUB_PRIVATE_KEY_PATH=/nonexistent
BOT_GITHUB_TOKEN=ghs_OLDOLDOLD
rv   r   r
   r   r   r   r\   'ghs_VERYSECRETtokenABCDEFGHIJ0123456789c                    ddS )N2026-05-08T00:00:00ZrD   r   r   )rz   r{   r|   secret_tokens      rP   r}   z7test_token_never_logged_plaintext.<locals>.fake_request   s    %5KLLrR   r~   r   r   r   r   r   r   r   zassert %(py5)sr(   N)not in)z%(py0)s not in %(py2)sr   outrW   u,   stdout/stderr에 토큰 원문 노출 금지rZ   r    )zL%(py0)s not in %(py6)s
{%(py6)s = %(py4)s
{%(py4)s = %(py2)s.read_text
}()
}r   r   u*   audit jsonl에 토큰 원문 기록 금지z
>assert %(py8)sr*   r   r   r   r)   )ra   r,   r   r-   r   r.   r/   r0   r1   r2   r3   r4   r   r   r5   r   rc   r:   r;   r?   r@   rA   r<   r=   r>   
readouterrr   errrd   r   )rj   capsysr   r   r   r   r   r   r   r}   r   rJ   rk   r   r   capturedr   rl   rK   rI   r   rL   rO   r   s                          @rP   !test_token_never_logged_plaintextr      s   +%H	+ M)J<=

"
"5$
/
=
=""##))""$C
 ,&H<LM 9<H18<	c(mJ 
B N27NNN2NNNNNN2NNN2NNNNNNNNNN  "H
,,
%Cs"RRR<sRRRRRR<RRR<RRRRRRsRRRsRRRR$RRRRRRR)33c35c<55ccc<5cccccc<ccc<cccccczccczccc3ccc5ccc7cccccccc#--/-//</////<///////<///<//////8///8///-///////////rR   c                   | dz  }|j                  d       | dz  }ddlm} ddlm} |j                  dd      j                  |j                  j                  |j                  j                  |j                               }| d	z  }|j                  |       d
 }|j                  t        d|       |j                  t        d|       t        j                  }	ddt!        |      dt!        |      g}
 |	|
      }d}||k(  }|st#        j$                  d|fd||f      dt'        j(                         v st#        j*                  t              rt#        j,                  t              ndt#        j,                  |	      t#        j,                  |
      t#        j,                  |      t#        j,                  |      dz  }dd|iz  }t/        t#        j0                  |            dx}	x}
x}x}}t        j                  }	dt!        |      dt!        |      g}
 |	|
      }d}||k(  }|st#        j$                  d|fd||f      dt'        j(                         v st#        j*                  t              rt#        j,                  t              ndt#        j,                  |	      t#        j,                  |
      t#        j,                  |      t#        j,                  |      dz  }dd|iz  }t/        t#        j0                  |            dx}	x}
x}x}}y)uJ   성공 케이스에서 main()은 0 반환 → systemd Type=oneshot 정상.rs   zBOT_GITHUB_APP_ID=3616524
BOT_GITHUB_INSTALLATION_ID=129882070
BOT_GITHUB_PRIVATE_KEY_PATH=/nonexistent
BOT_GITHUB_TOKEN=ghs_OLDOLD
rv   r   r
   r   r   r   r\   c                    dddS )N
ghs_NEWNEWr   r   r   ry   s      rP   r}   z?test_systemd_oneshot_compatible_exit_zero.<locals>.fake_request   s    %5KLLrR   r~   r   z	--dry-runr   r   r   )zJ%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.main
}(%(py4)s)
} == %(py9)sr5   )rX   rY   r    r"   py9zassert %(py11)spy11N)ra   r,   r   r-   r   r.   r/   r0   r1   r2   r3   r4   r   r   r5   r   rc   r:   r;   r?   r@   rA   r<   r=   r>   )rj   r   r   r   r   r   r   r   r}   rk   rI   r   @py_assert8@py_assert7@py_format10@py_format12s                   rP   )test_systemd_oneshot_compatible_exit_zeror      s    +%H	( M)J<=

"
"5$
/
=
=""##))""$C
 ,&HM 9<H18< 88e[,H~sS]_e8_`edee`deeeee`deeeeeee3eee3eee8eee_eee`eeedeeeeeeee88X\3x=.#j/RX8RSXWXXSWXXXXXSWXXXXXXX3XXX3XXX8XXXRXXXSXXXWXXXXXXXXXrR   )&__doc__
__future__r   builtinsr?   _pytest.assertion.rewrite	assertionrewriter:   importlib.util	importlibr   syspathlibr   rg   __file__resolveparents_WORKSPACE_ROOTpathinsertrc   	_RBT_PATHutilspec_from_file_location_specloaderskipmodule_from_specr5   exec_modulerQ   rq   r   r   r   r   r   rR   rP   <module>r      s    #      
  x.((*2215 3' (i'*@@	../BIN=ELL(FKK9+CDY]^nn%%e,    -:.dj'\\+$(0^YrR   