o
    3iXb                     @   sB  d dl Z d dlZd dlmZm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mZmZmZmZmZmZmZmZmZmZmZ d dlmZ ejdg ddd ZG d	d
 d
ejZG dd dejZejdg ddd Z dd Z!dd Z"dd Z#dd Z$G dd dejZ%G dd dejZ&G dd dejZ'dS )    N)datedatetime)APIErrorDatetimeSerializerGetResponseKEEP_ALIVE_SOCKET_OPTIONSQuotaLimitError_mask_tokens_in_url
batch_postdecidedetermine_server_hostdisable_connection_reuseenable_keep_aliveflagsgetset_socket_options)TEST_API_KEYzurl, expected))zAhttps://example.com/api/flags?token=phc_abc123xyz789&send_cohortsz>https://example.com/api/flags?token=phc_abc123...&send_cohorts)z4https://example.com/api/flags?token=phc_abc123xyz789z1https://example.com/api/flags?token=phc_abc123...))https://example.com/api/flags?other=valuer   ))https://example.com/api/flags?token=shortr   )z.https://example.com/api/flags?token=1234567890z1https://example.com/api/flags?token=1234567890...c                 C      t | |ksJ d S N)r	   )urlexpected r   g/lsinfo/ai/hellotax_ai/llm_service/venv_embed/lib/python3.10/site-packages/posthog/test/test_request.pytest_mask_tokens_in_url   s   r   c                   @   sT   e Zd Zdd Zdd Zdd Zdd Zd	d
 Zdd Zdd Z	dd Z
dd ZdS )TestRequestsc                 C   s(   t tddddgd}| |jd d S )Ndistinct_idpython eventtrackr   eventtypebatch   r
   r   assertEqualstatus_codeselfresr   r   r   test_valid_request@   s   
zTestRequests.test_valid_requestc                 C   s   |  ttdddd d S )N
testsecrethttps://t.posthog.comFz[{]assertRaises	Exceptionr
   r*   r   r   r   test_invalid_request_errorI   s   z'TestRequests.test_invalid_request_errorc                 C   s   | j ttddg d d S )Nr-   t.posthog.com/r#   r/   r2   r   r   r   test_invalid_hostN   s   

zTestRequests.test_invalid_hostc              	   C   s6   dt dddddddi}tj|td	}| |d
 d S )Ncreatedi                 i clsz){"created": "2012-03-04T05:06:07.891011"})r   jsondumpsr   r'   )r*   dataresultr   r   r   test_datetime_serializationS   s   z(TestRequests.test_datetime_serializationc                 C   s:   t  }d|i}tj|td}d|  }| || d S )Nr6   r<   z{"created": "%s"})r   todayr>   r?   r   	isoformatr'   )r*   rC   r@   rA   r   r   r   r   test_date_serializationX   s
   z$TestRequests.test_date_serializationc                 C   s*   t tddddgdd}| |jd d S )Nr   r   r   r       r$   timeoutr%   r&   r)   r   r   r   test_should_not_timeout_   s   
z$TestRequests.test_should_not_timeoutc                 C   sJ   |  tj tdddddgdd W d    d S 1 sw   Y  d S )Nkeyr   r   r   r    g-C6?rG   )r0   requestsReadTimeoutr
   r2   r   r   r   test_should_timeouti   s   "z TestRequests.test_should_timeoutc              	   C   s   t  }d|_tdgi i ddd|_tjd|d3 | 	t
}tdd	 W d    n1 s3w   Y  | |jjd | |jjd
 W d    d S 1 sSw   Y  d S )Nr%   feature_flagsFquotaLimitedfeatureFlagsfeatureFlagPayloadserrorsWhileComputingFlagsutf-8posthog.request._session.postreturn_valuefake_key	fake_hostzFeature flags quota limited)rK   Responser(   r>   r?   encode_contentmockpatchr0   r   r   r'   	exceptionstatusmessage)r*   mock_responsecmr   r   r   test_quota_limited_responsew   s$   	"z(TestRequests.test_quota_limited_responsec                 C   s~   t  }d|_tddii ddd|_tjd|d t	d	d
}| 
|d ddi W d    d S 1 s8w   Y  d S )Nr%   flag1TFrQ   rR   rS   rT   rU   rV   rX   rY   rQ   )rK   rZ   r(   r>   r?   r[   r\   r]   r^   r   r'   )r*   rb   responser   r   r   test_normal_decide_response   s   
"z(TestRequests.test_normal_decide_responseN)__name__
__module____qualname__r,   r3   r5   rB   rE   rI   rM   rd   rh   r   r   r   r   r   ?   s    	
r   c                   @   s   e Zd ZdZeddd Zeddd Zeddd Zedd	d
 Z	eddd Z
eddd Zeddd Zeddd Zeddd Zeddd Zeddd Zeddd ZdS )TestGetz6Unit tests for the get() function HTTP-level behavior.zposthog.request._session.getc                 C   s   t  }d|_d|jd< tdddigid|_||_t	dd	d
d}| 
|t | |jdddigi | |jd | |j dS )zDTest that get() returns GetResponse with data and etag from headers.r%   z"abc123"ETagr   rJ   	test-flagrT   api_key	/test-urlhttps://example.comhostN)rK   rZ   r(   headersr>   r?   r[   r\   rW   r   assertIsInstancer   r'   r@   etagassertFalsenot_modifiedr*   mock_getrb   rg   r   r   r   test_get_returns_data_and_etag   s   
z&TestGet.test_get_returns_data_and_etagc                 C   sf   t  }d|_d|jd< tdg id|_||_t	dddd	d
 |j
d }| |d d d	 dS )zGTest that If-None-Match header is sent when etag parameter is provided.r%   z
"new-etag"rm   r   rT   ro   rp   rq   z"previous-etag"rs   rv      rt   If-None-MatchN)rK   rZ   r(   rt   r>   r?   r[   r\   rW   r   	call_argsr'   r*   rz   rb   call_kwargsr   r   r   6test_get_sends_if_none_match_header_when_etag_provided   s   

z>TestGet.test_get_sends_if_none_match_header_when_etag_providedc                 C   sV   t  }d|_tdg id|_||_tdddd |j	d }| 
d	|d
  dS )zATest that If-None-Match header is not sent when no etag provided.r%   r   rT   ro   rp   rq   rr   r}   r~   rt   N)rK   rZ   r(   r>   r?   r[   r\   rW   r   r   assertNotInr   r   r   r   1test_get_does_not_send_if_none_match_when_no_etag   s   
z9TestGet.test_get_does_not_send_if_none_match_when_no_etagc                 C   sd   t  }d|_d|jd< ||_tddddd}| |t | |j	 | 
|jd | |j dS )	zKTest that 304 Not Modified response returns not_modified=True with no data.0  z"unchanged-etag"rm   ro   rp   rq   r|   N)rK   rZ   r(   rt   rW   r   ru   r   assertIsNoner@   r'   rv   
assertTruerx   ry   r   r   r   !test_get_handles_304_not_modified   s   
z)TestGet.test_get_handles_304_not_modifiedc                 C   sB   t  }d|_||_tddddd}| |j | |jd dS )zFTest that 304 response without ETag header falls back to request etag.r   ro   rp   rq   z"original-etag"r|   N)	rK   rZ   r(   rW   r   r   rx   r'   rv   ry   r   r   r   2test_get_304_without_etag_header_uses_request_etag   s   z:TestGet.test_get_304_without_etag_header_uses_request_etagc                 C   sf   t  }d|_tdg id|_||_tdddd}| 	|j
 | |j | |jdg i dS )	zATest that 200 response without ETag header returns None for etag.r%   r   rT   ro   rp   rq   rr   N)rK   rZ   r(   r>   r?   r[   r\   rW   r   rw   rx   r   rv   r'   r@   ry   r   r   r    test_get_200_without_etag_header   s   z(TestGet.test_get_200_without_etag_headerc                 C   s   t  }d|_tddid|_||_| t	}t
dddd W d	   n1 s,w   Y  | |jjd | |jjd d	S )
z)Test that error responses raise APIError.i  detailUnauthorizedrT   bad_keyrp   rq   rr   N)rK   rZ   r(   r>   r?   r[   r\   rW   r0   r   r   r'   r_   r`   ra   )r*   rz   rb   ctxr   r   r   (test_get_error_response_raises_api_error   s   z0TestGet.test_get_error_response_raises_api_errorc                 C   sV   t  }d|_ti d|_||_tdddd |j	d }| 
|d d	 d
 dS )z9Test that Authorization header is sent with Bearer token.r%   rT   z
my-api-keyrp   rq   rr   r}   rt   AuthorizationzBearer my-api-keyNrK   rZ   r(   r>   r?   r[   r\   rW   r   r   r'   r   r   r   r   #test_get_sends_authorization_header  s   
z+TestGet.test_get_sends_authorization_headerc                 C   sj   t  }d|_ti d|_||_tdddd |j	d }| 
d|d	  | |d	 d d
 dS )z$Test that User-Agent header is sent.r%   rT   ro   rp   rq   rr   r}   z
User-Agentrt   zposthog-python/N)rK   rZ   r(   r>   r?   r[   r\   rW   r   r   assertInr   
startswithr   r   r   r    test_get_sends_user_agent_header  s   
z(TestGet.test_get_sends_user_agent_headerc                 C   sT   t  }d|_ti d|_||_tddddd |j	d }| 
|d	 d d
S )z5Test that timeout parameter is passed to the request.r%   rT   ro   rp   rq      )rs   rH   r}   rH   Nr   r   r   r   r   test_get_passes_timeout$  s   
zTestGet.test_get_passes_timeoutc                 C   R   t  }d|_ti d|_||_tdddd |j	d }| 
|d d d	S )
z.Test that host and url are combined correctly.r%   rT   ro   
/api/flagsrq   rr   r   https://example.com/api/flagsNr   r*   rz   rb   r   r   r   r   test_get_constructs_full_url1     
z$TestGet.test_get_constructs_full_urlc                 C   r   )
z.Test that trailing slash is removed from host.r%   rT   ro   r   zhttps://example.com/rr   r   r   Nr   r   r   r   r   )test_get_removes_trailing_slash_from_host>  r   z1TestGet.test_get_removes_trailing_slash_from_hostN)ri   rj   rk   __doc__r]   r^   r{   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rl      s4    










rl   zhost, expected))r.   r.   )https://t.posthog.com/r   )t.posthog.comr   )r4   r4   )#https://us.posthog.com.rg.proxy.comr   )app.posthog.comr   )eu.posthog.comr   )zhttps://app.posthog.comhttps://us.i.posthog.com)zhttps://eu.posthog.comhttps://eu.i.posthog.com)zhttps://us.posthog.comr   )zhttps://app.posthog.com/r   )zhttps://eu.posthog.com/r   )zhttps://us.posthog.com/r   )Nr   c                 C   r   r   )r   )rs   r   r   r   r   test_routing_to_custom_hostL  s   r   c                  C   sD   zt   ddlm}  | d}|jtksJ W td  d S td  w Nr   )_sessionrq   )r   posthog.requestr   get_adaptersocket_optionsr   r   r   adapterr   r   r   *test_enable_keep_alive_sets_socket_optionsc  s   
r   c                  C   sL   z t   td  ddlm}  | d}|jd u sJ W td  d S td  w r   )r   r   r   r   r   r   r   r   r   r   (test_set_socket_options_clears_with_nonen  s   
r   c                  C   s8   zt   t } t }| |usJ W dt_d S dt_w )NT)r   request_module_get_session_pooling_enabledsession1session2r   r   r   4test_disable_connection_reuse_creates_fresh_sessionsz  s   r   c                  C   s>   zt   tj} t   tj}| |u sJ W td  d S td  w r   )r   r   r   r   r   r   r   r   %test_set_socket_options_is_idempotent  s   r   c                   @   sD   e Zd ZdZdd Zdd Zeddd Zedd	d
 Z	dS )TestFlagsSessionz&Tests for flags session configuration.c                 C      ddl m} | d| dS )zBVerify 429 (rate limit) is NOT retried - need to wait, not hammer.r   RETRY_STATUS_FORCELIST  Nr   r   r   r*   r   r   r   r   0test_retry_status_forcelist_excludes_rate_limits     zATestFlagsSession.test_retry_status_forcelist_excludes_rate_limitsc                 C   r   )zCVerify 402 (payment required/quota) is NOT retried - won't resolve.r   r     Nr   r   r   r   r   1test_retry_status_forcelist_excludes_quota_errors  r   zBTestFlagsSession.test_retry_status_forcelist_excludes_quota_errorsz"posthog.request._get_flags_sessionc                 C   sz   t  }d|_tddii ddd|_t }||j	_
||_
tddd	d
}| |d d d |  |j	  dS )zBflags() uses the dedicated flags session, not the general session.r%   rn   TFrf   rT   test-keyhttps://test.posthog.comuser123r   rQ   N)rK   rZ   r(   r>   r?   r[   r\   r]   	MagicMockpostrW   r   r'   assert_called_once)r*   mock_get_flags_sessionrb   mock_sessionrA   r   r   r   test_flags_uses_flags_session  s"   z.TestFlagsSession.test_flags_uses_flags_sessionc                 C   s   t  }d|_tdgi i ddd|_t }||j	_
||_
| t tdddd	 W d
   n1 s8w   Y  | |j	jd d
S )zGflags() raises QuotaLimitError without retrying (at application level).r%   rN   FrO   rT   r   r   r   r   Nr}   )rK   rZ   r(   r>   r?   r[   r\   r]   r   r   rW   r0   r   r   r'   
call_count)r*   r   rb   r   r   r   r   "test_flags_no_retry_on_quota_limit  s$   	z3TestFlagsSession.test_flags_no_retry_on_quota_limitN)
ri   rj   rk   r   r   r   r]   r^   r   r   r   r   r   r   r     s    
r   c                   @   s(   e Zd ZdZdd Zdd Zdd ZdS )	TestFlagsSessionNetworkRetriesz7Tests for network failure retries in the flags session.c                 C   sf   ddl m} | }|d}|j}| |jdd | |jdd | |jdd | d|j	d	 d
S )a(  
        Verify that the flags session is configured to retry on connection errors.

        The urllib3 Retry adapter with connect=2 and read=2 automatically
        retries on network-level failures (DNS failures, connection refused,
        connection reset, etc.) up to 2 times each.
        r   _build_flags_sessionr      zShould have 2 total retriesz$Should retry connection errors twicezShould retry read errors twicePOSTzShould allow POST retriesN)
r   r   r   max_retriesr'   totalconnectreadr   allowed_methodsr*   r   sessionr   retryr   r   r   :test_flags_session_retry_config_includes_connection_errors  s   
zYTestFlagsSessionNetworkRetries.test_flags_session_retry_config_includes_connection_errorsc                 C   s   ddl m}m} | }|d}|j}| t|jt|d | d|j | d|j | d|j | d|j | 	d	|j | 	d
|j dS )z
        Verify that transient server errors (5xx) trigger retries.

        This tests the status_forcelist configuration which specifies
        which HTTP status codes should trigger a retry.
        r   )r   r   r   z'Should retry on transient server errorsi  i    i  r   r   N)
r   r   r   r   r   r'   setstatus_forcelistr   r   )r*   r   r   r   r   r   r   r   r   +test_flags_session_retries_on_server_errors  s   
zJTestFlagsSessionNetworkRetries.test_flags_session_retries_on_server_errorsc                 C   s6   ddl m} | }|d}|j}| |jdd dS )zW
        Verify that retries use exponential backoff to avoid thundering herd.
        r   r   r   g      ?z0Should use 0.5s backoff factor (0.5s, 1s delays)N)r   r   r   r   r'   backoff_factorr   r   r   r   test_flags_session_has_backoff  s   
z=TestFlagsSessionNetworkRetries.test_flags_session_has_backoffN)ri   rj   rk   r   r   r   r   r   r   r   r   r     s
    r   c                   @   s    e Zd ZdZdd Zdd ZdS ) TestFlagsSessionRetryIntegrationzHIntegration tests that verify actual retry behavior with a local server.c              	      s*  ddl }ddlm}m} ddlm} ddlm} ddlm	}m
} d G  fddd|}G d	d
 d
||}	|	d|}
|
jd }|j|
jd}d|_|  z>||dddd|dgdd}t }|d| |jd| dddidd}| |jd |  d W |
  |
  dS |
  |
  w )z
        Verify that 503 errors trigger retries and eventually succeed.

        Uses a local HTTP server that fails twice with 503, then succeeds.
        This tests the full retry flow including backoff timing.
        r   N)
HTTPServerBaseHTTPRequestHandler)ThreadingMixInRetryHTTPAdapterWithSocketOptionsr   c                       s$   e Zd ZdZ fddZdd ZdS )z\TestFlagsSessionRetryIntegration.test_retries_on_503_then_succeeds.<locals>.RetryTestHandlerzHTTP/1.1c                    s    d7  t | jdd}|dkr| j|  dkr>| d | dd d}| dtt| | 	  | j
| d S | d	 | dd d
}| dtt| | 	  | j
| d S )Nr}   zContent-Lengthr   r   r   zContent-Typezapplication/jsons    {"error": "Service unavailable"}r%   s;   {"featureFlags": {"test": true}, "featureFlagPayloads": {}})intrt   r   rfiler   send_responsesend_headerstrlenend_headerswfilewrite)r*   content_lengthbodyrequest_countr   r   do_POST+  s$   

zdTestFlagsSessionRetryIntegration.test_retries_on_503_then_succeeds.<locals>.RetryTestHandler.do_POSTc                 W   s   d S r   r   )r*   formatargsr   r   r   log_messageE  s   zhTestFlagsSessionRetryIntegration.test_retries_on_503_then_succeeds.<locals>.RetryTestHandler.log_messageN)ri   rj   rk   protocol_versionr   r   r   r   r   r   RetryTestHandler(  s    r   c                   @   s   e Zd ZdZdS )z^TestFlagsSessionRetryIntegration.test_retries_on_503_then_succeeds.<locals>.ThreadedHTTPServerTN)ri   rj   rk   daemon_threadsr   r   r   r   ThreadedHTTPServerI  s    r   z	127.0.0.1r   r}   )targetTr   g{Gz?r   r   r   r   r   r   r   r   http://http://127.0.0.1:/flags/?v=2r   r   r9   r>   rH   r%   r7   )	threadinghttp.serverr   r   socketserverr   urllib3.util.retryr   r   r   r   server_addressThreadserve_foreverdaemonstartrK   Sessionmountr   r'   r(   shutdownserver_close)r*   r	  r   r   r   r   r   r   r   r   serverportserver_threadr   r   rg   r   r   r   !test_retries_on_503_then_succeeds  sJ   !




zBTestFlagsSessionRetryIntegration.test_retries_on_503_then_succeedsc              	   C   s   ddl }ddl}ddlm} ddlm}m} | |j|j}|	d |
 d }|  ||dddd|d	gd
d}t }	|	d| | }
| tjj |	jd| dddidd W d   n1 siw   Y  | |
 }| |dd dS )z
        Verify that connection errors (no server) trigger retries.

        Binds a socket to get a guaranteed available port, then closes it
        so connection attempts fail with ConnectionError.
        r   Nr   r   r  r}   r   g?r   r  r  r  r  r  r   r   r  z#Should have some delay from retries)sockettimer  r   r   r   r   AF_INETSOCK_STREAMbindgetsocknamecloserK   r  r  r0   
exceptionsConnectionErrorr   assertGreater)r*   r  r  r   r   r   sockr  r   r   r  elapsedr   r   r   "test_connection_errors_are_retriedp  s<   


zCTestFlagsSessionRetryIntegration.test_connection_errors_are_retriedN)ri   rj   rk   r   r  r&  r   r   r   r   r     s    Wr   )(r>   unittestr   r   r]   pytestrK   r   requestr   r   r   r   r   r   r	   r
   r   r   r   r   r   r   r   posthog.test.test_utilsr   markparametrizer   TestCaser   rl   r   r   r   r   r   r   r   r   r   r   r   r   <module>   s:    @
[ 3

?H