diff --git a/pyproject.toml b/pyproject.toml index babd002..deb1988 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "socketdev" -version = "3.0.16" +version = "3.0.17" requires-python = ">= 3.9" dependencies = [ 'requests', diff --git a/socketdev/uploadmanifests/__init__.py b/socketdev/uploadmanifests/__init__.py index 38c5a2c..dc6b634 100644 --- a/socketdev/uploadmanifests/__init__.py +++ b/socketdev/uploadmanifests/__init__.py @@ -10,6 +10,79 @@ class UploadManifests: def __init__(self, api): self.api = api + def _calculate_key_name(self, file_path: str, workspace: Optional[str] = None, base_path: Optional[str] = None, base_paths: Optional[List[str]] = None) -> str: + """ + Calculate the key name for a file using the same logic as load_files_for_sending_lazy. + This ensures consistency between lazy and non-lazy loading modes. + """ + # Normalize file path + if "\\" in file_path: + file_path = file_path.replace("\\", "/") + + # Normalize paths + if workspace and "\\" in workspace: + workspace = workspace.replace("\\", "/") + if base_path and "\\" in base_path: + base_path = base_path.replace("\\", "/") + if base_paths: + base_paths = [bp.replace("\\", "/") if "\\" in bp else bp for bp in base_paths] + + # Calculate the key name for the form data + key = file_path + path_stripped = False + + # If base_paths is provided, try to strip one of the paths from the file path + if base_paths: + for bp in base_paths: + normalized_base_path = bp.rstrip("/") + "/" if not bp.endswith("/") else bp + if key.startswith(normalized_base_path): + key = key[len(normalized_base_path):] + path_stripped = True + break + elif key.startswith(bp.rstrip("/")): + stripped_base = bp.rstrip("/") + if key.startswith(stripped_base + "/") or key == stripped_base: + key = key[len(stripped_base):] + key = key.lstrip("/") + path_stripped = True + break + elif base_path: + normalized_base_path = base_path.rstrip("/") + "/" if not base_path.endswith("/") else base_path + if key.startswith(normalized_base_path): + key = key[len(normalized_base_path):] + path_stripped = True + elif key.startswith(base_path.rstrip("/")): + stripped_base = base_path.rstrip("/") + if key.startswith(stripped_base + "/") or key == stripped_base: + key = key[len(stripped_base):] + key = key.lstrip("/") + path_stripped = True + + # If workspace is provided and no base paths matched, fall back to workspace logic + if not path_stripped and workspace and file_path.startswith(workspace): + key = file_path[len(workspace):] + # Remove all leading slashes (for absolute paths) + while key.startswith("/"): + key = key[1:] + path_stripped = True + + # Clean up relative path prefixes, but preserve filename dots + while key.startswith("./"): + key = key[2:] + while key.startswith("../"): + key = key[3:] + # Remove any remaining leading slashes (for absolute paths) + while key.startswith("/"): + key = key[1:] + + # Remove Windows drive letter if present (C:/...) + if len(key) > 2 and key[1] == ':' and (key[2] == '/' or key[2] == '\\'): + key = key[2:] + while key.startswith("/"): + key = key[1:] + + return key + def upload_manifest_files(self, org_slug: str, file_paths: List[str], workspace: Optional[str] = None, base_path: Optional[str] = None, base_paths: Optional[List[str]] = None, use_lazy_loading: bool = True) -> str: """ Upload manifest files to Socket API and return tarHash. @@ -46,7 +119,8 @@ def upload_manifest_files(self, org_slug: str, file_paths: List[str], workspace: # Fallback to basic file loading if needed loaded_files = [] for file_path in valid_files: - key = os.path.basename(file_path) + # Use the same key generation logic as lazy loading for consistency + key = self._calculate_key_name(file_path, workspace, base_path, base_paths) with open(file_path, 'rb') as f: loaded_files.append((key, (key, f.read()))) diff --git a/socketdev/version.py b/socketdev/version.py index cf5900b..6e22e02 100644 --- a/socketdev/version.py +++ b/socketdev/version.py @@ -1 +1 @@ -__version__ = "3.0.16" +__version__ = "3.0.17" diff --git a/tests/unit/test_upload_manifests.py b/tests/unit/test_upload_manifests.py new file mode 100644 index 0000000..e26aa62 --- /dev/null +++ b/tests/unit/test_upload_manifests.py @@ -0,0 +1,179 @@ +import os +import tempfile +import unittest +from unittest.mock import Mock, patch, mock_open +from socketdev.uploadmanifests import UploadManifests + + +class TestUploadManifests(unittest.TestCase): + def setUp(self): + self.mock_api = Mock() + self.upload_manifests = UploadManifests(self.mock_api) + + def test_calculate_key_name_with_base_path(self): + """Test that key names are calculated correctly with base_path.""" + # Test with base_path + key = self.upload_manifests._calculate_key_name( + "/project/frontend/package.json", + base_path="/project" + ) + self.assertEqual(key, "frontend/package.json") + + def test_calculate_key_name_with_workspace(self): + """Test that key names are calculated correctly with workspace.""" + # Test with workspace + key = self.upload_manifests._calculate_key_name( + "/project/frontend/package.json", + workspace="/project/" + ) + self.assertEqual(key, "frontend/package.json") + + def test_calculate_key_name_with_base_paths(self): + """Test that key names are calculated correctly with base_paths.""" + # Test with base_paths (takes precedence over base_path) + key = self.upload_manifests._calculate_key_name( + "/project/frontend/package.json", + base_path="/project", + base_paths=["/different", "/project"] + ) + self.assertEqual(key, "frontend/package.json") + + def test_calculate_key_name_no_stripping(self): + """Test that key names default to basename when no stripping options provided.""" + # Test without any path stripping - should preserve relative path structure + key = self.upload_manifests._calculate_key_name( + "frontend/package.json" + ) + self.assertEqual(key, "frontend/package.json") + + def test_calculate_key_name_absolute_path_no_stripping(self): + """Test that absolute paths get cleaned up when no stripping options provided.""" + key = self.upload_manifests._calculate_key_name( + "/absolute/path/frontend/package.json" + ) + self.assertEqual(key, "absolute/path/frontend/package.json") + + def test_calculate_key_name_windows_paths(self): + """Test that Windows paths are handled correctly.""" + key = self.upload_manifests._calculate_key_name( + "C:\\project\\frontend\\package.json", + base_path="C:\\project" + ) + self.assertEqual(key, "frontend/package.json") + + @patch('socketdev.uploadmanifests.Utils.load_files_for_sending_lazy') + @patch('os.path.exists') + @patch('os.path.isfile') + def test_upload_manifest_files_lazy_loading(self, mock_isfile, mock_exists, mock_lazy_load): + """Test that lazy loading preserves key names correctly.""" + # Setup mocks + mock_exists.return_value = True + mock_isfile.return_value = True + mock_lazy_load.return_value = [ + ('frontend/package.json', ('frontend/package.json', Mock())) + ] + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {'tarHash': 'test_hash'} + self.mock_api.do_request.return_value = mock_response + + # Test lazy loading + result = self.upload_manifests.upload_manifest_files( + "test_org", + ["/project/frontend/package.json"], + workspace="/project", + use_lazy_loading=True + ) + + self.assertEqual(result, 'test_hash') + mock_lazy_load.assert_called_once_with( + ["/project/frontend/package.json"], + workspace="/project", + base_path=None, + base_paths=None + ) + + @patch('builtins.open', new_callable=mock_open, read_data=b'{"name": "test"}') + @patch('os.path.exists') + @patch('os.path.isfile') + def test_upload_manifest_files_non_lazy_loading(self, mock_isfile, mock_exists, mock_file): + """Test that non-lazy loading produces same key names as lazy loading.""" + # Setup mocks + mock_exists.return_value = True + mock_isfile.return_value = True + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {'tarHash': 'test_hash'} + self.mock_api.do_request.return_value = mock_response + + # Test non-lazy loading with workspace + result = self.upload_manifests.upload_manifest_files( + "test_org", + ["frontend/package.json"], + workspace="/project", + use_lazy_loading=False + ) + + self.assertEqual(result, 'test_hash') + + # Verify the API was called with the correct file structure + call_args = self.mock_api.do_request.call_args + files_arg = call_args[1]['files'] + self.assertEqual(len(files_arg), 1) + + # The key should be 'frontend/package.json' not just 'package.json' + key, (filename, content) = files_arg[0] + self.assertEqual(key, "frontend/package.json") + self.assertEqual(filename, "frontend/package.json") + + @patch('builtins.open', new_callable=mock_open, read_data=b'{"name": "test"}') + @patch('os.path.exists') + @patch('os.path.isfile') + def test_upload_manifest_files_consistency_between_modes(self, mock_isfile, mock_exists, mock_file): + """Test that lazy and non-lazy loading produce identical key names.""" + # Setup mocks + mock_exists.return_value = True + mock_isfile.return_value = True + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {'tarHash': 'test_hash'} + self.mock_api.do_request.return_value = mock_response + + test_files = ["frontend/package.json", "backend/package.json"] + + # Test non-lazy loading + with patch('socketdev.uploadmanifests.Utils.load_files_for_sending_lazy') as mock_lazy_load: + mock_lazy_load.return_value = [ + ('frontend/package.json', ('frontend/package.json', Mock())), + ('backend/package.json', ('backend/package.json', Mock())) + ] + + # Get lazy loading result + self.upload_manifests.upload_manifest_files( + "test_org", + test_files, + use_lazy_loading=True + ) + lazy_call_args = self.mock_api.do_request.call_args[1]['files'] + + # Reset mock + self.mock_api.reset_mock() + + # Get non-lazy loading result + self.upload_manifests.upload_manifest_files( + "test_org", + test_files, + use_lazy_loading=False + ) + non_lazy_call_args = self.mock_api.do_request.call_args[1]['files'] + + # Compare key names - they should be identical + lazy_keys = [item[0] for item in lazy_call_args] + non_lazy_keys = [item[0] for item in non_lazy_call_args] + + self.assertEqual(lazy_keys, non_lazy_keys) + self.assertEqual(non_lazy_keys, ['frontend/package.json', 'backend/package.json']) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file