|
6 | 6 | from click.testing import CliRunner |
7 | 7 |
|
8 | 8 | from weft.cli.runtime import down, logs, up |
9 | | -from weft.cli.runtime.helpers import validate_docker |
| 9 | +from weft.cli.runtime.helpers import sanitize_docker_project_name, validate_docker |
| 10 | + |
| 11 | + |
| 12 | +class TestSanitizeDockerProjectName: |
| 13 | + """Tests for Docker Compose project name sanitization.""" |
| 14 | + |
| 15 | + def test_valid_name_unchanged(self): |
| 16 | + """Test that valid names are not modified.""" |
| 17 | + assert sanitize_docker_project_name("myproject") == "myproject" |
| 18 | + assert sanitize_docker_project_name("my-project") == "my-project" |
| 19 | + assert sanitize_docker_project_name("my_project") == "my_project" |
| 20 | + assert sanitize_docker_project_name("project123") == "project123" |
| 21 | + assert sanitize_docker_project_name("123project") == "123project" |
| 22 | + |
| 23 | + def test_uppercase_converted_to_lowercase(self): |
| 24 | + """Test that uppercase letters are converted to lowercase.""" |
| 25 | + assert sanitize_docker_project_name("MyProject") == "myproject" |
| 26 | + assert sanitize_docker_project_name("MY-PROJECT") == "my-project" |
| 27 | + assert sanitize_docker_project_name("MY_PROJECT") == "my_project" |
| 28 | + |
| 29 | + def test_periods_replaced_with_hyphens(self): |
| 30 | + """Test that periods are replaced with hyphens.""" |
| 31 | + assert sanitize_docker_project_name("my.project") == "my-project" |
| 32 | + assert sanitize_docker_project_name("example.app") == "example-app" |
| 33 | + assert sanitize_docker_project_name("app.example.com") == "app-example-com" |
| 34 | + |
| 35 | + def test_multiple_invalid_chars_replaced(self): |
| 36 | + """Test that sequences of invalid characters are replaced with single hyphen.""" |
| 37 | + assert sanitize_docker_project_name("my..project") == "my-project" |
| 38 | + assert sanitize_docker_project_name("my...project") == "my-project" |
| 39 | + # Note: existing hyphens are valid and preserved, so "my.-.project" keeps all hyphens |
| 40 | + assert sanitize_docker_project_name("my.-.project") == "my---project" |
| 41 | + assert sanitize_docker_project_name("my@#$project") == "my-project" |
| 42 | + |
| 43 | + def test_special_characters_replaced(self): |
| 44 | + """Test that special characters are replaced with hyphens.""" |
| 45 | + assert sanitize_docker_project_name("my@project") == "my-project" |
| 46 | + assert sanitize_docker_project_name("my!project") == "my-project" |
| 47 | + assert sanitize_docker_project_name("my project") == "my-project" # space |
| 48 | + assert sanitize_docker_project_name("my&project") == "my-project" |
| 49 | + |
| 50 | + def test_leading_trailing_hyphens_removed(self): |
| 51 | + """Test that leading and trailing hyphens are removed.""" |
| 52 | + assert sanitize_docker_project_name("-myproject") == "myproject" |
| 53 | + assert sanitize_docker_project_name("myproject-") == "myproject" |
| 54 | + assert sanitize_docker_project_name("-myproject-") == "myproject" |
| 55 | + assert sanitize_docker_project_name("---myproject---") == "myproject" |
| 56 | + |
| 57 | + def test_leading_trailing_underscores_removed(self): |
| 58 | + """Test that leading and trailing underscores are removed.""" |
| 59 | + assert sanitize_docker_project_name("_myproject") == "myproject" |
| 60 | + assert sanitize_docker_project_name("myproject_") == "myproject" |
| 61 | + assert sanitize_docker_project_name("_myproject_") == "myproject" |
| 62 | + |
| 63 | + def test_starts_with_non_alphanumeric_prepends_prefix(self): |
| 64 | + """Test that names starting with non-alphanumeric get prefix.""" |
| 65 | + # After stripping leading hyphens/underscores, if it still doesn't start with alphanumeric |
| 66 | + # Actually, our function strips leading hyphens/underscores first, so this case is rare |
| 67 | + # But we test it anyway for edge cases |
| 68 | + assert ( |
| 69 | + sanitize_docker_project_name("123project") == "123project" |
| 70 | + ) # Starts with number (valid) |
| 71 | + assert sanitize_docker_project_name("_project") == "project" # Underscore stripped |
| 72 | + |
| 73 | + def test_empty_string_returns_fallback(self): |
| 74 | + """Test that empty string returns fallback name.""" |
| 75 | + assert sanitize_docker_project_name("") == "weft-project" |
| 76 | + assert sanitize_docker_project_name(" ") == "weft-project" |
| 77 | + assert sanitize_docker_project_name("...") == "weft-project" |
| 78 | + assert sanitize_docker_project_name("@@@") == "weft-project" |
| 79 | + |
| 80 | + def test_only_invalid_characters(self): |
| 81 | + """Test names with only invalid characters.""" |
| 82 | + assert sanitize_docker_project_name("...") == "weft-project" |
| 83 | + assert sanitize_docker_project_name("@#$%") == "weft-project" |
| 84 | + assert sanitize_docker_project_name(" ") == "weft-project" |
| 85 | + |
| 86 | + def test_complex_real_world_names(self): |
| 87 | + """Test complex real-world project names.""" |
| 88 | + assert sanitize_docker_project_name("MyApp.Backend.API") == "myapp-backend-api" |
| 89 | + assert sanitize_docker_project_name("company.com-app") == "company-com-app" |
| 90 | + # Note: underscores are valid characters and preserved |
| 91 | + assert sanitize_docker_project_name("Project_2024.v1.0") == "project_2024-v1-0" |
| 92 | + assert sanitize_docker_project_name("my-AWESOME-app!") == "my-awesome-app" |
10 | 93 |
|
11 | 94 |
|
12 | 95 | class TestValidateDocker: |
@@ -89,6 +172,51 @@ def test_up_success( |
89 | 172 | assert "watcher-meta" in call_args |
90 | 173 | assert "watcher-architect" in call_args |
91 | 174 |
|
| 175 | + @patch("subprocess.run") |
| 176 | + @patch("weft.cli.runtime.up.validate_docker") |
| 177 | + @patch("weft.cli.runtime.up.check_docker_daemon") |
| 178 | + @patch("weft.cli.runtime.up.load_weftrc") |
| 179 | + @patch("weft.cli.runtime.helpers.get_project_root") |
| 180 | + def test_up_sanitizes_project_name_with_periods( |
| 181 | + self, |
| 182 | + mock_get_root, |
| 183 | + mock_load_config, |
| 184 | + mock_check_daemon, |
| 185 | + mock_validate, |
| 186 | + mock_run, |
| 187 | + tmp_path: Path, |
| 188 | + monkeypatch, |
| 189 | + ): |
| 190 | + """Test that project names with periods are sanitized for Docker Compose.""" |
| 191 | + monkeypatch.chdir(tmp_path) |
| 192 | + mock_get_root.return_value = tmp_path |
| 193 | + |
| 194 | + # Create project with name containing period |
| 195 | + (tmp_path / ".weftrc.yaml").write_text("project:\n name: example.app\n type: backend\n") |
| 196 | + (tmp_path / "docker-compose.yml").write_text("version: '3'\n") |
| 197 | + |
| 198 | + # Mock config with project name containing period |
| 199 | + mock_config = Mock() |
| 200 | + mock_config.project.name = "example.app" # Contains period (invalid for Docker Compose) |
| 201 | + mock_config.agents.enabled = ["meta"] |
| 202 | + mock_config.ai.provider = "anthropic" |
| 203 | + mock_config.ai.model = "claude-3-5-sonnet-20241022" |
| 204 | + mock_config.ai.history_path = "./weft-ai-history" |
| 205 | + mock_load_config.return_value = mock_config |
| 206 | + |
| 207 | + mock_validate.return_value = True |
| 208 | + mock_check_daemon.return_value = True |
| 209 | + mock_run.return_value = Mock(returncode=0) |
| 210 | + |
| 211 | + runner = CliRunner() |
| 212 | + result = runner.invoke(up) |
| 213 | + |
| 214 | + assert result.exit_code == 0 |
| 215 | + |
| 216 | + # Verify docker compose was called with sanitized project name in environment |
| 217 | + env_vars = mock_run.call_args[1]["env"] |
| 218 | + assert env_vars["COMPOSE_PROJECT_NAME"] == "example-app" # Sanitized |
| 219 | + |
92 | 220 | @patch("weft.cli.runtime.up.validate_docker") |
93 | 221 | def test_up_docker_not_installed(self, mock_validate, tmp_path: Path, monkeypatch): |
94 | 222 | """Test 'weft up' when docker not installed.""" |
|
0 commit comments