mirror of
https://github.com/apache/superset.git
synced 2026-04-07 18:35:15 +00:00
534 lines
18 KiB
Python
534 lines
18 KiB
Python
# 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 pathlib import Path
|
|
|
|
import pytest
|
|
from superset_extensions_cli.cli import app
|
|
|
|
from tests.utils import (
|
|
assert_directory_exists,
|
|
assert_directory_structure,
|
|
assert_file_exists,
|
|
assert_file_structure,
|
|
assert_json_content,
|
|
create_test_extension_structure,
|
|
load_json_file,
|
|
)
|
|
|
|
|
|
# Init Command Tests
|
|
@pytest.mark.cli
|
|
def test_init_creates_extension_with_both_frontend_and_backend(
|
|
cli_runner, isolated_filesystem, cli_input_both
|
|
):
|
|
"""Test that init creates a complete extension with both frontend and backend."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
|
|
|
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
|
assert (
|
|
"🎉 Extension Test Extension (ID: test-org.test-extension) initialized"
|
|
in result.output
|
|
)
|
|
|
|
# Verify directory structure
|
|
extension_path = isolated_filesystem / "test-org.test-extension"
|
|
assert_directory_exists(extension_path, "main extension directory")
|
|
|
|
expected_structure = create_test_extension_structure(
|
|
isolated_filesystem,
|
|
"test-org.test-extension",
|
|
include_frontend=True,
|
|
include_backend=True,
|
|
)
|
|
|
|
# Check directories
|
|
assert_directory_structure(extension_path, expected_structure["expected_dirs"])
|
|
|
|
# Check files
|
|
assert_file_structure(extension_path, expected_structure["expected_files"])
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_creates_extension_with_frontend_only(
|
|
cli_runner, isolated_filesystem, cli_input_frontend_only
|
|
):
|
|
"""Test that init creates extension with only frontend components."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_frontend_only)
|
|
|
|
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
|
|
|
extension_path = isolated_filesystem / "test-org.test-extension"
|
|
assert_directory_exists(extension_path)
|
|
|
|
# Should have frontend directory and package.json
|
|
assert_directory_exists(extension_path / "frontend")
|
|
assert_file_exists(extension_path / "frontend" / "package.json")
|
|
|
|
# Should NOT have backend directory
|
|
backend_path = extension_path / "backend"
|
|
assert not backend_path.exists(), (
|
|
"Backend directory should not exist for frontend-only extension"
|
|
)
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_creates_extension_with_backend_only(
|
|
cli_runner, isolated_filesystem, cli_input_backend_only
|
|
):
|
|
"""Test that init creates extension with only backend components."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_backend_only)
|
|
|
|
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
|
|
|
extension_path = isolated_filesystem / "test-org.test-extension"
|
|
assert_directory_exists(extension_path)
|
|
|
|
# Should have backend directory and pyproject.toml
|
|
assert_directory_exists(extension_path / "backend")
|
|
assert_file_exists(extension_path / "backend" / "pyproject.toml")
|
|
|
|
# Should NOT have frontend directory
|
|
frontend_path = extension_path / "frontend"
|
|
assert not frontend_path.exists(), (
|
|
"Frontend directory should not exist for backend-only extension"
|
|
)
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_creates_extension_with_neither_frontend_nor_backend(
|
|
cli_runner, isolated_filesystem, cli_input_neither
|
|
):
|
|
"""Test that init creates minimal extension with neither frontend nor backend."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_neither)
|
|
|
|
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
|
|
|
extension_path = isolated_filesystem / "test-org.test-extension"
|
|
assert_directory_exists(extension_path)
|
|
|
|
# Should only have extension.json
|
|
assert_file_exists(extension_path / "extension.json")
|
|
|
|
# Should NOT have frontend or backend directories
|
|
assert not (extension_path / "frontend").exists()
|
|
assert not (extension_path / "backend").exists()
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_accepts_valid_display_name(cli_runner, isolated_filesystem):
|
|
"""Test that init accepts valid display names and generates proper ID."""
|
|
cli_input = "My Awesome Extension\n\ntest-org\n0.1.0\nApache-2.0\ny\ny\n"
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input)
|
|
|
|
assert result.exit_code == 0, f"Should accept display name: {result.output}"
|
|
assert Path("test-org.my-awesome-extension").exists(), (
|
|
"Directory for generated composite ID should be created"
|
|
)
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_accepts_mixed_alphanumeric_name(cli_runner, isolated_filesystem):
|
|
"""Test that init accepts mixed alphanumeric display names."""
|
|
cli_input = "Tool 123\n\ntest-org\n0.1.0\nApache-2.0\ny\ny\n"
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input)
|
|
|
|
assert result.exit_code == 0, (
|
|
f"Mixed alphanumeric display name should be valid: {result.output}"
|
|
)
|
|
assert Path("test-org.tool-123").exists(), (
|
|
"Directory for 'test-org.tool-123' should be created"
|
|
)
|
|
|
|
|
|
@pytest.mark.cli
|
|
@pytest.mark.parametrize(
|
|
"display_name,expected_id",
|
|
[
|
|
("Test Extension", "test-org.test-extension"),
|
|
("My Tool v2", "test-org.my-tool-v2"),
|
|
("Dashboard Helper", "test-org.dashboard-helper"),
|
|
("Chart Builder Pro", "test-org.chart-builder-pro"),
|
|
],
|
|
)
|
|
def test_init_with_various_display_names(cli_runner, display_name, expected_id):
|
|
"""Test that init accepts various display names and generates proper IDs."""
|
|
with cli_runner.isolated_filesystem():
|
|
cli_input = f"{display_name}\n\ntest-org\n0.1.0\nApache-2.0\ny\ny\n"
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input)
|
|
|
|
assert result.exit_code == 0, (
|
|
f"Valid display name '{display_name}' was rejected: {result.output}"
|
|
)
|
|
assert Path(expected_id).exists(), (
|
|
f"Directory for '{expected_id}' was not created"
|
|
)
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_fails_when_directory_already_exists(
|
|
cli_runner, isolated_filesystem, cli_input_both
|
|
):
|
|
"""Test that init fails gracefully when target directory already exists."""
|
|
# Create the directory first
|
|
existing_dir = isolated_filesystem / "test-org.test-extension"
|
|
existing_dir.mkdir()
|
|
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
|
|
|
assert result.exit_code == 1, "Command should fail when directory already exists"
|
|
assert "already exists" in result.output
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_extension_json_content_is_correct(
|
|
cli_runner, isolated_filesystem, cli_input_both
|
|
):
|
|
"""Test that the generated extension.json has the correct content."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
|
assert result.exit_code == 0
|
|
|
|
extension_path = isolated_filesystem / "test-org.test-extension"
|
|
extension_json_path = extension_path / "extension.json"
|
|
|
|
# Verify the JSON structure and values
|
|
assert_json_content(
|
|
extension_json_path,
|
|
{
|
|
"publisher": "test-org",
|
|
"name": "test-extension",
|
|
"displayName": "Test Extension",
|
|
"version": "0.1.0",
|
|
"license": "Apache-2.0",
|
|
"permissions": [],
|
|
},
|
|
)
|
|
|
|
# Load and verify more complex nested structures
|
|
content = load_json_file(extension_json_path)
|
|
|
|
# Verify frontend section is not present (contributions are code-first)
|
|
assert "frontend" not in content
|
|
|
|
# Verify no backend section in extension.json (moved to pyproject.toml)
|
|
assert "backend" not in content
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_frontend_package_json_content_is_correct(
|
|
cli_runner, isolated_filesystem, cli_input_both
|
|
):
|
|
"""Test that the generated frontend/package.json has the correct content."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
|
assert result.exit_code == 0
|
|
|
|
extension_path = isolated_filesystem / "test-org.test-extension"
|
|
package_json_path = extension_path / "frontend" / "package.json"
|
|
|
|
# Verify the package.json structure and values
|
|
assert_json_content(
|
|
package_json_path,
|
|
{
|
|
"name": "@test-org/test-extension",
|
|
"version": "0.1.0",
|
|
"license": "Apache-2.0",
|
|
},
|
|
)
|
|
|
|
# Verify more complex structures
|
|
content = load_json_file(package_json_path)
|
|
assert "scripts" in content
|
|
assert "build" in content["scripts"]
|
|
assert "peerDependencies" in content
|
|
assert "@apache-superset/core" in content["peerDependencies"]
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_backend_pyproject_toml_is_created(
|
|
cli_runner, isolated_filesystem, cli_input_both
|
|
):
|
|
"""Test that the generated backend/pyproject.toml file is created."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
|
assert result.exit_code == 0
|
|
|
|
extension_path = isolated_filesystem / "test-org.test-extension"
|
|
pyproject_path = extension_path / "backend" / "pyproject.toml"
|
|
|
|
assert_file_exists(pyproject_path, "backend pyproject.toml")
|
|
|
|
# Basic content verification (without parsing TOML for now)
|
|
content = pyproject_path.read_text()
|
|
assert (
|
|
"test_org-test_extension" in content
|
|
) # Package name uses collision-safe naming
|
|
assert "0.1.0" in content
|
|
assert "Apache-2.0" in content
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_command_output_messages(cli_runner, isolated_filesystem, cli_input_both):
|
|
"""Test that init command produces expected output messages."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
|
|
|
assert result.exit_code == 0
|
|
output = result.output
|
|
|
|
# Check for expected success messages
|
|
assert "Created extension.json" in output
|
|
assert "Created .gitignore" in output
|
|
assert "Created frontend folder structure" in output
|
|
assert "Created backend folder structure" in output
|
|
assert (
|
|
"Extension Test Extension (ID: test-org.test-extension) initialized" in output
|
|
)
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_gitignore_content_is_correct(cli_runner, isolated_filesystem, cli_input_both):
|
|
"""Test that the generated .gitignore has the correct content."""
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
|
assert result.exit_code == 0
|
|
|
|
extension_path = isolated_filesystem / "test-org.test-extension"
|
|
gitignore_path = extension_path / ".gitignore"
|
|
|
|
assert_file_exists(gitignore_path, ".gitignore")
|
|
|
|
content = gitignore_path.read_text()
|
|
|
|
# Verify key patterns are present
|
|
assert "node_modules/" in content
|
|
assert "dist/" in content
|
|
assert "*.supx" in content
|
|
assert "__pycache__" in content
|
|
assert ".venv/" in content
|
|
assert ".DS_Store" in content
|
|
assert ".env" in content
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_with_custom_version_and_license(cli_runner, isolated_filesystem):
|
|
"""Test init with custom version and license parameters."""
|
|
cli_input = "My Extension\n\ntest-org\n2.1.0\nMIT\ny\nn\n"
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input)
|
|
|
|
assert result.exit_code == 0
|
|
|
|
extension_path = isolated_filesystem / "test-org.my-extension"
|
|
extension_json_path = extension_path / "extension.json"
|
|
|
|
assert_json_content(
|
|
extension_json_path,
|
|
{
|
|
"publisher": "test-org",
|
|
"name": "my-extension",
|
|
"displayName": "My Extension",
|
|
"version": "2.1.0",
|
|
"license": "MIT",
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.mark.integration
|
|
@pytest.mark.cli
|
|
def test_full_init_workflow_integration(cli_runner, isolated_filesystem):
|
|
"""Integration test for the complete init workflow."""
|
|
# Test the complete flow with realistic user input
|
|
cli_input = "Awesome Charts\n\nawesome-org\n1.0.0\nApache-2.0\ny\ny\n"
|
|
result = cli_runner.invoke(app, ["init"], input=cli_input)
|
|
|
|
# Verify success
|
|
assert result.exit_code == 0
|
|
|
|
# Verify complete directory structure
|
|
extension_path = isolated_filesystem / "awesome-org.awesome-charts"
|
|
expected_structure = create_test_extension_structure(
|
|
isolated_filesystem,
|
|
"awesome-org.awesome-charts",
|
|
include_frontend=True,
|
|
include_backend=True,
|
|
)
|
|
|
|
# Comprehensive structure verification
|
|
assert_directory_structure(extension_path, expected_structure["expected_dirs"])
|
|
assert_file_structure(extension_path, expected_structure["expected_files"])
|
|
|
|
# Verify all generated files have correct content
|
|
extension_json = load_json_file(extension_path / "extension.json")
|
|
assert extension_json["publisher"] == "awesome-org"
|
|
assert extension_json["name"] == "awesome-charts"
|
|
assert extension_json["displayName"] == "Awesome Charts"
|
|
assert extension_json["version"] == "1.0.0"
|
|
assert extension_json["license"] == "Apache-2.0"
|
|
|
|
package_json = load_json_file(extension_path / "frontend" / "package.json")
|
|
assert package_json["name"] == "@awesome-org/awesome-charts"
|
|
|
|
pyproject_content = (extension_path / "backend" / "pyproject.toml").read_text()
|
|
assert (
|
|
"awesome_org-awesome_charts" in pyproject_content
|
|
) # Package name uses collision-safe naming
|
|
|
|
|
|
# Non-interactive mode tests
|
|
@pytest.mark.cli
|
|
def test_init_non_interactive_with_all_options(cli_runner, isolated_filesystem):
|
|
"""Test that init works in non-interactive mode with all CLI options."""
|
|
result = cli_runner.invoke(
|
|
app,
|
|
[
|
|
"init",
|
|
"--publisher",
|
|
"my-org",
|
|
"--name",
|
|
"my-ext",
|
|
"--display-name",
|
|
"My Extension",
|
|
"--version",
|
|
"1.0.0",
|
|
"--license",
|
|
"MIT",
|
|
"--frontend",
|
|
"--backend",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
|
assert "🎉 Extension My Extension (ID: my-org.my-ext) initialized" in result.output
|
|
|
|
extension_path = isolated_filesystem / "my-org.my-ext"
|
|
assert_directory_exists(extension_path)
|
|
assert_directory_exists(extension_path / "frontend")
|
|
assert_directory_exists(extension_path / "backend")
|
|
|
|
extension_json = load_json_file(extension_path / "extension.json")
|
|
assert extension_json["publisher"] == "my-org"
|
|
assert extension_json["name"] == "my-ext"
|
|
assert extension_json["displayName"] == "My Extension"
|
|
assert extension_json["version"] == "1.0.0"
|
|
assert extension_json["license"] == "MIT"
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_frontend_only_with_cli_options(cli_runner, isolated_filesystem):
|
|
"""Test init with frontend only using CLI options."""
|
|
result = cli_runner.invoke(
|
|
app,
|
|
[
|
|
"init",
|
|
"--publisher",
|
|
"frontend-org",
|
|
"--name",
|
|
"frontend-ext",
|
|
"--display-name",
|
|
"Frontend Extension",
|
|
"--version",
|
|
"1.0.0",
|
|
"--license",
|
|
"MIT",
|
|
"--frontend",
|
|
"--no-backend",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
|
|
|
extension_path = isolated_filesystem / "frontend-org.frontend-ext"
|
|
assert_directory_exists(extension_path / "frontend")
|
|
assert not (extension_path / "backend").exists()
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_backend_only_with_cli_options(cli_runner, isolated_filesystem):
|
|
"""Test init with backend only using CLI options."""
|
|
result = cli_runner.invoke(
|
|
app,
|
|
[
|
|
"init",
|
|
"--publisher",
|
|
"backend-org",
|
|
"--name",
|
|
"backend-ext",
|
|
"--display-name",
|
|
"Backend Extension",
|
|
"--version",
|
|
"1.0.0",
|
|
"--license",
|
|
"MIT",
|
|
"--no-frontend",
|
|
"--backend",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
|
|
|
extension_path = isolated_filesystem / "backend-org.backend-ext"
|
|
assert not (extension_path / "frontend").exists()
|
|
assert_directory_exists(extension_path / "backend")
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_prompts_for_missing_options(cli_runner, isolated_filesystem):
|
|
"""Test that init prompts for options not provided via CLI and uses defaults."""
|
|
# Provide publisher, name, and display-name via CLI, but version/license will be prompted (accept defaults)
|
|
result = cli_runner.invoke(
|
|
app,
|
|
[
|
|
"init",
|
|
"--publisher",
|
|
"default-org",
|
|
"--name",
|
|
"default-ext",
|
|
"--display-name",
|
|
"Default Extension",
|
|
"--frontend",
|
|
"--backend",
|
|
],
|
|
input="\n\n", # Accept defaults for version and license prompts
|
|
)
|
|
|
|
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
|
|
|
extension_path = isolated_filesystem / "default-org.default-ext"
|
|
extension_json = load_json_file(extension_path / "extension.json")
|
|
assert extension_json["version"] == "0.1.0"
|
|
assert extension_json["license"] == "Apache-2.0"
|
|
|
|
|
|
@pytest.mark.cli
|
|
def test_init_non_interactive_validates_technical_name(cli_runner, isolated_filesystem):
|
|
"""Test that non-interactive mode validates technical name."""
|
|
result = cli_runner.invoke(
|
|
app,
|
|
[
|
|
"init",
|
|
"--publisher",
|
|
"test-org",
|
|
"--name",
|
|
"invalid_name",
|
|
"--display-name",
|
|
"Invalid Extension",
|
|
"--frontend",
|
|
"--backend",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 1
|
|
assert "must start with a letter" in result.output.lower()
|