11import unittest
22
33import mock
4+ from hypothesis import given
5+ from hypothesis import strategies as st
46
57from pyntc .devices .base_device import RollbackError
68from pyntc .devices .nxos_device import NXOSDevice
@@ -286,22 +288,21 @@ def test_refresh(self):
286288 self .assertIsNone (self .device ._uptime )
287289 self .assertFalse (hasattr (self .device .native , "_facts" ))
288290
289- @ mock . patch . object ( NXOSDevice , "show" , return_value = "bootflash:" )
290- def test_get_file_system ( self , mock_show ):
291+ def test_get_file_system ( self ):
292+ self . device . native_ssh . send_command . return_value = "bootflash:"
291293 self .assertEqual (self .device ._get_file_system (), "bootflash:" )
292- mock_show . assert_called_with ("dir" , raw_text = True )
294+ self . device . native_ssh . send_command . assert_called_with ("dir" , read_timeout = 30 )
293295
294- @ mock . patch . object ( NXOSDevice , "show" , return_value = "no filesystems here" )
295- def test_get_file_system_not_found ( self , mock_show ):
296+ def test_get_file_system_not_found ( self ):
297+ self . device . native_ssh . send_command . return_value = "no filesystems here"
296298 with self .assertRaises (FileSystemNotFoundError ):
297299 self .device ._get_file_system ()
298- mock_show . assert_called_with ("dir" , raw_text = True )
300+ self . device . native_ssh . send_command . assert_called_with ("dir" , read_timeout = 30 )
299301
300- @mock .patch .object (NXOSDevice , "show" )
301- def test_get_free_space (self , mock_show ):
302+ def test_get_free_space (self ):
302303 """Test _get_free_space parses NXOS dir output correctly."""
303304 # NXOS dir output format with free space at the end
304- mock_show .return_value = """Directory of bootflash:/
305+ self . device . native_ssh . send_command .return_value = """Directory of bootflash:/
3053064096 Mar 03 22:47:15 2026 .rpmstore/
3063074733329408 bytes used
30730847171194880 bytes free
@@ -310,25 +311,25 @@ def test_get_free_space(self, mock_show):
310311"""
311312 result = self .device ._get_free_space ()
312313 self .assertEqual (result , 47171194880 )
313- mock_show .assert_called_with ("dir bootflash:" , raw_text = True )
314+ # Should call _get_file_system (which uses SSH) and then dir command via SSH
315+ ssh_calls = self .device .native_ssh .send_command .call_args_list
316+ self .assertTrue (any ("dir" in str (call ) for call in ssh_calls ))
314317
315- @mock .patch .object (NXOSDevice , "show" )
316- def test_get_free_space_with_custom_filesystem (self , mock_show ):
318+ def test_get_free_space_with_custom_filesystem (self ):
317319 """Test _get_free_space uses custom file system when provided."""
318- mock_show .return_value = """Directory of disk0:/
320+ self . device . native_ssh . send_command .return_value = """Directory of disk0:/
3193211000000 bytes used
3203222000000 bytes free
3213233000000 bytes total
322324
323325"""
324326 result = self .device ._get_free_space ("disk0:" )
325327 self .assertEqual (result , 2000000 )
326- mock_show . assert_called_with ("dir disk0:" , raw_text = True )
328+ self . device . native_ssh . send_command . assert_called_with ("dir disk0:" , read_timeout = 30 )
327329
328- @mock .patch .object (NXOSDevice , "show" )
329- def test_get_free_space_raises_on_parse_error (self , mock_show ):
330+ def test_get_free_space_raises_on_parse_error (self ):
330331 """Test _get_free_space raises CommandError when output can't be parsed."""
331- mock_show .return_value = "Directory of bootflash:/\n No free space info here\n "
332+ self . device . native_ssh . send_command .return_value = "Directory of bootflash:/\n No free space info here\n "
332333 with self .assertRaises (CommandError ):
333334 self .device ._get_free_space ()
334335
@@ -455,6 +456,43 @@ def test_remote_file_copy_query_string_not_supported(self):
455456 with self .assertRaises (ValueError ):
456457 self .device .remote_file_copy (src , file_system = "bootflash:" )
457458
459+ @given (
460+ scheme = st .sampled_from (["http" , "https" , "scp" , "sftp" , "ftp" , "tftp" ]),
461+ hostname = st .text (min_size = 1 , max_size = 20 , alphabet = st .characters (whitelist_categories = ("Ll" , "Lu" , "Nd" ))),
462+ filename = st .text (
463+ min_size = 1 , max_size = 20 , alphabet = st .characters (whitelist_categories = ("Ll" , "Lu" , "Nd" , "Pd" ))
464+ ),
465+ checksum = st .text (min_size = 32 , max_size = 32 , alphabet = st .characters (whitelist_categories = ("Ll" , "Nd" ))),
466+ )
467+ def test_remote_file_copy_uses_ssh_for_filesystem_detection (self , scheme , hostname , filename , checksum ):
468+ """Property-based test: remote_file_copy should use SSH for _get_file_system calls.
469+
470+ This test verifies that the SSH/HTTP protocol mismatch bug is fixed by ensuring
471+ that _get_file_system always uses SSH for file system operations.
472+ """
473+ src = FileCopyModel (
474+ download_url = f"{ scheme } ://{ hostname } /{ filename } " ,
475+ checksum = checksum ,
476+ file_name = filename ,
477+ hashing_algorithm = "md5" ,
478+ timeout = 30 ,
479+ )
480+
481+ # Mock SSH operations to simulate successful file system detection
482+ self .device .native_ssh .send_command .return_value = "Directory of bootflash:/\n 47171194880 bytes free"
483+ self .device .native_ssh .find_prompt .return_value = "host#"
484+
485+ # Mock verify_file to return True (file already exists and verified)
486+ with mock .patch .object (NXOSDevice , "verify_file" , return_value = True ):
487+ # This should complete without attempting HTTP connections
488+ self .device .remote_file_copy (src )
489+
490+ # Verify that SSH was used for directory command (filesystem detection)
491+ ssh_calls = self .device .native_ssh .send_command .call_args_list
492+ self .assertTrue (
493+ any ("dir" in str (call ) for call in ssh_calls ), "Expected SSH 'dir' command for filesystem detection"
494+ )
495+
458496
459497if __name__ == "__main__" :
460498 unittest .main ()
0 commit comments