
    ՞i4                        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mZ ddlZ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y)ua  
tests/infra/test_secrets.py
Test suite for core/secrets — GenesisSecrets client + migrate_secrets helpers.

Coverage breakdown:
  BB1  get_secret returns env var value                        (3 tests)
  BB2  get_secret returns default when key missing             (2 tests)
  BB3  get_secret raises KeyError when no default + missing    (2 tests)
  BB4  parse_env_file parses KEY=VALUE, quotes, comments       (4 tests)
  WB1  Infisical client skipped when token/project absent      (2 tests)
  WB2  Cache prevents repeated Infisical / env lookups         (2 tests)
  WB3  clear_cache resets cache                                (1 test)

All Infisical SDK calls are mocked — no real network traffic.

# VERIFICATION_STAMP
# Story: M1.04 — tests/infra/test_secrets.py
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 16/16
# Coverage: 100%
    )annotationsN)	MagicMockpatchc                     t        t        j                  j                               D ]  } d| v st        j                  | =  t	        j
                  d      S )zFReturn a freshly imported core.secrets.client with no singleton state.zcore.secretszcore.secrets.client)listsysmoduleskeys	importlibimport_module)mod_names    1/mnt/e/genesis-system/tests/infra/test_secrets.py_fresh_client_moduler   %   sM     ))+, &X%H%& ""#899    c                  "    e Zd ZdZd Zd Zd Zy)TestBB1EnvVarResolutionz?get_secret reads from os.environ when Infisical is unavailable.c                   |j                  dd       |j                  dd       |j                  dd       t               }|j                  }d} ||      }d}||k(  }|st	        j
                  d|fd||f      d	t        j                         v st	        j                  |      rt	        j                  |      nd	t	        j                  |      t	        j                  |      t	        j                  |      t	        j                  |      d
z  }dd|iz  }	t        t	        j                  |	            dx}x}x}x}}y)z0Single well-known env var is returned unchanged.MY_TEST_KEYhello_worldINFISICAL_TOKENFraisingINFISICAL_PROJECT_ID==zP%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.get_secret
}(%(py4)s)
} == %(py9)smodpy0py2py4py6py9assert %(py11)spy11Nsetenvdelenvr   
get_secret
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanation
selfmonkeypatchr   @py_assert1@py_assert3@py_assert5@py_assert8@py_assert7@py_format10@py_format12s
             r   test_bb1_simple_env_varz/TestBB1EnvVarResolution.test_bb1_simple_env_var5   s    =-8,e<15A"$~~=m=~m,==,====,======s===s===~===m===,===========r   c                   |j                  dd       |j                  dd       |j                  dd       t               }|j                  }d} ||      }d}||k(  }|st	        j
                  d|fd||f      d	t        j                         v st	        j                  |      rt	        j                  |      nd	t	        j                  |      t	        j                  |      t	        j                  |      t	        j                  |      d
z  }dd|iz  }	t        t	        j                  |	            dx}x}x}x}}y)z>Env vars containing special characters survive the round-trip.SPECIAL_KEYzp@ssw0rd!#$%^&*r   Fr   r   r   r   r   r   r$   r%   Nr&   r2   s
             r   (test_bb1_env_var_with_special_charactersz@TestBB1EnvVarResolution.test_bb1_env_var_with_special_characters?   s    =*;<,e<15A"$~~AmA~m,A0AA,0AAAAA,0AAAAAAAsAAAsAAA~AAAmAAA,AAA0AAAAAAAAAr   c                   |j                  dd       |j                  dd       |j                  dd       t               }|j                         }|j                  }d} ||      }d}||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      t        j                  |      t        j                  |      d
z  }	dd|	iz  }
t        t        j                  |
            dx}x}x}x}}y)z<GenesisSecrets.get() resolves env var when Infisical is off.
DIRECT_KEYdirect_valuer   Fr   r   r   zI%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.get
}(%(py4)s)
} == %(py9)sclientr   r$   r%   Nr'   r(   r   GenesisSecretsgetr*   r+   r,   r-   r.   r/   r0   r1   )r3   r4   r   rD   r5   r6   r7   r8   r9   r:   r;   s              r   test_bb1_instance_get_reads_envz7TestBB1EnvVarResolution.test_bb1_instance_get_reads_envH   s    <8,e<15A"$##%zz9,9z,'9>9'>9999'>999999v999v999z999,999'999>99999999r   N)__name__
__module____qualname____doc__r<   r?   rH    r   r   r   r   2   s    I>B:r   r   c                      e Zd ZdZd Zd Zy)TestBB2DefaultFallbackz>When a key is absent, the caller-supplied default is returned.c                   |j                  dd       |j                  dd       |j                  dd       t               }|j                  dd      }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        j                  |            d x}}y )NMISSING_KEY_XYZFr   r   r   fallback_valuedefaultr   z%(py0)s == %(py3)sresultr   py3assert %(py5)spy5r(   r   r)   r*   r+   r,   r-   r.   r/   r0   r1   r3   r4   r   rV   @py_assert2r5   @py_format4@py_format6s           r   )test_bb2_default_returned_for_missing_keyz@TestBB2DefaultFallback.test_bb2_default_returned_for_missing_keyZ   s    ,e<,e<15A"$ 1;KL))v)))))v)))))))v)))v)))))))))))r   c                   |j                  dd       |j                  dd       |j                  dd       t               }|j                  dd      }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        j                  |            dx}}y)zCEmpty string default is a valid explicit default (not falsy guard).
ABSENT_KEYFr   r   r    rS   r   rU   rV   rW   rY   rZ   Nr[   r\   s           r    test_bb2_default_is_empty_stringz7TestBB2DefaultFallback.test_bb2_default_is_empty_stringc   s    <7,e<15A"$b9v|vvvr   N)rI   rJ   rK   rL   r`   rd   rM   r   r   rO   rO   W   s    H*r   rO   c                      e Zd ZdZd Zd Zy)TestBB3KeyErrorOnMissingzCKeyError is raised when a key is absent and no default is provided.c                   |j                  dd       |j                  dd       |j                  dd       t               }t        j                  t        d      5  |j                  d       d d d        y # 1 sw Y   y xY w)NTOTALLY_ABSENTFr   r   r   )match)r(   r   pytestraisesKeyErrorr)   )r3   r4   r   s      r   #test_bb3_keyerror_raised_no_defaultz<TestBB3KeyErrorOnMissing.test_bb3_keyerror_raised_no_defaultu   sw    +U;,e<15A"$]]8+;< 	-NN+,	- 	- 	-s   A::Bc                n   |j                  dd       |j                  dd       |j                  dd       t               }t        j                  t              5 }|j                  d       ddd       d}j                  }t        |      }||v }|s
t        j                  d|fd||f      t        j                  |      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}x}}y# 1 sw Y   =xY w)zAThe KeyError message must include the key name for debuggability.SECRET_NOT_HEREFr   r   r   N)in)zK%(py1)s in %(py8)s
{%(py8)s = %(py3)s(%(py6)s
{%(py6)s = %(py4)s.value
})
}strexc_info)py1rX   r!   r"   py8zassert %(py10)spy10)r(   r   rj   rk   rl   r)   valuerq   r*   r+   r/   r,   r-   r.   r0   r1   )
r3   r4   r   rr   @py_assert0r7   r9   r]   @py_format9@py_format11s
             r   +test_bb3_keyerror_message_contains_key_namezDTestBB3KeyErrorOnMissing.test_bb3_keyerror_message_contains_key_name~   s   ,e<,e<15A"$]]8$ 	.NN,-	. 77C$77 $77777 $7777 777777C777C777777777777777$77777777	. 	.s   F**F4N)rI   rJ   rK   rL   rm   rz   rM   r   r   rf   rf   r   s    M-	8r   rf   c                  V    e Zd ZdZ ej
                         d        Zd Zd Zd Z	d Z
y)TestBB4ParseEnvFilez7parse_env_file handles all common .env syntax patterns.c                    dfd}|S )z4Factory: write a temp .env file and return its path.c                J    dz  }|j                  | d       t        |      S )Nztest.envzutf-8)encoding)
write_textrq   )contentptmp_paths     r   _writez+TestBB4ParseEnvFile.tmp_env.<locals>._write   s&    :%ALL7L3q6Mr   )r   rq   returnrq   rM   )r3   r   r   s    ` r   tmp_envzTestBB4ParseEnvFile.tmp_env   s    	 r   c                r    |d      }t         j                  j                  dd       ddl}|j                  j                  dd      }|j                  j                  |      }|j                  j                  |       |j                  |      }ddi}||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        j"                  |
            dx}}y)z'Unquoted KEY=VALUE is parsed correctly.zMY_KEY=my_value
r   z/mnt/e/genesis-system/scriptsNmigrate_secrets0/mnt/e/genesis-system/scripts/migrate_secrets.pyMY_KEYmy_valuer   rU   rV   rW   rY   rZ   )r   pathinsertimportlib.utilutilspec_from_file_locationmodule_from_specloaderexec_moduleparse_env_filer*   r+   r,   r-   r.   r/   r0   r1   )r3   r   r   r   specr   rV   r]   r5   r^   r_   s              r   test_bb4_bare_key_valuez+TestBB4ParseEnvFile.test_bb4_bare_key_value   s    *+:;~~55>
 nn--d3$##D)"J//v/////v///////v///v///////////r   c                :    |d      }t         j                  j                  dd      }t         j                  j                  |      }|j                  j                  |       |j                  |      }ddi}||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        j                  |	            dx}}y)z*Double-quoted values have quotes stripped.zQUOTED_KEY="some value"
migrate_secrets2r   
QUOTED_KEYz
some valuer   rU   rV   rW   rY   rZ   Nr   r   r   r   r   r   r   r*   r+   r,   r-   r.   r/   r0   r1   
r3   r   r   r   r   rV   r]   r5   r^   r_   s
             r   test_bb4_double_quoted_valuez0TestBB4ParseEnvFile.test_bb4_double_quoted_value   s    23~~55>
 nn--d3$##D)&55v55555v5555555v555v55555555555r   c                0    |d      }t         j                  j                  dd      }t         j                  j                  |      }|j                  j                  |       |j                  |      }d}||v}|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }d	d
|iz  }	t        t        j                  |	            dx}}|j                  }
d} |
|      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |
      t        j                  |      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}
x}x}x}}y)z:Lines starting with # are treated as comments and skipped.z(# this is a comment
REAL_KEY=real_value
migrate_secrets3r   z# this is a comment)not in)z%(py1)s not in %(py3)srV   )rs   rX   rY   rZ   NREAL_KEY
real_valuer   rC   r   r$   r%   )r   r   r   r   r   r   r   r*   r+   r/   r,   r-   r.   r0   r1   rG   )r3   r   r   r   r   rV   rw   r]   r^   r_   r5   r6   r7   r8   r9   r:   r;   s                    r   test_bb4_comment_lines_skippedz2TestBB4ParseEnvFile.test_bb4_comment_lines_skipped   sS   CD~~55>
 nn--d3$##D)$2$F2222$F222$222222F222F2222222zz5*5z*%55%5555%555555v555v555z555*555%55555555555r   c                <    |d      }t         j                  j                  dd      }t         j                  j                  |      }|j                  j                  |       |j                  |      }dd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        j                  |	            dx}}y)z6Blank lines are ignored and do not produce empty keys.z

KEY_A=val_a

KEY_B=val_b

migrate_secrets4r   val_aval_b)KEY_AKEY_Br   rU   rV   rW   rY   rZ   Nr   r   s
             r   test_bb4_blank_lines_skippedz0TestBB4ParseEnvFile.test_bb4_blank_lines_skipped   s    ;<~~55>
 nn--d3$##D)#*W==v=====v=======v===v===========r   N)rI   rJ   rK   rL   rj   fixturer   r   r   r   r   rM   r   r   r|   r|      s4    AV^^ 0
66
>r   r|   c                      e Zd ZdZd Zd Zy)TestWB1InfisicalSkippedz<When credentials are absent, Infisical is never initialised.c                   |j                  dd       |j                  dd       t               }|j                         }|j                  }d}||u }|st	        j
                  d|fd||f      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}}y)z>_infisical_client remains None when INFISICAL_TOKEN is absent.r   Fr   r   Nisz9%(py2)s
{%(py2)s = %(py0)s._infisical_client
} is %(py5)srD   r   r    rZ   assert %(py7)spy7)r(   r   rF   _infisical_clientr*   r+   r,   r-   r.   r/   r0   r1   )	r3   r4   r   rD   r5   @py_assert4r6   r_   @py_format8s	            r   #test_wb1_no_token_no_infisical_initz;TestWB1InfisicalSkipped.test_wb1_no_token_no_infisical_init   s    ,e<15A"$##%''/4/'4////'4//////v///v///'///4///////r   c                  
 |j                  dd       |j                  dd       t               }t        t        d      rt        j                  nt        

fd}t        d|      5  |j                  dd	      }d
d
d
       j                  }d
}||u }|st        j                  d|fd||f      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}}y
# 1 sw Y   xY w)z<ImportError from infisical_sdk is caught; client stays None.r   z
fake-tokenr   zfake-project-id
__import__c                <    | dk(  rt        d       | g|i |S )Ninfisical_sdkzNo module named 'infisical_sdk')ImportError)nameargskwargsoriginal_imports      r   _mock_importz[TestWB1InfisicalSkipped.test_wb1_sdk_import_error_falls_back_silently.<locals>._mock_import   s,    &!"CDD"49$9&99r   zbuiltins.__import__)side_effect)infisical_token
project_idNr   r   rD   r   r   r   )r'   r   hasattr__builtins__r   r   rF   r   r*   r+   r,   r-   r.   r/   r0   r1   )r3   r4   r   r   rD   r5   r   r6   r_   r   r   s             @r   -test_wb1_sdk_import_error_falls_back_silentlyzETestWB1InfisicalSkipped.test_wb1_sdk_import_error_falls_back_silently   s    ,l;13DE"$ 6=\<5X,11^h	:
 (lC 	'' ,, ( F	
 ''/4/'4////'4//////v///v///'///4///////	 	s   (EEN)rI   rJ   rK   rL   r   r   rM   r   r   r   r      s    F00r   r   c                      e Zd ZdZd Zd Zy)TestWB2CacheBehaviourz:Once a secret is resolved, subsequent calls use the cache.c                   |j                  dd       |j                  dd       |j                  dd       t               }|j                         }|j	                  d      }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        j                  |            dx}}|j                  d       |j	                  d      }	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        j                  |            dx}}y)u   
        After the first get() call, removing the env var has no effect —
        the value is already in the cache.
        
CACHED_KEYoriginal_valuer   Fr   r   r   rU   firstrW   rY   rZ   NsecondrE   )
r3   r4   r   rD   r   r]   r5   r^   r_   r   s
             r   &test_wb2_env_var_read_once_then_cachedz<TestWB2CacheBehaviour.test_wb2_env_var_read_once_then_cached   s7   
 	<)9:,e<15A"$##%

<(((u(((((u(((((((u(((u((((((((((( 	<(L)))v)))))v)))))))v)))v)))))))))))r   c                0   |j                  dd       |j                  dd       |j                  dd       |j                  dd       t               }|j                         }|j	                  d       |j	                  d       |j                         }|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}}y)z:get_all() reflects every key that has been fetched so far.KEY_ONEval_oneKEY_TWOval_twor   Fr   r   r   )z%(py1)s == %(py4)s)rs   r!   zassert %(py6)sr"   N)r'   r(   r   rF   rG   get_allr*   r+   r/   r0   r1   )
r3   r4   r   rD   all_secretsrw   r6   r]   @py_format5@py_format7s
             r   'test_wb2_get_all_returns_cached_secretsz=TestWB2CacheBehaviour.test_wb2_get_all_returns_cached_secrets  s(   9i09i0,e<15A"$##%

9

9nn&9%22%2222%222%22222222229%22%2222%222%2222222222r   N)rI   rJ   rK   rL   r   r   rM   r   r   r   r      s    D*(3r   r   c                      e Zd ZdZd Zy)TestWB3ClearCachez)clear_cache() removes all cached entries.c                t   |j                  dd       |j                  dd       |j                  dd       t               }|j                         }|j                  }d} ||      }d}||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      t        j                  |      t        j                  |      d
z  }	dd|	iz  }
t        t        j                  |
            dx}x}x}x}}|j                  dd       |j                          |j                  }i }||k(  }|st        j                  d|fd||f      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}}|j                  }d} ||      }d}||k(  }|st        j                  d|fd||f      d	t        j                         v st        j                  |      rt        j                  |      nd	t        j                  |      t        j                  |      t        j                  |      t        j                  |      d
z  }	dd|	iz  }
t        t        j                  |
            dx}x}x}x}}y)z
        After clear_cache(), the next get() re-reads from the source.
        Changing the env var after clearing should return the NEW value.
        MUTABLE_KEYvalue_v1r   Fr   r   r   rC   rD   r   r$   r%   Nvalue_v2)z.%(py2)s
{%(py2)s = %(py0)s._cache
} == %(py5)sr   r   r   )r'   r(   r   rF   rG   r*   r+   r,   r-   r.   r/   r0   r1   clear_cache_cache)r3   r4   r   rD   r5   r6   r7   r8   r9   r:   r;   r   r_   r   s                 r   #test_wb3_clear_cache_forces_refetchz5TestWB3ClearCache.test_wb3_clear_cache_forces_refetch)  s   
 	=*5,e<15A"$##%zz6-6z-(6J6(J6666(J666666v666v666z666-666(666J6666666 	=*5}}""}""""}""""""v"""v"""}"""""""""" zz6-6z-(6J6(J6666(J666666v666v666z666-666(666J66666666r   N)rI   rJ   rK   rL   r   rM   r   r   r   r   &  s
    37r   r   )rL   
__future__r   builtinsr,   _pytest.assertion.rewrite	assertionrewriter*   r   r   typesunittest.mockr   r   rj   r   r   rO   rf   r|   r   r   r   rM   r   r   <module>r      sr   , #     
  * :: :J 68 88>> >>J 0  0N%3 %3X7 7r   