| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| from __future__ import annotations |
| |
| import json |
| import zipfile |
| from unittest.mock import patch |
| |
| import pytest |
| from superset_extensions_cli.cli import app |
| |
| from tests.utils import assert_file_exists |
| |
| |
| # Bundle Command Tests |
| @pytest.mark.cli |
| @patch("superset_extensions_cli.cli.build") |
| def test_bundle_command_creates_zip_with_default_name( |
| mock_build, cli_runner, isolated_filesystem, extension_setup_for_bundling |
| ): |
| """Test bundle command creates zip file with default name.""" |
| # Mock the build command to do nothing (we'll set up dist manually) |
| mock_build.return_value = None |
| |
| # Setup extension for bundling (this creates the dist structure) |
| extension_setup_for_bundling(isolated_filesystem) |
| |
| result = cli_runner.invoke(app, ["bundle"]) |
| |
| assert result.exit_code == 0 |
| assert "✅ Bundle created: test_extension-1.0.0.supx" in result.output |
| |
| # Verify zip file was created |
| zip_path = isolated_filesystem / "test_extension-1.0.0.supx" |
| assert_file_exists(zip_path) |
| |
| # Verify zip contents |
| with zipfile.ZipFile(zip_path, "r") as zipf: |
| file_list = zipf.namelist() |
| assert "manifest.json" in file_list |
| assert "frontend/dist/remoteEntry.abc123.js" in file_list |
| assert "frontend/dist/main.js" in file_list |
| assert "backend/src/test_extension/__init__.py" in file_list |
| |
| |
| @pytest.mark.cli |
| @patch("superset_extensions_cli.cli.build") |
| def test_bundle_command_with_custom_output_filename( |
| mock_build, cli_runner, isolated_filesystem, extension_setup_for_bundling |
| ): |
| """Test bundle command with custom output filename.""" |
| # Mock the build command |
| mock_build.return_value = None |
| |
| extension_setup_for_bundling(isolated_filesystem) |
| |
| custom_name = "my_custom_bundle.supx" |
| result = cli_runner.invoke(app, ["bundle", "--output", custom_name]) |
| |
| assert result.exit_code == 0 |
| assert f"✅ Bundle created: {custom_name}" in result.output |
| |
| # Verify custom-named zip file was created |
| zip_path = isolated_filesystem / custom_name |
| assert_file_exists(zip_path) |
| |
| |
| @pytest.mark.cli |
| @patch("superset_extensions_cli.cli.build") |
| def test_bundle_command_with_output_directory( |
| mock_build, cli_runner, isolated_filesystem, extension_setup_for_bundling |
| ): |
| """Test bundle command with output directory.""" |
| # Mock the build command |
| mock_build.return_value = None |
| |
| extension_setup_for_bundling(isolated_filesystem) |
| |
| # Create output directory |
| output_dir = isolated_filesystem / "output" |
| output_dir.mkdir() |
| |
| result = cli_runner.invoke(app, ["bundle", "--output", str(output_dir)]) |
| |
| assert result.exit_code == 0 |
| |
| # Verify zip file was created in output directory |
| expected_path = output_dir / "test_extension-1.0.0.supx" |
| assert_file_exists(expected_path) |
| assert f"✅ Bundle created: {expected_path}" in result.output |
| |
| |
| @pytest.mark.cli |
| @patch("superset_extensions_cli.cli.build") |
| def test_bundle_command_fails_without_manifest( |
| mock_build, cli_runner, isolated_filesystem |
| ): |
| """Test bundle command fails when manifest.json doesn't exist.""" |
| # Mock build to succeed but not create manifest |
| mock_build.return_value = None |
| |
| # Create empty dist directory |
| (isolated_filesystem / "dist").mkdir() |
| |
| result = cli_runner.invoke(app, ["bundle"]) |
| |
| assert result.exit_code == 1 |
| assert "dist/manifest.json not found" in result.output |
| |
| |
| @pytest.mark.cli |
| @patch("superset_extensions_cli.cli.build") |
| def test_bundle_command_handles_zip_creation_error( |
| mock_build, cli_runner, isolated_filesystem, extension_setup_for_bundling |
| ): |
| """Test bundle command handles zip file creation errors.""" |
| # Mock the build command |
| mock_build.return_value = None |
| |
| extension_setup_for_bundling(isolated_filesystem) |
| |
| # Try to bundle to an invalid location (directory that doesn't exist) |
| invalid_path = isolated_filesystem / "nonexistent" / "bundle.supx" |
| |
| with patch("zipfile.ZipFile", side_effect=OSError("Permission denied")): |
| result = cli_runner.invoke(app, ["bundle", "--output", str(invalid_path)]) |
| |
| assert result.exit_code == 1 |
| assert "Failed to create bundle" in result.output |
| |
| |
| @pytest.mark.cli |
| @patch("superset_extensions_cli.cli.build") |
| def test_bundle_includes_all_files_recursively( |
| mock_build, cli_runner, isolated_filesystem |
| ): |
| """Test that bundle includes all files from dist directory recursively.""" |
| # Mock the build command |
| mock_build.return_value = None |
| |
| # Create complex dist structure |
| dist_dir = isolated_filesystem / "dist" |
| dist_dir.mkdir(parents=True) |
| |
| # Manifest |
| manifest = { |
| "id": "complex_extension", |
| "name": "Complex Extension", |
| "version": "2.1.0", |
| "permissions": [], |
| } |
| (dist_dir / "manifest.json").write_text(json.dumps(manifest)) |
| |
| # Frontend files with nested structure |
| frontend_dir = dist_dir / "frontend" / "dist" |
| frontend_dir.mkdir(parents=True) |
| (frontend_dir / "remoteEntry.xyz789.js").write_text("// entry") |
| |
| assets_dir = frontend_dir / "assets" |
| assets_dir.mkdir() |
| (assets_dir / "style.css").write_text("/* css */") |
| (assets_dir / "image.png").write_bytes(b"fake image data") |
| |
| # Backend files with nested structure |
| backend_dir = dist_dir / "backend" / "src" / "complex_extension" |
| backend_dir.mkdir(parents=True) |
| (backend_dir / "__init__.py").write_text("# init") |
| (backend_dir / "core.py").write_text("# core") |
| |
| utils_dir = backend_dir / "utils" |
| utils_dir.mkdir() |
| (utils_dir / "helpers.py").write_text("# helpers") |
| |
| result = cli_runner.invoke(app, ["bundle"]) |
| |
| assert result.exit_code == 0 |
| |
| # Verify zip file and contents |
| zip_path = isolated_filesystem / "complex_extension-2.1.0.supx" |
| assert_file_exists(zip_path) |
| |
| with zipfile.ZipFile(zip_path, "r") as zipf: |
| file_list = set(zipf.namelist()) |
| |
| # Verify all files are included |
| expected_files = { |
| "manifest.json", |
| "frontend/dist/remoteEntry.xyz789.js", |
| "frontend/dist/assets/style.css", |
| "frontend/dist/assets/image.png", |
| "backend/src/complex_extension/__init__.py", |
| "backend/src/complex_extension/core.py", |
| "backend/src/complex_extension/utils/helpers.py", |
| } |
| |
| assert expected_files.issubset(file_list), ( |
| f"Missing files: {expected_files - file_list}" |
| ) |
| |
| |
| @pytest.mark.cli |
| @patch("superset_extensions_cli.cli.build") |
| def test_bundle_command_short_option( |
| mock_build, cli_runner, isolated_filesystem, extension_setup_for_bundling |
| ): |
| """Test bundle command with short -o option.""" |
| # Mock the build command |
| mock_build.return_value = None |
| |
| extension_setup_for_bundling(isolated_filesystem) |
| |
| result = cli_runner.invoke(app, ["bundle", "-o", "short_option.supx"]) |
| |
| assert result.exit_code == 0 |
| assert "✅ Bundle created: short_option.supx" in result.output |
| assert_file_exists(isolated_filesystem / "short_option.supx") |
| |
| |
| @pytest.mark.cli |
| @pytest.mark.parametrize("output_option", ["--output", "-o"]) |
| @patch("superset_extensions_cli.cli.build") |
| def test_bundle_command_output_options( |
| mock_build, |
| output_option, |
| cli_runner, |
| isolated_filesystem, |
| extension_setup_for_bundling, |
| ): |
| """Test bundle command with both long and short output options.""" |
| # Mock the build command |
| mock_build.return_value = None |
| |
| extension_setup_for_bundling(isolated_filesystem) |
| |
| filename = f"test_{output_option.replace('-', '')}.supx" |
| result = cli_runner.invoke(app, ["bundle", output_option, filename]) |
| |
| assert result.exit_code == 0 |
| assert f"✅ Bundle created: {filename}" in result.output |
| assert_file_exists(isolated_filesystem / filename) |