#!/usr/bin/env python3 """ Unit tests for transfer_ownership functionality """ import unittest from unittest.mock import Mock, patch import sys import os import json import tempfile from requests.exceptions import HTTPError # Add parent directory to path to import modules sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from transfer_ownership import ( OwnershipManager, TransferResult, format_file_table ) from sigma_api_client import SigmaAPIClient class TestTransferResult(unittest.TestCase): """Test cases for TransferResult dataclass""" def test_transfer_result_success(self): """Test successful transfer result""" result = TransferResult( inode_id='test_id', name='Test Workbook', success=True, file_type='workbook' ) self.assertEqual(result.inode_id, 'test_id') self.assertEqual(result.name, 'Test Workbook') self.assertTrue(result.success) self.assertEqual(result.file_type, 'workbook') self.assertIsNone(result.error) def test_transfer_result_failure(self): """Test failed transfer result""" result = TransferResult( inode_id='test_id', name='Test Workbook', success=False, file_type='workbook', error='Permission denied' ) self.assertFalse(result.success) self.assertEqual(result.error, 'Permission denied') class TestOwnershipManager(unittest.TestCase): """Test cases for OwnershipManager""" def setUp(self): """Set up test fixtures""" self.mock_client = Mock(spec=SigmaAPIClient) self.ownership_manager = OwnershipManager(self.mock_client) def test_init(self): """Test OwnershipManager initialization""" self.assertEqual(self.ownership_manager.api, self.mock_client) def test_list_all_members_single_page(self): """Test listing members with single page""" mock_response = { 'entries': [ {'memberId': '1', 'email': 'user1@test.com'}, {'memberId': '2', 'email': 'user2@test.com'} ] } self.mock_client.get.return_value = mock_response result = self.ownership_manager.list_all_members() self.assertEqual(len(result), 2) self.mock_client.get.assert_called_once() def test_list_all_members_pagination(self): """Test listing members with pagination""" # First page first_response = { 'entries': [{'memberId': '1', 'email': 'user1@test.com'}], 'nextPage': 'page2' } # Second page second_response = { 'entries': [{'memberId': '2', 'email': 'user2@test.com'}] } self.mock_client.get.side_effect = [first_response, second_response] result = self.ownership_manager.list_all_members() self.assertEqual(len(result), 2) self.assertEqual(self.mock_client.get.call_count, 2) def test_get_member_by_email_found(self): """Test finding member by email""" mock_members = [ {'memberId': '1', 'email': 'user1@test.com'}, {'memberId': '2', 'email': 'user2@test.com'} ] with patch.object(self.ownership_manager, 'list_all_members', return_value=mock_members): result = self.ownership_manager.get_member_by_email('user2@test.com') self.assertIsNotNone(result) self.assertEqual(result['memberId'], '2') def test_get_member_by_email_not_found(self): """Test member not found by email""" mock_members = [{'memberId': '1', 'email': 'user1@test.com'}] with patch.object(self.ownership_manager, 'list_all_members', return_value=mock_members): result = self.ownership_manager.get_member_by_email('nonexistent@test.com') self.assertIsNone(result) def test_get_member_by_email_case_insensitive(self): """Test case insensitive email search""" mock_members = [{'memberId': '1', 'email': 'User1@Test.Com'}] with patch.object(self.ownership_manager, 'list_all_members', return_value=mock_members): result = self.ownership_manager.get_member_by_email('user1@test.com') self.assertIsNotNone(result) self.assertEqual(result['memberId'], '1') def test_search_members_by_email(self): """Test partial email search""" mock_members = [ {'memberId': '1', 'email': 'john.doe@test.com'}, {'memberId': '2', 'email': 'jane.doe@test.com'}, {'memberId': '3', 'email': 'bob.smith@test.com'} ] with patch.object(self.ownership_manager, 'list_all_members', return_value=mock_members): result = self.ownership_manager.search_members_by_email('doe') self.assertEqual(len(result), 2) def test_list_files_single_page(self): """Test listing files with single page""" mock_response = { 'entries': [ {'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}, {'id': '2', 'name': 'Workbook 2', 'type': 'workbook'} ] } self.mock_client.get.return_value = mock_response result = self.ownership_manager.list_files() self.assertEqual(len(result), 2) def test_list_files_with_type_filter(self): """Test listing files with type filter""" mock_response = { 'entries': [ {'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}, {'id': '2', 'name': 'Folder 1', 'type': 'folder'} ] } self.mock_client.get.return_value = mock_response result = self.ownership_manager.list_files(type_filters=['workbook']) self.assertEqual(len(result), 1) self.assertEqual(result[0]['type'], 'workbook') def test_list_files_by_owner(self): """Test listing files by owner""" mock_member = {'memberId': 'owner123', 'email': 'owner@test.com'} mock_files = [ {'id': '1', 'name': 'Workbook 1', 'ownerId': 'owner123'}, {'id': '2', 'name': 'Workbook 2', 'ownerId': 'other456'}, {'id': '3', 'name': 'Workbook 3', 'ownerId': 'owner123'} ] with patch.object(self.ownership_manager, 'get_member_by_email', return_value=mock_member): with patch.object(self.ownership_manager, 'list_files', return_value=mock_files): result = self.ownership_manager.list_files_by_owner('owner@test.com') self.assertEqual(len(result), 2) self.assertEqual(result[0]['id'], '1') self.assertEqual(result[1]['id'], '3') def test_list_files_by_owner_user_not_found(self): """Test listing files by owner when user not found""" with patch.object(self.ownership_manager, 'get_member_by_email', return_value=None): with self.assertRaises(ValueError) as context: self.ownership_manager.list_files_by_owner('nonexistent@test.com') self.assertIn('User not found', str(context.exception)) def test_transfer_ownership(self): """Test single file ownership transfer""" mock_response = {'id': 'file123', 'ownerId': 'new_owner'} self.mock_client.patch.return_value = mock_response result = self.ownership_manager.transfer_ownership('file123', 'new_owner') self.assertEqual(result['ownerId'], 'new_owner') self.mock_client.patch.assert_called_once_with( '/v2/files/file123', {'ownerId': 'new_owner'} ) def test_bulk_transfer_ownership_dry_run(self): """Test bulk transfer in dry run mode""" files = [ {'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}, {'id': '2', 'name': 'Workbook 2', 'type': 'workbook'} ] results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=True ) self.assertEqual(len(results), 2) self.assertTrue(all(r.success for r in results)) # No actual API calls should be made self.mock_client.patch.assert_not_called() def test_bulk_transfer_ownership_success(self): """Test successful bulk transfer""" files = [ {'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}, {'id': '2', 'name': 'Workbook 2', 'type': 'workbook'} ] self.mock_client.patch.return_value = {'id': 'test', 'ownerId': 'new_owner'} results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0 # No delay for tests ) self.assertEqual(len(results), 2) self.assertTrue(all(r.success for r in results)) self.assertEqual(self.mock_client.patch.call_count, 2) def test_bulk_transfer_ownership_missing_inode_id(self): """Test bulk transfer with missing inode_id""" files = [ {'name': 'Workbook without ID', 'type': 'workbook'} ] results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False ) self.assertEqual(len(results), 1) self.assertFalse(results[0].success) self.assertIn('Missing file ID', results[0].error) def test_bulk_transfer_ownership_with_checkpoint(self): """Test bulk transfer with checkpoint file""" files = [ {'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}, {'id': '2', 'name': 'Workbook 2', 'type': 'workbook'} ] self.mock_client.patch.return_value = {'id': 'test', 'ownerId': 'new_owner'} with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: checkpoint_file = f.name try: results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, checkpoint_file=checkpoint_file ) # Verify results self.assertEqual(len(results), 2) self.assertTrue(all(r.success for r in results)) # Check checkpoint file was created with open(checkpoint_file, 'r') as f: checkpoint_data = json.load(f) self.assertIn('completed_ids', checkpoint_data) self.assertEqual(len(checkpoint_data['completed_ids']), 2) finally: os.unlink(checkpoint_file) def test_bulk_transfer_ownership_resume_from_checkpoint(self): """Test resuming bulk transfer from checkpoint""" files = [ {'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}, {'id': '2', 'name': 'Workbook 2', 'type': 'workbook'} ] self.mock_client.patch.return_value = {'id': 'test', 'ownerId': 'new_owner'} # Create checkpoint with first file already completed with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: json.dump({'completed_ids': ['1']}, f) checkpoint_file = f.name try: results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, checkpoint_file=checkpoint_file ) # Only second file should be transferred self.assertEqual(self.mock_client.patch.call_count, 1) # First result should show as success (skipped from checkpoint) self.assertTrue(results[0].success) self.assertIsNone(results[0].error) finally: os.unlink(checkpoint_file) def test_bulk_transfer_ownership_with_progress_callback(self): """Test bulk transfer with progress callback""" files = [ {'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}, {'id': '2', 'name': 'Workbook 2', 'type': 'workbook'} ] self.mock_client.patch.return_value = {'id': 'test', 'ownerId': 'new_owner'} progress_calls = [] def track_progress(current, total, name): progress_calls.append((current, total, name)) results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, progress_callback=track_progress ) # Verify results self.assertEqual(len(results), 2) # Verify progress callback was called correctly self.assertEqual(len(progress_calls), 2) self.assertEqual(progress_calls[0], (1, 2, 'Workbook 1')) self.assertEqual(progress_calls[1], (2, 2, 'Workbook 2')) class TestFormatFileTable(unittest.TestCase): """Test cases for format_file_table function""" def test_empty_files(self): """Test formatting empty file list""" result = format_file_table([]) self.assertEqual(result, " (no files found)") def test_single_file(self): """Test formatting single file""" files = [ {'name': 'Test Workbook', 'type': 'workbook', 'path': '/My Documents'} ] result = format_file_table(files) self.assertIn('Test Workbook', result) self.assertIn('workbook', result) self.assertIn('/My Documents', result) def test_multiple_files(self): """Test formatting multiple files""" files = [ {'name': 'Workbook 1', 'type': 'workbook', 'path': '/Folder1'}, {'name': 'Workbook 2', 'type': 'workbook', 'path': '/Folder2'} ] result = format_file_table(files) self.assertIn('Workbook 1', result) self.assertIn('Workbook 2', result) self.assertIn('#', result) # Header should have row numbers def test_long_name_truncation(self): """Test that long names are truncated""" files = [ {'name': 'A' * 50, 'type': 'workbook', 'path': '/'} ] result = format_file_table(files) # Name should be truncated to 40 chars self.assertNotIn('A' * 50, result) class TestFolderPathFiltering(unittest.TestCase): """Test cases for folder path filtering logic""" def test_exact_folder_match(self): """Test exact folder path matching""" folder_filter = 'MyFolder' folder_filter_with_sep = folder_filter + '/' test_paths = [ ('MyFolder', True), # Exact match ('MyFolder/', True), # With trailing slash treated same ('MyFolder/Subfolder', True), # Subfolder ('MyFolderExtra', False), # Should NOT match ('Other/MyFolder', False), # Different path ] for path, expected in test_paths: matches = path == folder_filter or path.startswith(folder_filter_with_sep) self.assertEqual( matches, expected, f"Path '{path}' expected {expected} but got {matches}" ) class TestHTTPErrorRetryLogic(unittest.TestCase): """Test cases for HTTP error retry logic in bulk_transfer_ownership""" def setUp(self): """Set up test fixtures""" self.mock_client = Mock(spec=SigmaAPIClient) self.ownership_manager = OwnershipManager(self.mock_client) def _create_http_error(self, status_code, response_text=''): """Helper to create mock HTTPError""" mock_response = Mock() mock_response.status_code = status_code mock_response.text = response_text error = HTTPError() error.response = mock_response return error def test_retry_on_429_rate_limit(self): """Test retry on 429 rate limit error""" files = [{'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}] # First call fails with 429, second succeeds self.mock_client.patch.side_effect = [ self._create_http_error(429, 'Rate limit exceeded'), {'id': '1', 'ownerId': 'new_owner'} ] results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, max_retries=3 ) self.assertEqual(len(results), 1) self.assertTrue(results[0].success) self.assertEqual(self.mock_client.patch.call_count, 2) def test_retry_on_500_server_error(self): """Test retry on 500 server error""" files = [{'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}] # First call fails with 500, second succeeds self.mock_client.patch.side_effect = [ self._create_http_error(500, 'Internal Server Error'), {'id': '1', 'ownerId': 'new_owner'} ] results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, max_retries=3 ) self.assertEqual(len(results), 1) self.assertTrue(results[0].success) self.assertEqual(self.mock_client.patch.call_count, 2) def test_retry_on_502_bad_gateway(self): """Test retry on 502 bad gateway error""" files = [{'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}] self.mock_client.patch.side_effect = [ self._create_http_error(502, 'Bad Gateway'), {'id': '1', 'ownerId': 'new_owner'} ] results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, max_retries=3 ) self.assertTrue(results[0].success) def test_retry_on_503_service_unavailable(self): """Test retry on 503 service unavailable error""" files = [{'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}] self.mock_client.patch.side_effect = [ self._create_http_error(503, 'Service Unavailable'), {'id': '1', 'ownerId': 'new_owner'} ] results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, max_retries=3 ) self.assertTrue(results[0].success) def test_no_retry_on_404_not_found(self): """Test no retry on 404 not found error""" files = [{'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}] self.mock_client.patch.side_effect = self._create_http_error(404, 'Not Found') results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, max_retries=3 ) self.assertEqual(len(results), 1) self.assertFalse(results[0].success) self.assertIn('404', results[0].error) # Should not retry - only 1 call self.assertEqual(self.mock_client.patch.call_count, 1) def test_no_retry_on_403_forbidden(self): """Test no retry on 403 forbidden error""" files = [{'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}] self.mock_client.patch.side_effect = self._create_http_error(403, 'Forbidden') results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, max_retries=3 ) self.assertFalse(results[0].success) self.assertIn('403', results[0].error) self.assertEqual(self.mock_client.patch.call_count, 1) def test_max_retries_exceeded(self): """Test failure after max retries exceeded""" files = [{'id': '1', 'name': 'Workbook 1', 'type': 'workbook'}] # All retries fail with 429 self.mock_client.patch.side_effect = [ self._create_http_error(429, 'Rate limit'), self._create_http_error(429, 'Rate limit'), self._create_http_error(429, 'Rate limit'), ] results = self.ownership_manager.bulk_transfer_ownership( files, 'new_owner', dry_run=False, rate_limit_delay=0, max_retries=3 ) self.assertFalse(results[0].success) self.assertIn('429', results[0].error) self.assertIn('retries', results[0].error) self.assertEqual(self.mock_client.patch.call_count, 3) if __name__ == '__main__': unittest.main()