|
1 | 1 | import importlib |
2 | 2 | import os |
| 3 | +import stat |
3 | 4 | import sys |
4 | 5 | from unittest.mock import MagicMock, call, patch |
5 | 6 |
|
@@ -510,3 +511,80 @@ def test_render_progress_bar(monkeypatch): |
510 | 511 | # Color codes (accessing instance Colors or m.Colors) |
511 | 512 | assert m.Colors.CYAN in combined |
512 | 513 | assert m.Colors.ENDC in combined |
| 514 | + |
| 515 | + |
| 516 | +# Case 14: validate_folder_url rejects URLs with credentials |
| 517 | +def test_validate_folder_url_rejects_credentials(monkeypatch): |
| 518 | + m = reload_main_with_env(monkeypatch) |
| 519 | + mock_log = MagicMock() |
| 520 | + monkeypatch.setattr(m, "log", mock_log) |
| 521 | + |
| 522 | + # Use a mock for socket.getaddrinfo so we don't hit network or fail DNS |
| 523 | + # But validate_folder_url logic for credentials happens BEFORE DNS. |
| 524 | + # So we don't strictly need to mock DNS if it returns early. |
| 525 | + # However, if it falls through (fails logic), it hits DNS. |
| 526 | + # To be safe and isolated, mock getaddrinfo. |
| 527 | + with patch("socket.getaddrinfo") as mock_dns: |
| 528 | + mock_dns.return_value = [(2, 1, 6, "", ("1.1.1.1", 443))] |
| 529 | + # URL with credentials |
| 530 | + url = "https://user:[email protected]/list.json" |
| 531 | + assert m.validate_folder_url(url) is False |
| 532 | + assert mock_log.warning.called |
| 533 | + # Check that warning message contains "credentials detected" |
| 534 | + # call_args is (args, kwargs) |
| 535 | + args, _ = mock_log.warning.call_args |
| 536 | + assert "credentials detected" in args[0] |
| 537 | + |
| 538 | + |
| 539 | +# Case 15: plan.json is created with secure permissions |
| 540 | +def test_plan_json_secure_permissions(monkeypatch, tmp_path, capsys): |
| 541 | + # Prepare environment |
| 542 | + monkeypatch.setenv("TOKEN", "dummy") |
| 543 | + m = reload_main_with_env(monkeypatch) |
| 544 | + |
| 545 | + # Mock args |
| 546 | + plan_file = tmp_path / "plan.json" |
| 547 | + mock_args = MagicMock() |
| 548 | + mock_args.plan_json = str(plan_file) |
| 549 | + mock_args.dry_run = True |
| 550 | + mock_args.profiles = "p1" |
| 551 | + # Empty folder_url to avoid fetch logic |
| 552 | + mock_args.folder_url = [] |
| 553 | + mock_args.no_delete = False |
| 554 | + |
| 555 | + monkeypatch.setattr(m, "parse_args", lambda: mock_args) |
| 556 | + m.TOKEN = "dummy" |
| 557 | + |
| 558 | + # Mock warm_up_cache to avoid actual network |
| 559 | + monkeypatch.setattr(m, "warm_up_cache", MagicMock()) |
| 560 | + |
| 561 | + # Mock sync_profile so it returns True and populates plan |
| 562 | + def mock_sync(pid, urls, dry_run, no_delete, plan_accumulator=None): |
| 563 | + if plan_accumulator is not None: |
| 564 | + # Add a dummy plan entry |
| 565 | + plan_accumulator.append({ |
| 566 | + "profile": pid, |
| 567 | + "folders": [{"name": "F1", "rules": 10, "action": 0, "status": 1}] |
| 568 | + }) |
| 569 | + return True |
| 570 | + |
| 571 | + monkeypatch.setattr(m, "sync_profile", mock_sync) |
| 572 | + |
| 573 | + # Run main, expect SystemExit(0) on success |
| 574 | + with pytest.raises(SystemExit) as e: |
| 575 | + m.main() |
| 576 | + assert e.value.code == 0 |
| 577 | + |
| 578 | + # Verify file exists |
| 579 | + assert plan_file.exists() |
| 580 | + |
| 581 | + # Verify permissions on POSIX |
| 582 | + if os.name != "nt": |
| 583 | + mode = os.stat(plan_file).st_mode |
| 584 | + # Check permissions are 600 (rw-------) |
| 585 | + # S_IRWXG = 0o070 (group rwx) |
| 586 | + # S_IRWXO = 0o007 (other rwx) |
| 587 | + assert not (mode & stat.S_IRWXG), "Group should have no permissions" |
| 588 | + assert not (mode & stat.S_IRWXO), "Others should have no permissions" |
| 589 | + assert mode & stat.S_IRUSR, "Owner should have read" |
| 590 | + assert mode & stat.S_IWUSR, "Owner should have write" |
0 commit comments