From 4279ce720c6a3cafb6f0fa25639d41d6e56c63e2 Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:27:16 -0500 Subject: [PATCH] fix: reject trailing newlines in preset IDs Require the entire preset ID to match the allowed character set and cover valid and malformed IDs with regression tests. Assisted-by: GitHub Copilot (model: GPT-6.1 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/presets/_manifest.py | 4 ++-- tests/specify_cli/presets/test_manifest.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/specify_cli/presets/_manifest.py b/src/specify_cli/presets/_manifest.py index b5a7ff03c3..6cd7d0d54e 100644 --- a/src/specify_cli/presets/_manifest.py +++ b/src/specify_cli/presets/_manifest.py @@ -97,7 +97,7 @@ def _validate(self): # Validate preset metadata pack = self.data["preset"] # Check presence AND type: the format/version checks below feed these - # values straight to ``re.match`` and ``packaging.Version``, both of + # values straight to ``re.fullmatch`` and ``packaging.Version``, both of # which raise a bare TypeError on a non-string. YAML makes that an easy # authoring slip -- unquoted ``version: 1.0`` parses as a float and # ``id: 2`` as an int -- and TypeError is not a PresetValidationError, @@ -117,7 +117,7 @@ def _validate(self): ) # Validate pack ID format - if not re.match(r'^[a-z0-9-]+$', pack["id"]): + if not re.fullmatch(r'[a-z0-9-]+', pack["id"]): raise PresetValidationError( f"Invalid preset ID '{pack['id']}': " "must be lowercase alphanumeric with hyphens only" diff --git a/tests/specify_cli/presets/test_manifest.py b/tests/specify_cli/presets/test_manifest.py index 27ac723daa..7fa2cc4992 100644 --- a/tests/specify_cli/presets/test_manifest.py +++ b/tests/specify_cli/presets/test_manifest.py @@ -199,15 +199,29 @@ def test_missing_pack_id(self, temp_dir, valid_pack_data): with pytest.raises(PresetValidationError, match="Missing preset.id"): PresetManifest(manifest_path) - def test_invalid_pack_id_format(self, temp_dir, valid_pack_data): + @pytest.mark.parametrize( + "pack_id", + ["Invalid_ID", "", "test-pack\n", "test-pack\r\n", "test\npack", + " test-pack", "test-pack ", "../test-pack", "test/pack"], + ) + def test_invalid_pack_id_format(self, temp_dir, valid_pack_data, pack_id): """Test invalid pack ID format.""" - valid_pack_data["preset"]["id"] = "Invalid_ID" + valid_pack_data["preset"]["id"] = pack_id manifest_path = temp_dir / "preset.yml" with open(manifest_path, 'w') as f: yaml.dump(valid_pack_data, f) with pytest.raises(PresetValidationError, match="Invalid preset ID"): PresetManifest(manifest_path) + @pytest.mark.parametrize("pack_id", ["test-pack", "preset123", "123", "a"]) + def test_valid_pack_id_format(self, temp_dir, valid_pack_data, pack_id): + """Accept IDs containing only lowercase letters, digits, and hyphens.""" + valid_pack_data["preset"]["id"] = pack_id + manifest_path = temp_dir / "preset.yml" + manifest_path.write_text(yaml.safe_dump(valid_pack_data), encoding="utf-8") + + assert PresetManifest(manifest_path).id == pack_id + def test_invalid_version(self, temp_dir, valid_pack_data): """Test invalid semantic version.""" valid_pack_data["preset"]["version"] = "not-a-version"