| # 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 |
| |
| from unittest.mock import Mock, patch |
| |
| import pytest |
| from superset_extensions_cli.cli import app, validate_npm |
| |
| |
| # Validate Command Tests |
| @pytest.mark.cli |
| def test_validate_command_success(cli_runner): |
| """Test validate command succeeds when npm is available and valid.""" |
| with patch("superset_extensions_cli.cli.validate_npm") as mock_validate: |
| result = cli_runner.invoke(app, ["validate"]) |
| |
| assert result.exit_code == 0 |
| assert "✅ Validation successful" in result.output |
| mock_validate.assert_called_once() |
| |
| |
| @pytest.mark.cli |
| def test_validate_command_calls_npm_validation(cli_runner): |
| """Test that validate command calls the npm validation function.""" |
| with patch("superset_extensions_cli.cli.validate_npm") as mock_validate: |
| cli_runner.invoke(app, ["validate"]) |
| mock_validate.assert_called_once() |
| |
| |
| # Validate NPM Function Tests |
| @pytest.mark.unit |
| @patch("shutil.which") |
| def test_validate_npm_fails_when_npm_not_on_path(mock_which): |
| """Test validate_npm fails when npm is not on PATH.""" |
| mock_which.return_value = None |
| |
| with pytest.raises(SystemExit) as exc_info: |
| validate_npm() |
| |
| assert exc_info.value.code == 1 |
| mock_which.assert_called_once_with("npm") |
| |
| |
| @pytest.mark.unit |
| @patch("shutil.which") |
| @patch("subprocess.run") |
| def test_validate_npm_fails_when_npm_command_fails(mock_run, mock_which): |
| """Test validate_npm fails when npm -v command fails.""" |
| mock_which.return_value = "/usr/bin/npm" |
| mock_run.return_value = Mock(returncode=1, stderr="Command failed") |
| |
| with pytest.raises(SystemExit) as exc_info: |
| validate_npm() |
| |
| assert exc_info.value.code == 1 |
| |
| |
| @pytest.mark.unit |
| @patch("shutil.which") |
| @patch("subprocess.run") |
| def test_validate_npm_fails_when_version_too_low(mock_run, mock_which): |
| """Test validate_npm fails when npm version is below minimum.""" |
| mock_which.return_value = "/usr/bin/npm" |
| mock_run.return_value = Mock(returncode=0, stdout="9.0.0\n", stderr="") |
| |
| with pytest.raises(SystemExit) as exc_info: |
| validate_npm() |
| |
| assert exc_info.value.code == 1 |
| |
| |
| @pytest.mark.unit |
| @pytest.mark.parametrize( |
| "npm_version", |
| [ |
| "10.8.2", # Exact minimum version |
| "11.0.0", # Higher version |
| "10.9.0-alpha.1", # Pre-release version higher than minimum |
| ], |
| ) |
| @patch("shutil.which") |
| @patch("subprocess.run") |
| def test_validate_npm_succeeds_with_valid_versions(mock_run, mock_which, npm_version): |
| """Test validate_npm succeeds when npm version is valid.""" |
| mock_which.return_value = "/usr/bin/npm" |
| mock_run.return_value = Mock(returncode=0, stdout=f"{npm_version}\n", stderr="") |
| |
| # Should not raise SystemExit |
| validate_npm() |
| |
| |
| @pytest.mark.unit |
| @pytest.mark.parametrize( |
| "npm_version,should_pass", |
| [ |
| ("10.8.2", True), # Exact minimum version |
| ("10.8.1", False), # Slightly lower version |
| ("10.9.0-alpha.1", True), # Pre-release version higher than minimum |
| ("9.9.9", False), # Much lower version |
| ("11.0.0", True), # Much higher version |
| ], |
| ) |
| @patch("shutil.which") |
| @patch("subprocess.run") |
| def test_validate_npm_version_comparison_edge_cases( |
| mock_run, mock_which, npm_version, should_pass |
| ): |
| """Test npm version comparison with edge cases.""" |
| mock_which.return_value = "/usr/bin/npm" |
| mock_run.return_value = Mock(returncode=0, stdout=f"{npm_version}\n", stderr="") |
| |
| if should_pass: |
| # Should not raise SystemExit |
| validate_npm() |
| else: |
| with pytest.raises(SystemExit): |
| validate_npm() |
| |
| |
| @pytest.mark.unit |
| @patch("shutil.which") |
| @patch("subprocess.run") |
| def test_validate_npm_handles_file_not_found_exception(mock_run, mock_which): |
| """Test validate_npm handles FileNotFoundError gracefully.""" |
| mock_which.return_value = "/usr/bin/npm" |
| mock_run.side_effect = FileNotFoundError("Test error") |
| |
| with pytest.raises(SystemExit) as exc_info: |
| validate_npm() |
| |
| assert exc_info.value.code == 1 |
| |
| |
| @pytest.mark.unit |
| @pytest.mark.parametrize( |
| "exception_type", |
| [ |
| OSError, |
| PermissionError, |
| ], |
| ) |
| @patch("shutil.which") |
| @patch("subprocess.run") |
| def test_validate_npm_does_not_catch_other_subprocess_exceptions( |
| mock_run, mock_which, exception_type |
| ): |
| """ |
| Test validate_npm does not catch OSError and PermissionError (they propagate up). |
| """ |
| mock_which.return_value = "/usr/bin/npm" |
| mock_run.side_effect = exception_type("Test error") |
| |
| # These exceptions should propagate up, not be caught |
| with pytest.raises(exception_type): |
| validate_npm() |
| |
| |
| @pytest.mark.unit |
| @patch("shutil.which") |
| @patch("subprocess.run") |
| def test_validate_npm_with_malformed_version_output_raises_error(mock_run, mock_which): |
| """Test validate_npm raises ValueError with malformed version output.""" |
| mock_which.return_value = "/usr/bin/npm" |
| mock_run.return_value = Mock(returncode=0, stdout="not-a-version\n", stderr="") |
| |
| # semver.compare will raise ValueError for malformed version |
| with pytest.raises(ValueError): |
| validate_npm() |
| |
| |
| @pytest.mark.unit |
| @patch("shutil.which") |
| @patch("subprocess.run") |
| def test_validate_npm_with_empty_version_output_raises_error(mock_run, mock_which): |
| """Test validate_npm raises ValueError with empty version output.""" |
| mock_which.return_value = "/usr/bin/npm" |
| mock_run.return_value = Mock(returncode=0, stdout="", stderr="") |
| |
| # semver.compare will raise ValueError for empty version |
| with pytest.raises(ValueError): |
| validate_npm() |