11import unittest
22
33import mock
4+ from hypothesis import given , strategies as st
45
56from pyntc .devices .base_device import RollbackError
67from pyntc .devices .nxos_device import NXOSDevice
@@ -286,22 +287,21 @@ def test_refresh(self):
286287 self .assertIsNone (self .device ._uptime )
287288 self .assertFalse (hasattr (self .device .native , "_facts" ))
288289
289- @ mock . patch . object ( NXOSDevice , "show" , return_value = "bootflash:" )
290- def test_get_file_system ( self , mock_show ):
290+ def test_get_file_system ( self ):
291+ self . device . native_ssh . send_command . return_value = "bootflash:"
291292 self .assertEqual (self .device ._get_file_system (), "bootflash:" )
292- mock_show . assert_called_with ("dir" , raw_text = True )
293+ self . device . native_ssh . send_command . assert_called_with ("dir" , read_timeout = 30 )
293294
294- @ mock . patch . object ( NXOSDevice , "show" , return_value = "no filesystems here" )
295- def test_get_file_system_not_found ( self , mock_show ):
295+ def test_get_file_system_not_found ( self ):
296+ self . device . native_ssh . send_command . return_value = "no filesystems here"
296297 with self .assertRaises (FileSystemNotFoundError ):
297298 self .device ._get_file_system ()
298- mock_show . assert_called_with ("dir" , raw_text = True )
299+ self . device . native_ssh . send_command . assert_called_with ("dir" , read_timeout = 30 )
299300
300- @mock .patch .object (NXOSDevice , "show" )
301- def test_get_free_space (self , mock_show ):
301+ def test_get_free_space (self ):
302302 """Test _get_free_space parses NXOS dir output correctly."""
303303 # NXOS dir output format with free space at the end
304- mock_show .return_value = """Directory of bootflash:/
304+ self . device . native_ssh . send_command .return_value = """Directory of bootflash:/
3053054096 Mar 03 22:47:15 2026 .rpmstore/
3063064733329408 bytes used
30730747171194880 bytes free
@@ -310,25 +310,25 @@ def test_get_free_space(self, mock_show):
310310"""
311311 result = self .device ._get_free_space ()
312312 self .assertEqual (result , 47171194880 )
313- mock_show .assert_called_with ("dir bootflash:" , raw_text = True )
313+ # Should call _get_file_system (which uses SSH) and then dir command via SSH
314+ ssh_calls = self .device .native_ssh .send_command .call_args_list
315+ self .assertTrue (any ("dir" in str (call ) for call in ssh_calls ))
314316
315- @mock .patch .object (NXOSDevice , "show" )
316- def test_get_free_space_with_custom_filesystem (self , mock_show ):
317+ def test_get_free_space_with_custom_filesystem (self ):
317318 """Test _get_free_space uses custom file system when provided."""
318- mock_show .return_value = """Directory of disk0:/
319+ self . device . native_ssh . send_command .return_value = """Directory of disk0:/
3193201000000 bytes used
3203212000000 bytes free
3213223000000 bytes total
322323
323324"""
324325 result = self .device ._get_free_space ("disk0:" )
325326 self .assertEqual (result , 2000000 )
326- mock_show . assert_called_with ("dir disk0:" , raw_text = True )
327+ self . device . native_ssh . send_command . assert_called_with ("dir disk0:" , read_timeout = 30 )
327328
328- @mock .patch .object (NXOSDevice , "show" )
329- def test_get_free_space_raises_on_parse_error (self , mock_show ):
329+ def test_get_free_space_raises_on_parse_error (self ):
330330 """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 "
331+ self . device . native_ssh . send_command .return_value = "Directory of bootflash:/\n No free space info here\n "
332332 with self .assertRaises (CommandError ):
333333 self .device ._get_free_space ()
334334
@@ -455,6 +455,40 @@ def test_remote_file_copy_query_string_not_supported(self):
455455 with self .assertRaises (ValueError ):
456456 self .device .remote_file_copy (src , file_system = "bootflash:" )
457457
458+ @given (
459+ scheme = st .sampled_from (["http" , "https" , "scp" , "sftp" , "ftp" , "tftp" ]),
460+ hostname = st .text (min_size = 1 , max_size = 20 , alphabet = st .characters (whitelist_categories = ("Ll" , "Lu" , "Nd" ))),
461+ filename = st .text (min_size = 1 , max_size = 20 , alphabet = st .characters (whitelist_categories = ("Ll" , "Lu" , "Nd" , "Pd" ))),
462+ checksum = st .text (min_size = 32 , max_size = 32 , alphabet = st .characters (whitelist_categories = ("Ll" , "Nd" ))),
463+ )
464+ def test_remote_file_copy_uses_ssh_for_filesystem_detection (self , scheme , hostname , filename , checksum ):
465+ """Property-based test: remote_file_copy should use SSH for _get_file_system calls.
466+
467+ This test verifies that the SSH/HTTP protocol mismatch bug is fixed by ensuring
468+ that _get_file_system always uses SSH for file system operations.
469+ """
470+ src = FileCopyModel (
471+ download_url = f"{ scheme } ://{ hostname } /{ filename } " ,
472+ checksum = checksum ,
473+ file_name = filename ,
474+ hashing_algorithm = "md5" ,
475+ timeout = 30 ,
476+ )
477+
478+ # Mock SSH operations to simulate successful file system detection
479+ self .device .native_ssh .send_command .return_value = "Directory of bootflash:/\n 47171194880 bytes free"
480+ self .device .native_ssh .find_prompt .return_value = "host#"
481+
482+ # Mock verify_file to return True (file already exists and verified)
483+ with mock .patch .object (NXOSDevice , "verify_file" , return_value = True ):
484+ # This should complete without attempting HTTP connections
485+ self .device .remote_file_copy (src )
486+
487+ # Verify that SSH was used for directory command (filesystem detection)
488+ ssh_calls = self .device .native_ssh .send_command .call_args_list
489+ self .assertTrue (any ("dir" in str (call ) for call in ssh_calls ),
490+ "Expected SSH 'dir' command for filesystem detection" )
491+
458492
459493if __name__ == "__main__" :
460494 unittest .main ()
0 commit comments