@@ -98,20 +98,68 @@ pub fn decode_string_to_map(raw: &str) -> Result<HashMap<ModuleId, String>> {
9898
9999#[ cfg( test) ]
100100mod tests {
101+ use std:: sync:: Mutex ;
102+
101103 use super :: * ;
102104 use crate :: utils:: TestRandomSeed ;
103105
104- /// TODO: This was only used by the old JWT loader, can it be removed now?
106+ // Serializes all tests that read/write environment variables.
107+ // std::env::set_var is unsafe (Rust 1.81+) because mutating `environ`
108+ // while another thread reads it is UB at the OS level. Holding this
109+ // lock ensures our Rust threads don't race each other.
110+ static ENV_LOCK : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
111+
112+ /// Sets or removes env vars for the duration of `f`, then restores the
113+ /// original values. Pass `Some("val")` to set, `None` to ensure absent.
114+ fn with_env < R > ( vars : & [ ( & str , Option < & str > ) ] , f : impl FnOnce ( ) -> R ) -> R {
115+ let _guard = ENV_LOCK . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
116+ let saved: Vec < ( & str , Option < String > ) > =
117+ vars. iter ( ) . map ( |( k, _) | ( * k, std:: env:: var ( k) . ok ( ) ) ) . collect ( ) ;
118+ for ( k, v) in vars {
119+ match v {
120+ Some ( val) => unsafe { std:: env:: set_var ( k, val) } ,
121+ None => unsafe { std:: env:: remove_var ( k) } ,
122+ }
123+ }
124+ let result = f ( ) ;
125+ for ( k, old) in & saved {
126+ match old {
127+ Some ( v) => unsafe { std:: env:: set_var ( k, v) } ,
128+ None => unsafe { std:: env:: remove_var ( k) } ,
129+ }
130+ }
131+ result
132+ }
133+
134+ // Minimal TOML-deserializable type used by load_from_file / load_file_from_env
135+ // tests.
136+ #[ derive( serde:: Deserialize , Debug , PartialEq ) ]
137+ struct TestConfig {
138+ value : String ,
139+ }
140+
141+ // ── decode_string_to_map ─────────────────────────────────────────────────
142+
105143 #[ test]
106- fn test_decode_string_to_map ( ) {
107- let raw = " KEY=VALUE , KEY2=value2 " ;
144+ fn test_decode_string_to_map_single_pair ( ) {
145+ let map = decode_string_to_map ( "ONLY=ONE" ) . unwrap ( ) ;
146+ assert_eq ! ( map. len( ) , 1 ) ;
147+ assert_eq ! ( map. get( & ModuleId ( "ONLY" . into( ) ) ) , Some ( & "ONE" . to_string( ) ) ) ;
148+ }
108149
109- let map = decode_string_to_map ( raw) . unwrap ( ) ;
150+ #[ test]
151+ fn test_decode_string_to_map_empty_string ( ) {
152+ // An empty string yields one token with no `=`, which is invalid.
153+ assert ! ( decode_string_to_map( "" ) . is_err( ) ) ;
154+ }
110155
111- assert_eq ! ( map. get( & ModuleId ( "KEY" . into( ) ) ) , Some ( & "VALUE" . to_string( ) ) ) ;
112- assert_eq ! ( map. get( & ModuleId ( "KEY2" . into( ) ) ) , Some ( & "value2" . to_string( ) ) ) ;
156+ #[ test]
157+ fn test_decode_string_to_map_malformed_no_equals ( ) {
158+ assert ! ( decode_string_to_map( "KEYONLY" ) . is_err( ) ) ;
113159 }
114160
161+ // ── remove_duplicate_keys ────────────────────────────────────────────────
162+
115163 #[ test]
116164 fn test_remove_duplicate_keys ( ) {
117165 let key1 = BlsPublicKey :: test_random ( ) ;
@@ -123,4 +171,134 @@ mod tests {
123171 assert ! ( unique_keys. contains( & key1) ) ;
124172 assert ! ( unique_keys. contains( & key2) ) ;
125173 }
174+
175+ // ── load_env_var ─────────────────────────────────────────────────────────
176+
177+ #[ test]
178+ fn test_load_env_var_present ( ) {
179+ with_env ( & [ ( "CB_TEST_LOAD_ENV_VAR" , Some ( "hello" ) ) ] , || {
180+ assert_eq ! ( load_env_var( "CB_TEST_LOAD_ENV_VAR" ) . unwrap( ) , "hello" ) ;
181+ } ) ;
182+ }
183+
184+ #[ test]
185+ fn test_load_env_var_absent ( ) {
186+ with_env ( & [ ( "CB_TEST_LOAD_ENV_VAR_ABSENT" , None ) ] , || {
187+ let err = load_env_var ( "CB_TEST_LOAD_ENV_VAR_ABSENT" ) . unwrap_err ( ) ;
188+ assert ! ( err. to_string( ) . contains( "CB_TEST_LOAD_ENV_VAR_ABSENT" ) ) ;
189+ } ) ;
190+ }
191+
192+ // ── load_optional_env_var ────────────────────────────────────────────────
193+
194+ #[ test]
195+ fn test_load_optional_env_var_present ( ) {
196+ with_env ( & [ ( "CB_TEST_OPT_VAR" , Some ( "world" ) ) ] , || {
197+ assert_eq ! ( load_optional_env_var( "CB_TEST_OPT_VAR" ) , Some ( "world" . to_string( ) ) ) ;
198+ } ) ;
199+ }
200+
201+ #[ test]
202+ fn test_load_optional_env_var_absent ( ) {
203+ with_env ( & [ ( "CB_TEST_OPT_VAR_ABSENT" , None ) ] , || {
204+ assert_eq ! ( load_optional_env_var( "CB_TEST_OPT_VAR_ABSENT" ) , None ) ;
205+ } ) ;
206+ }
207+
208+ // ── load_from_file ───────────────────────────────────────────────────────
209+
210+ #[ test]
211+ fn test_load_from_file_valid ( ) {
212+ use std:: io:: Write as _;
213+ let mut file = tempfile:: NamedTempFile :: new ( ) . unwrap ( ) ;
214+ file. write_all ( b"value = \" hello\" " ) . unwrap ( ) ;
215+ let path = file. path ( ) . to_path_buf ( ) ;
216+
217+ let ( config, returned_path) : ( TestConfig , _ ) = load_from_file ( & path) . unwrap ( ) ;
218+ assert_eq ! ( config. value, "hello" ) ;
219+ assert_eq ! ( returned_path, path) ;
220+ }
221+
222+ #[ test]
223+ fn test_load_from_file_missing ( ) {
224+ let result: eyre:: Result < ( TestConfig , _ ) > =
225+ load_from_file ( "/nonexistent/cb_test_path/file.toml" ) ;
226+ assert ! ( result. is_err( ) ) ;
227+ }
228+
229+ #[ test]
230+ fn test_load_from_file_invalid_toml ( ) {
231+ use std:: io:: Write as _;
232+ let mut file = tempfile:: NamedTempFile :: new ( ) . unwrap ( ) ;
233+ file. write_all ( b"not valid toml !!!{{" ) . unwrap ( ) ;
234+
235+ let result: eyre:: Result < ( TestConfig , _ ) > = load_from_file ( file. path ( ) ) ;
236+ assert ! ( result. is_err( ) ) ;
237+ }
238+
239+ // ── load_file_from_env ───────────────────────────────────────────────────
240+
241+ #[ test]
242+ fn test_load_file_from_env_ok ( ) {
243+ use std:: io:: Write as _;
244+ let mut file = tempfile:: NamedTempFile :: new ( ) . unwrap ( ) ;
245+ file. write_all ( b"value = \" from_env\" " ) . unwrap ( ) ;
246+ let path = file. path ( ) . to_str ( ) . unwrap ( ) . to_owned ( ) ;
247+
248+ with_env ( & [ ( "CB_TEST_FILE_ENV" , Some ( & path) ) ] , || {
249+ let ( config, _) : ( TestConfig , _ ) = load_file_from_env ( "CB_TEST_FILE_ENV" ) . unwrap ( ) ;
250+ assert_eq ! ( config. value, "from_env" ) ;
251+ } ) ;
252+ }
253+
254+ #[ test]
255+ fn test_load_file_from_env_var_not_set ( ) {
256+ with_env ( & [ ( "CB_TEST_FILE_ENV_ABSENT" , None ) ] , || {
257+ let result: eyre:: Result < ( TestConfig , _ ) > =
258+ load_file_from_env ( "CB_TEST_FILE_ENV_ABSENT" ) ;
259+ assert ! ( result. is_err( ) ) ;
260+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "CB_TEST_FILE_ENV_ABSENT" ) ) ;
261+ } ) ;
262+ }
263+
264+ // ── load_jwt_secrets ─────────────────────────────────────────────────────
265+
266+ #[ test]
267+ fn test_load_jwt_secrets_ok ( ) {
268+ with_env (
269+ & [
270+ ( ADMIN_JWT_ENV , Some ( "admin_secret" ) ) ,
271+ ( JWTS_ENV , Some ( "MODULE1=secret1,MODULE2=secret2" ) ) ,
272+ ] ,
273+ || {
274+ let ( admin_jwt, secrets) = load_jwt_secrets ( ) . unwrap ( ) ;
275+ assert_eq ! ( admin_jwt, "admin_secret" ) ;
276+ assert_eq ! ( secrets. get( & ModuleId ( "MODULE1" . into( ) ) ) , Some ( & "secret1" . to_string( ) ) ) ;
277+ assert_eq ! ( secrets. get( & ModuleId ( "MODULE2" . into( ) ) ) , Some ( & "secret2" . to_string( ) ) ) ;
278+ } ,
279+ ) ;
280+ }
281+
282+ #[ test]
283+ fn test_load_jwt_secrets_missing_admin_jwt ( ) {
284+ with_env ( & [ ( ADMIN_JWT_ENV , None ) , ( JWTS_ENV , Some ( "MODULE1=secret1" ) ) ] , || {
285+ let err = load_jwt_secrets ( ) . unwrap_err ( ) ;
286+ assert ! ( err. to_string( ) . contains( ADMIN_JWT_ENV ) ) ;
287+ } ) ;
288+ }
289+
290+ #[ test]
291+ fn test_load_jwt_secrets_missing_jwts ( ) {
292+ with_env ( & [ ( ADMIN_JWT_ENV , Some ( "admin_secret" ) ) , ( JWTS_ENV , None ) ] , || {
293+ let err = load_jwt_secrets ( ) . unwrap_err ( ) ;
294+ assert ! ( err. to_string( ) . contains( JWTS_ENV ) ) ;
295+ } ) ;
296+ }
297+
298+ #[ test]
299+ fn test_load_jwt_secrets_malformed_jwts ( ) {
300+ with_env ( & [ ( ADMIN_JWT_ENV , Some ( "admin_secret" ) ) , ( JWTS_ENV , Some ( "MALFORMED" ) ) ] , || {
301+ assert ! ( load_jwt_secrets( ) . is_err( ) ) ;
302+ } ) ;
303+ }
126304}
0 commit comments