|
9 | 9 |
|
10 | 10 | from opsbox.backup.exceptions import ( |
11 | 11 | ConfigurationError, |
| 12 | + FolderNotFoundError, |
12 | 13 | NetworkUnreachableError, |
13 | 14 | SSHKeyNotFoundError, |
14 | 15 | ) |
@@ -177,6 +178,52 @@ def test_config_email_settings_path_not_exists(self) -> None: |
177 | 178 |
|
178 | 179 | assert "Email settings file not found" in str(exc_info.value) |
179 | 180 |
|
| 181 | + def test_config_exclude_file_not_exists(self) -> None: |
| 182 | + """Test raises FolderNotFoundError when exclude_file doesn't exist.""" |
| 183 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 184 | + temp_path = Path(temp_dir) |
| 185 | + email_settings = self._create_test_email_settings(temp_path) |
| 186 | + |
| 187 | + with pytest.raises(FolderNotFoundError): |
| 188 | + RsyncConfig( |
| 189 | + rsync_source="user@host:/source", |
| 190 | + rsync_target="/target", |
| 191 | + email_settings_path=email_settings, |
| 192 | + exclude_file=Path("/nonexistent/exclude.txt"), |
| 193 | + ) |
| 194 | + |
| 195 | + def test_config_exclude_file_optional(self) -> None: |
| 196 | + """Test that exclude_file is optional and None is valid.""" |
| 197 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 198 | + temp_path = Path(temp_dir) |
| 199 | + email_settings = self._create_test_email_settings(temp_path) |
| 200 | + |
| 201 | + config = RsyncConfig( |
| 202 | + rsync_source="user@host:/source", |
| 203 | + rsync_target="/target", |
| 204 | + email_settings_path=email_settings, |
| 205 | + exclude_file=None, |
| 206 | + ) |
| 207 | + |
| 208 | + assert config.exclude_file is None |
| 209 | + |
| 210 | + def test_config_exclude_file_valid(self) -> None: |
| 211 | + """Test that exclude_file is accepted when file exists.""" |
| 212 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 213 | + temp_path = Path(temp_dir) |
| 214 | + email_settings = self._create_test_email_settings(temp_path) |
| 215 | + exclude_file = temp_path / "exclude.txt" |
| 216 | + exclude_file.write_text("*.tmp\n*.log\n") |
| 217 | + |
| 218 | + config = RsyncConfig( |
| 219 | + rsync_source="user@host:/source", |
| 220 | + rsync_target="/target", |
| 221 | + email_settings_path=email_settings, |
| 222 | + exclude_file=exclude_file, |
| 223 | + ) |
| 224 | + |
| 225 | + assert config.exclude_file == exclude_file |
| 226 | + |
180 | 227 |
|
181 | 228 | class TestRsyncManagerInitialization: |
182 | 229 | """Test cases for RsyncManager initialization.""" |
@@ -349,6 +396,89 @@ def test_initialization_missing_required_field(self) -> None: |
349 | 396 |
|
350 | 397 | assert "Missing required configuration field" in str(exc_info.value) |
351 | 398 |
|
| 399 | + @patch("opsbox.rsync.rsync_manager.configure_logging") |
| 400 | + @patch("opsbox.rsync.rsync_manager.EncryptedMail") |
| 401 | + @patch("opsbox.rsync.rsync_manager.LockManager") |
| 402 | + @patch("opsbox.rsync.rsync_manager.NetworkChecker") |
| 403 | + @patch("opsbox.rsync.rsync_manager.SSHManager") |
| 404 | + def test_initialization_with_exclude_file( |
| 405 | + self, |
| 406 | + mock_ssh_manager: MagicMock, |
| 407 | + mock_network_checker: MagicMock, |
| 408 | + mock_lock_manager: MagicMock, |
| 409 | + mock_encrypted_mail: MagicMock, |
| 410 | + mock_configure_logging: MagicMock, |
| 411 | + ) -> None: |
| 412 | + """Test initializes successfully with exclude_file in config.""" |
| 413 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 414 | + temp_path = Path(temp_dir) |
| 415 | + email_settings = self._create_test_email_settings(temp_path) |
| 416 | + exclude_file = temp_path / "exclude.txt" |
| 417 | + exclude_file.write_text("*.tmp\n*.log\n") |
| 418 | + |
| 419 | + config_data = { |
| 420 | + "rsync_source": "user@host:/source", |
| 421 | + "rsync_target": str(temp_path / "target"), |
| 422 | + "email_settings_path": str(email_settings), |
| 423 | + "exclude_file": str(exclude_file), |
| 424 | + } |
| 425 | + config_file = self._create_test_config_file( |
| 426 | + temp_path, |
| 427 | + email_settings, |
| 428 | + config_data, |
| 429 | + ) |
| 430 | + |
| 431 | + target_dir = temp_path / "target" |
| 432 | + target_dir.mkdir() |
| 433 | + |
| 434 | + mock_logger = Mock() |
| 435 | + mock_configure_logging.return_value = mock_logger |
| 436 | + |
| 437 | + manager = RsyncManager(config_path=config_file, log_level="INFO") |
| 438 | + |
| 439 | + assert manager.config.exclude_file == exclude_file |
| 440 | + |
| 441 | + @patch("opsbox.rsync.rsync_manager.configure_logging") |
| 442 | + @patch("opsbox.rsync.rsync_manager.EncryptedMail") |
| 443 | + @patch("opsbox.rsync.rsync_manager.LockManager") |
| 444 | + @patch("opsbox.rsync.rsync_manager.NetworkChecker") |
| 445 | + @patch("opsbox.rsync.rsync_manager.SSHManager") |
| 446 | + def test_initialization_with_exclude_file_not_exists( |
| 447 | + self, |
| 448 | + mock_ssh_manager: MagicMock, |
| 449 | + mock_network_checker: MagicMock, |
| 450 | + mock_lock_manager: MagicMock, |
| 451 | + mock_encrypted_mail: MagicMock, |
| 452 | + mock_configure_logging: MagicMock, |
| 453 | + ) -> None: |
| 454 | + """Test raises FolderNotFoundError when exclude_file in config doesn't exist.""" |
| 455 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 456 | + temp_path = Path(temp_dir) |
| 457 | + email_settings = self._create_test_email_settings(temp_path) |
| 458 | + |
| 459 | + config_data = { |
| 460 | + "rsync_source": "user@host:/source", |
| 461 | + "rsync_target": str(temp_path / "target"), |
| 462 | + "email_settings_path": str(email_settings), |
| 463 | + "exclude_file": "/nonexistent/exclude.txt", |
| 464 | + } |
| 465 | + config_file = self._create_test_config_file( |
| 466 | + temp_path, |
| 467 | + email_settings, |
| 468 | + config_data, |
| 469 | + ) |
| 470 | + |
| 471 | + target_dir = temp_path / "target" |
| 472 | + target_dir.mkdir() |
| 473 | + |
| 474 | + mock_logger = Mock() |
| 475 | + mock_configure_logging.return_value = mock_logger |
| 476 | + |
| 477 | + with pytest.raises(ConfigurationError) as exc_info: |
| 478 | + RsyncManager(config_path=config_file, log_level="INFO") |
| 479 | + |
| 480 | + assert "Exclude file not found" in str(exc_info.value) |
| 481 | + |
352 | 482 |
|
353 | 483 | class TestRsyncCommandBuilding: |
354 | 484 | """Test cases for rsync command building.""" |
@@ -463,6 +593,69 @@ def test_build_rsync_command_with_log_file(self) -> None: |
463 | 593 | log_file_index = cmd.index("--log-file") |
464 | 594 | assert cmd[log_file_index + 1] == str(manager.log_file_path) |
465 | 595 |
|
| 596 | + def test_build_rsync_command_with_exclude_file(self) -> None: |
| 597 | + """Test includes exclude-from option when exclude_file is configured.""" |
| 598 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 599 | + temp_path = Path(temp_dir) |
| 600 | + exclude_file = temp_path / "exclude.txt" |
| 601 | + exclude_file.write_text("*.tmp\n*.log\n") |
| 602 | + |
| 603 | + config_data = { |
| 604 | + "rsync_source": "user@host:/source", |
| 605 | + "rsync_target": str(temp_path / "target"), |
| 606 | + "email_settings_path": str(temp_path / "email_settings.yaml"), |
| 607 | + "exclude_file": str(exclude_file), |
| 608 | + } |
| 609 | + manager = self._create_test_manager(temp_path, config_data) |
| 610 | + |
| 611 | + cmd = manager._build_rsync_command() |
| 612 | + |
| 613 | + assert "--exclude-from" in cmd |
| 614 | + exclude_index = cmd.index("--exclude-from") |
| 615 | + assert cmd[exclude_index + 1] == str(exclude_file) |
| 616 | + |
| 617 | + def test_build_rsync_command_without_exclude_file(self) -> None: |
| 618 | + """Test does not include exclude-from option when exclude_file is not configured.""" |
| 619 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 620 | + temp_path = Path(temp_dir) |
| 621 | + manager = self._create_test_manager(temp_path) |
| 622 | + |
| 623 | + cmd = manager._build_rsync_command() |
| 624 | + |
| 625 | + assert "--exclude-from" not in cmd |
| 626 | + |
| 627 | + def test_build_rsync_command_with_exclude_file_and_other_options(self) -> None: |
| 628 | + """Test exclude-from works correctly with other rsync options.""" |
| 629 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 630 | + temp_path = Path(temp_dir) |
| 631 | + exclude_file = temp_path / "exclude.txt" |
| 632 | + exclude_file.write_text("*.tmp\n*.log\n") |
| 633 | + |
| 634 | + config_data = { |
| 635 | + "rsync_source": "user@host:/source", |
| 636 | + "rsync_target": str(temp_path / "target"), |
| 637 | + "email_settings_path": str(temp_path / "email_settings.yaml"), |
| 638 | + "exclude_file": str(exclude_file), |
| 639 | + "rsync_options": { |
| 640 | + "chown": "user:group", |
| 641 | + "delete": True, |
| 642 | + "progress": True, |
| 643 | + }, |
| 644 | + } |
| 645 | + manager = self._create_test_manager(temp_path, config_data) |
| 646 | + |
| 647 | + cmd = manager._build_rsync_command() |
| 648 | + |
| 649 | + # Verify all options are present |
| 650 | + assert "--exclude-from" in cmd |
| 651 | + assert "--chown" in cmd |
| 652 | + assert "--delete" in cmd |
| 653 | + assert "--progress" in cmd |
| 654 | + # Verify exclude-from comes before log-file (as per implementation) |
| 655 | + exclude_index = cmd.index("--exclude-from") |
| 656 | + log_file_index = cmd.index("--log-file") |
| 657 | + assert exclude_index < log_file_index |
| 658 | + |
466 | 659 |
|
467 | 660 | class TestRsyncUtilityFunctions: |
468 | 661 | """Test cases for utility functions.""" |
|
0 commit comments