@@ -16,15 +16,15 @@ def test_load_config_from_yaml():
1616 - name: server1
1717 url: https://opal-demo.obiba.org
1818 user: dsuser
19- password: P@ssw0rd
19+ password: your-password-here
2020 - name: server2
2121 url: https://opal.example.org
2222 token: your-access-token-here
2323 profile: default
2424 - name: server3
2525 url: https://study.example.org/opal
2626 user: dsuser
27- password: P@ssw0rd
27+ password: your-password-here
2828 profile: custom
2929 driver: datashield_opal.OpalDriver
3030"""
@@ -46,7 +46,7 @@ def test_load_config_from_yaml():
4646 assert config .servers [0 ].name == "server1"
4747 assert config .servers [0 ].url == "https://opal-demo.obiba.org"
4848 assert config .servers [0 ].user == "dsuser"
49- assert config .servers [0 ].password == "P@ssw0rd "
49+ assert config .servers [0 ].password == "your-password-here "
5050 assert config .servers [0 ].token is None
5151 assert config .servers [0 ].profile == "default"
5252 assert config .servers [0 ].driver == "datashield_opal.OpalDriver"
@@ -64,7 +64,7 @@ def test_load_config_from_yaml():
6464 assert config .servers [2 ].name == "server3"
6565 assert config .servers [2 ].url == "https://study.example.org/opal"
6666 assert config .servers [2 ].user == "dsuser"
67- assert config .servers [2 ].password == "P@ssw0rd "
67+ assert config .servers [2 ].password == "your-password-here "
6868 assert config .servers [2 ].token is None
6969 assert config .servers [2 ].profile == "custom"
7070 assert config .servers [2 ].driver == "datashield_opal.OpalDriver"
@@ -179,6 +179,14 @@ def test_create_dslogininfo_directly():
179179 assert login .driver == "datashield_opal.OpalDriver"
180180
181181
182+ def test_create_dslogininfo_requires_user_or_token ():
183+ """Test that DSLoginInfo requires at least one credential method."""
184+ with pytest .raises (ValidationError ) as exc_info :
185+ DSLoginInfo (name = "test" , url = "https://example.org" )
186+
187+ assert "either user or token must be provided" in str (exc_info .value ).lower ()
188+
189+
182190def test_create_dsconfig_directly ():
183191 """Test creating DSConfig objects directly with Pydantic."""
184192 login1 = DSLoginInfo (name = "server1" , url = "https://example1.org" , user = "user1" , password = "pass1" )
@@ -229,3 +237,229 @@ def test_model_serialization():
229237 assert data ["password" ] == "testpass"
230238 assert data ["profile" ] == "default"
231239 assert data ["driver" ] == "datashield_opal.OpalDriver"
240+
241+
242+ def test_load_no_config_files (monkeypatch ):
243+ """Test loading configuration when no config files exist."""
244+ # Mock the CONFIG_FILES to point to non-existent paths
245+ monkeypatch .setattr (
246+ "datashield.interface.CONFIG_FILES" , ["/nonexistent/path/config.yaml" , "/another/nonexistent/path/config.yaml" ]
247+ )
248+
249+ config = DSConfig .load ()
250+
251+ assert config is not None
252+ assert len (config .servers ) == 0
253+
254+
255+ def test_load_from_first_config_file (monkeypatch ):
256+ """Test loading configuration from the first config file found."""
257+ yaml_content = """
258+ servers:
259+ - name: server1
260+ url: https://example1.org
261+ user: user1
262+ password: pass1
263+ """
264+
265+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".yaml" , delete = False ) as f :
266+ f .write (yaml_content )
267+ first_config_file = f .name
268+
269+ try :
270+ # Mock CONFIG_FILES to use our temporary file
271+ monkeypatch .setattr ("datashield.interface.CONFIG_FILES" , [first_config_file , "/nonexistent/second/config.yaml" ])
272+
273+ config = DSConfig .load ()
274+
275+ assert config is not None
276+ assert len (config .servers ) == 1
277+ assert config .servers [0 ].name == "server1"
278+ assert config .servers [0 ].url == "https://example1.org"
279+ assert config .servers [0 ].user == "user1"
280+
281+ finally :
282+ os .unlink (first_config_file )
283+
284+
285+ def test_load_from_second_config_file (monkeypatch ):
286+ """Test loading configuration from the second config file if first doesn't exist."""
287+ yaml_content = """
288+ servers:
289+ - name: server2
290+ url: https://example2.org
291+ user: user2
292+ token: token123
293+ """
294+
295+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".yaml" , delete = False ) as f :
296+ f .write (yaml_content )
297+ second_config_file = f .name
298+
299+ try :
300+ # Mock CONFIG_FILES to use our temporary file as second option
301+ monkeypatch .setattr ("datashield.interface.CONFIG_FILES" , ["/nonexistent/first/config.yaml" , second_config_file ])
302+
303+ config = DSConfig .load ()
304+
305+ assert config is not None
306+ assert len (config .servers ) == 1
307+ assert config .servers [0 ].name == "server2"
308+ assert config .servers [0 ].url == "https://example2.org"
309+ assert config .servers [0 ].token == "token123"
310+
311+ finally :
312+ os .unlink (second_config_file )
313+
314+
315+ def test_load_merge_multiple_config_files (monkeypatch ):
316+ """Test merging configurations from multiple config files."""
317+ yaml_content1 = """
318+ servers:
319+ - name: server1
320+ url: https://example1.org
321+ user: user1
322+ password: pass1
323+ - name: server2
324+ url: https://example2.org
325+ user: user2
326+ password: pass2
327+ """
328+
329+ yaml_content2 = """
330+ servers:
331+ - name: server2
332+ url: https://example2-updated.org
333+ user: user2_updated
334+ password: pass2_updated
335+ - name: server3
336+ url: https://example3.org
337+ token: token123
338+ """
339+
340+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".yaml" , delete = False ) as f1 :
341+ f1 .write (yaml_content1 )
342+ first_config_file = f1 .name
343+
344+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".yaml" , delete = False ) as f2 :
345+ f2 .write (yaml_content2 )
346+ second_config_file = f2 .name
347+
348+ try :
349+ # Mock CONFIG_FILES to use both our temporary files
350+ monkeypatch .setattr ("datashield.interface.CONFIG_FILES" , [first_config_file , second_config_file ])
351+
352+ config = DSConfig .load ()
353+
354+ assert config is not None
355+ assert len (config .servers ) == 3
356+
357+ # Check server1 (from first file, unchanged)
358+ server1 = next (s for s in config .servers if s .name == "server1" )
359+ assert server1 .url == "https://example1.org"
360+ assert server1 .user == "user1"
361+
362+ # Check server2 (from first file, but updated by second file)
363+ server2 = next (s for s in config .servers if s .name == "server2" )
364+ assert server2 .url == "https://example2-updated.org"
365+ assert server2 .user == "user2_updated"
366+ assert server2 .password == "pass2_updated"
367+
368+ # Check server3 (from second file only)
369+ server3 = next (s for s in config .servers if s .name == "server3" )
370+ assert server3 .url == "https://example3.org"
371+ assert server3 .token == "token123"
372+
373+ finally :
374+ os .unlink (first_config_file )
375+ os .unlink (second_config_file )
376+
377+
378+ def test_load_handles_invalid_yaml_silently (monkeypatch ):
379+ """Test that load() silently handles invalid YAML files."""
380+ invalid_yaml_content = """
381+ servers:
382+ - name: server1
383+ url: https://example.org
384+ invalid: yaml: content:
385+ """
386+
387+ valid_yaml_content = """
388+ servers:
389+ - name: server2
390+ url: https://example2.org
391+ user: user2
392+ password: pass2
393+ """
394+
395+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".yaml" , delete = False ) as f1 :
396+ f1 .write (invalid_yaml_content )
397+ invalid_config_file = f1 .name
398+
399+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".yaml" , delete = False ) as f2 :
400+ f2 .write (valid_yaml_content )
401+ valid_config_file = f2 .name
402+
403+ try :
404+ # Mock CONFIG_FILES with invalid file first, then valid file
405+ monkeypatch .setattr ("datashield.interface.CONFIG_FILES" , [invalid_config_file , valid_config_file ])
406+
407+ # Should not raise an exception, but load from the valid file
408+ config = DSConfig .load ()
409+
410+ assert config is not None
411+ assert len (config .servers ) == 1
412+ assert config .servers [0 ].name == "server2"
413+ assert config .servers [0 ].url == "https://example2.org"
414+
415+ finally :
416+ os .unlink (invalid_config_file )
417+ os .unlink (valid_config_file )
418+
419+
420+ def test_load_handles_permission_denied_silently (monkeypatch ):
421+ """Test that load() silently handles files without read permissions."""
422+ yaml_content1 = """
423+ servers:
424+ - name: server1
425+ url: https://example1.org
426+ user: user1
427+ password: pass1
428+ """
429+
430+ yaml_content2 = """
431+ servers:
432+ - name: server2
433+ url: https://example2.org
434+ user: user2
435+ password: pass2
436+ """
437+
438+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".yaml" , delete = False ) as f1 :
439+ f1 .write (yaml_content1 )
440+ no_read_file = f1 .name
441+
442+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".yaml" , delete = False ) as f2 :
443+ f2 .write (yaml_content2 )
444+ readable_file = f2 .name
445+
446+ try :
447+ # Remove read permissions from first file
448+ os .chmod (no_read_file , 0o000 )
449+
450+ # Mock CONFIG_FILES with unreadable file first, then readable file
451+ monkeypatch .setattr ("datashield.interface.CONFIG_FILES" , [no_read_file , readable_file ])
452+
453+ # Should not raise an exception, but load from the readable file
454+ config = DSConfig .load ()
455+
456+ assert config is not None
457+ assert len (config .servers ) == 1
458+ assert config .servers [0 ].name == "server2"
459+ assert config .servers [0 ].url == "https://example2.org"
460+
461+ finally :
462+ # Restore permissions to allow cleanup
463+ os .chmod (no_read_file , 0o644 )
464+ os .unlink (no_read_file )
465+ os .unlink (readable_file )
0 commit comments