diff --git a/src/specify_cli/bundles/sources.py b/src/specify_cli/bundles/sources.py index 3a6ebe5d4c..97d2022a1a 100644 --- a/src/specify_cli/bundles/sources.py +++ b/src/specify_cli/bundles/sources.py @@ -36,7 +36,7 @@ def _local_manifest_source(arg: str): raise BundlerError(f"No bundle.yml found in '{candidate}'.") return BundleManifest.from_file(manifest_path) - if candidate.suffix == ".zip": + if candidate.suffix.lower() == ".zip": import yaml as _yaml from .._download_security import open_zip_bounded, read_zip_member_limited diff --git a/tests/specify_cli/bundles/test_command_install.py b/tests/specify_cli/bundles/test_command_install.py index ad50c6f8ae..78d96f0b95 100644 --- a/tests/specify_cli/bundles/test_command_install.py +++ b/tests/specify_cli/bundles/test_command_install.py @@ -132,6 +132,33 @@ def test_local_firstparty_bundle_installs_bundled_components_offline( assert registry["workflows"][bundle_id]["version"] == "1.0.0" +@pytest.mark.parametrize("filename", ["demo-bundle.zip", "DEMO-BUNDLE.ZIP"]) +def test_local_zip_artifact_installs_regardless_of_extension_case( + project: Path, filename: str +): + from specify_cli.bundles.records import load_records + + artifact = project / filename + with zipfile.ZipFile(artifact, "w") as archive: + archive.writestr("bundle.yml", yaml.safe_dump(valid_manifest_dict(provides={}))) + + result = runner.invoke(app, ["bundle", "install", str(artifact), "--offline"]) + + assert result.exit_code == 0, result.output + assert [record.bundle_id for record in load_records(project)] == ["demo-bundle"] + + +def test_local_uppercase_zip_without_manifest_is_rejected(project: Path): + artifact = project / "broken.ZIP" + with zipfile.ZipFile(artifact, "w") as archive: + archive.writestr("README.md", "No manifest") + + result = runner.invoke(app, ["bundle", "install", str(artifact), "--offline"]) + + assert result.exit_code == 1 + assert "does not contain a bundle.yml" in " ".join(result.output.split()) + + def test_local_bundle_rejects_mismatched_bundled_workflow_pin_offline(project: Path): bundle_dir = project / "mismatched-workflow-pin" (bundle_dir / "bundle.yml").parent.mkdir()