Files
superset2/superset-extensions-cli/tests/test_name_transformations.py

503 lines
16 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.
import pytest
from superset_extensions_cli.exceptions import ExtensionNameError
from superset_extensions_cli.utils import (
generate_extension_names,
get_module_federation_name,
kebab_to_camel_case,
kebab_to_snake_case,
name_to_kebab_case,
suggest_technical_name,
validate_display_name,
validate_npm_package_name,
validate_publisher,
validate_python_package_name,
validate_technical_name,
)
# Name transformation tests
@pytest.mark.parametrize(
("display_name", "expected"),
[
("Hello World", "hello-world"),
("Data Explorer", "data-explorer"),
("My Extension", "my-extension"),
("hello-world", "hello-world"), # Already normalized
("Hello@World!", "helloworld"), # Special chars removed
(
"Data_Explorer",
"data-explorer",
), # Underscores become spaces then hyphens
("My Extension", "my-extension"), # Multiple spaces normalized
(" Hello World ", "hello-world"), # Trimmed
("API v2 Client", "api-v2-client"), # Numbers preserved
("Simple", "simple"), # Single word
],
)
def test_name_to_kebab_case(display_name, expected):
"""Test direct kebab case conversion from display names."""
assert name_to_kebab_case(display_name) == expected
@pytest.mark.parametrize(
("kebab_name", "expected"),
[
("hello-world", "helloWorld"),
("data-explorer", "dataExplorer"),
("my-extension", "myExtension"),
("api-v2-client", "apiV2Client"),
("simple", "simple"), # Single word
("chart-tool", "chartTool"),
("dashboard-helper", "dashboardHelper"),
],
)
def test_kebab_to_camel_case(kebab_name, expected):
"""Test kebab-case to camelCase conversion."""
assert kebab_to_camel_case(kebab_name) == expected
@pytest.mark.parametrize(
("kebab_name", "expected"),
[
("hello-world", "hello_world"),
("data-explorer", "data_explorer"),
("my-extension", "my_extension"),
("api-v2-client", "api_v2_client"),
("simple", "simple"), # Single word
("chart-tool", "chart_tool"),
("dashboard-helper", "dashboard_helper"),
],
)
def test_kebab_to_snake_case(kebab_name, expected):
"""Test kebab-case to snake_case conversion."""
assert kebab_to_snake_case(kebab_name) == expected
# Display name validation tests
@pytest.mark.parametrize(
("valid_display", "expected_normalized"),
[
("Hello World", "Hello World"),
("Data Explorer", "Data Explorer"),
("My Extension", "My Extension"),
("Simple", "Simple"),
(" Extra Spaces ", "Extra Spaces"), # Gets normalized
("Dashboard Widgets", "Dashboard Widgets"),
("Chart Builder Pro", "Chart Builder Pro"),
("API Client v2.0", "API Client v2.0"),
("Tool_123", "Tool_123"), # Underscores allowed
("My-Extension", "My-Extension"), # Hyphens allowed
],
)
def test_validate_display_name_valid(valid_display, expected_normalized):
"""Test valid display names return correctly normalized output."""
result = validate_display_name(valid_display)
assert result == expected_normalized
@pytest.mark.parametrize(
("invalid_display", "error_match"),
[
("", "cannot be empty"),
(" ", "cannot be empty"),
("@#$%", "must start with a letter"),
("123 Tool", "must start with a letter"),
("-My Extension", "must start with a letter"),
],
)
def test_validate_display_name_invalid(invalid_display, error_match):
"""Test invalid display names."""
with pytest.raises(ExtensionNameError, match=error_match):
validate_display_name(invalid_display)
# Python package name validation tests
@pytest.mark.parametrize(
("valid_package",),
[
("hello_world",),
("data_explorer",),
("myext",),
("test123",),
("package_with_many_parts",),
],
)
def test_validate_python_package_name_valid(valid_package):
"""Test valid Python package names."""
# Should not raise exceptions
validate_python_package_name(valid_package)
@pytest.mark.parametrize(
("keyword",),
[
("class",),
("import",),
("def",),
("return",),
("if",),
("else",),
("for",),
("while",),
("try",),
("except",),
("finally",),
("with",),
("as",),
("lambda",),
("yield",),
("False",),
("None",),
("True",),
],
)
def test_validate_python_package_name_keywords(keyword):
"""Test that Python reserved keywords are rejected."""
with pytest.raises(
ExtensionNameError, match="Package name cannot start with Python keyword"
):
validate_python_package_name(keyword)
@pytest.mark.parametrize(
("invalid_package",),
[
("hello-world",), # Hyphens not allowed in Python identifiers
],
)
def test_validate_python_package_name_invalid(invalid_package):
"""Test invalid Python package names."""
with pytest.raises(ExtensionNameError, match="not a valid Python package"):
validate_python_package_name(invalid_package)
# NPM package validation tests
@pytest.mark.parametrize(
("valid_npm",),
[
("hello-world",),
("data-explorer",),
("myext",),
("package-with-many-parts",),
],
)
def test_validate_npm_package_name_valid(valid_npm):
"""Test valid npm package names."""
# Should not raise exceptions
validate_npm_package_name(valid_npm)
@pytest.mark.parametrize(
("reserved_name",),
[
("node_modules",),
("npm",),
("yarn",),
("package.json",),
("localhost",),
("favicon.ico",),
],
)
def test_validate_npm_package_name_reserved(reserved_name):
"""Test that npm reserved names are rejected."""
with pytest.raises(ExtensionNameError, match="reserved npm package name"):
validate_npm_package_name(reserved_name)
# Publisher validation tests
@pytest.mark.parametrize(
("valid_publisher",),
[
("my-org",),
("acme",),
("apache-superset",),
("test123",),
("a",), # Single character
("publisher-with-many-parts",),
],
)
def test_validate_publisher_valid(valid_publisher):
"""Test valid publisher namespaces."""
# Should not raise exceptions
validate_publisher(valid_publisher)
@pytest.mark.parametrize(
("invalid_publisher", "error_match"),
[
("", "cannot be empty"),
("My-Org", "must start with a letter and contain only lowercase letters"),
("-publisher", "must start with a letter and contain only lowercase letters"),
("publisher-", "must start with a letter and contain only lowercase letters"),
("pub--lisher", "must start with a letter and contain only lowercase letters"),
],
)
def test_validate_publisher_invalid(invalid_publisher, error_match):
"""Test invalid publisher namespaces."""
with pytest.raises(ExtensionNameError, match=error_match):
validate_publisher(invalid_publisher)
# Technical name validation tests
@pytest.mark.parametrize(
("valid_name",),
[
("dashboard-widgets",),
("chart-builder",),
("simple",),
("api-client-v2",),
("tool123",),
],
)
def test_validate_technical_name_valid(valid_name):
"""Test valid technical names."""
# Should not raise exceptions
validate_technical_name(valid_name)
@pytest.mark.parametrize(
("invalid_name", "error_match"),
[
("", "cannot be empty"),
(
"Dashboard-Widgets",
"must start with a letter and contain only lowercase letters",
),
("-name", "must start with a letter and contain only lowercase letters"),
("name-", "must start with a letter and contain only lowercase letters"),
("na--me", "must start with a letter and contain only lowercase letters"),
],
)
def test_validate_technical_name_invalid(invalid_name, error_match):
"""Test invalid technical names."""
with pytest.raises(ExtensionNameError, match=error_match):
validate_technical_name(invalid_name)
# Name suggestion tests
@pytest.mark.parametrize(
("display_name", "expected_technical"),
[
("Dashboard Widgets", "dashboard-widgets"),
("Chart Builder Pro!", "chart-builder-pro"),
("My@Tool#123", "mytool123"),
(" Spaced Out ", "spaced-out"),
("API v2 Client", "api-v2-client"),
],
)
def test_suggest_technical_name(display_name, expected_technical):
"""Test technical name suggestion from display names."""
result = suggest_technical_name(display_name)
assert result == expected_technical
@pytest.mark.parametrize(
("publisher", "name", "expected_mf"),
[
("my-org", "dashboard-widgets", "myOrg_dashboardWidgets"),
("acme", "chart-builder", "acme_chartBuilder"),
("test-company", "simple", "testCompany_simple"),
],
)
def test_get_module_federation_name(publisher, name, expected_mf):
"""Test Module Federation name generation."""
result = get_module_federation_name(publisher, name)
assert result == expected_mf
# Complete name generation tests
@pytest.mark.parametrize(
("display_name", "expected_kebab", "expected_snake", "expected_camel"),
[
("Hello World", "hello-world", "hello_world", "helloWorld"),
("Data Explorer", "data-explorer", "data_explorer", "dataExplorer"),
("My Extension v2", "my-extension-v2", "my_extension_v2", "myExtensionV2"),
("Chart Tool", "chart-tool", "chart_tool", "chartTool"),
("Simple", "simple", "simple", "simple"),
("API v2 Client", "api-v2-client", "api_v2_client", "apiV2Client"),
(
"Dashboard Helper",
"dashboard-helper",
"dashboard_helper",
"dashboardHelper",
),
],
)
def test_generate_extension_names_complete_flow(
display_name, expected_kebab, expected_snake, expected_camel
):
"""Test complete name generation flow with publisher concept."""
publisher = "test-org"
names = generate_extension_names(display_name, publisher, expected_kebab)
# Test all transformations with publisher concept
assert names["display_name"] == display_name
assert names["publisher"] == publisher
assert names["name"] == expected_kebab # Technical name
assert names["id"] == f"{publisher}.{expected_kebab}" # Composite ID
assert names["npm_name"] == f"@{publisher}/{expected_kebab}" # NPM scoped
assert (
names["mf_name"] == f"testOrg_{expected_camel}"
) # Module Federation with publisher prefix
assert (
names["backend_package"] == f"{publisher.replace('-', '_')}-{expected_snake}"
) # Collision-safe
assert (
names["backend_path"]
== f"superset_extensions.{publisher.replace('-', '_')}.{expected_snake}"
)
assert (
names["backend_entry"]
== f"superset_extensions.{publisher.replace('-', '_')}.{expected_snake}.entrypoint"
)
@pytest.mark.parametrize(
("invalid_display",),
[
("Class Helper",), # Would create 'class_helper' - reserved keyword
("Import Tool",), # Would create 'import_tool' - reserved keyword
("@#$%",), # All special chars - becomes empty
("123 Tool",), # Starts with number after kebab conversion
],
)
def test_generate_extension_names_invalid(invalid_display):
"""Test invalid name generation scenarios."""
with pytest.raises(ExtensionNameError):
generate_extension_names(invalid_display, "test-org")
def test_generate_extension_names_unicode():
"""Test handling of unicode characters."""
# Use a simpler approach - the display name validation now requires starting with letter
names = generate_extension_names("Cafe Extension", "test-org", "cafe-extension")
assert names["id"] == "test-org.cafe-extension"
assert names["display_name"] == "Cafe Extension" # Original preserved
def test_generate_extension_names_special_chars():
"""Test name generation with special characters."""
# Use manual technical name since display validation is stricter
names = generate_extension_names("My Extension", "test-org", "my-extension")
assert names["display_name"] == "My Extension"
assert names["id"] == "test-org.my-extension"
assert names["backend_package"] == "test_org-my_extension"
def test_generate_extension_names_case_preservation():
"""Test that display name case is preserved."""
names = generate_extension_names("CamelCase Extension", "test-org")
assert names["display_name"] == "CamelCase Extension"
assert names["id"] == "test-org.camelcase-extension"
# Edge case tests
@pytest.mark.parametrize(
("edge_case",),
[
("",), # Empty string
(" ",), # Only spaces
("---",), # Only hyphens
("___",), # Only underscores
],
)
def test_empty_or_invalid_inputs(edge_case):
"""Test inputs that become empty or invalid after processing."""
with pytest.raises(ExtensionNameError):
generate_extension_names(edge_case, "test-org")
def test_minimal_valid_input():
"""Test minimal valid input."""
names = generate_extension_names("A Extension", "test-org")
assert names["id"] == "test-org.a-extension"
assert names["backend_package"] == "test_org-a_extension"
def test_numbers_handling():
"""Test handling of numbers in names."""
names = generate_extension_names("Tool 123 v2", "test-org")
assert names["id"] == "test-org.tool-123-v2"
assert names["backend_package"] == "test_org-tool_123_v2"
def test_manual_technical_name_override():
"""Test using manual technical name instead of auto-generated."""
display_name = "My Awesome Chart Builder Pro"
publisher = "acme"
technical_name = "chart-builder" # Much shorter than display name
# Create names using manual technical name
names = generate_extension_names(display_name, publisher, technical_name)
# Verify technical names come from provided technical name, not display name
assert (
names["display_name"] == "My Awesome Chart Builder Pro"
) # Display name preserved
assert names["publisher"] == "acme"
assert names["name"] == "chart-builder" # Technical name used
assert names["id"] == "acme.chart-builder" # Composite ID
assert names["mf_name"] == "acme_chartBuilder" # Module Federation format
assert names["backend_package"] == "acme-chart_builder" # Collision-safe
assert names["backend_path"] == "superset_extensions.acme.chart_builder"
assert names["backend_entry"] == "superset_extensions.acme.chart_builder.entrypoint"
def test_generate_names_uses_suggested_technical_names():
"""Test that generate_extension_names can auto-suggest technical names."""
display_name = "Hello World"
publisher = "test-org"
# Generated names should use suggested technical name generation
names = generate_extension_names(display_name, publisher)
# Verify the technical name was suggested from display name
assert names["name"] == "hello-world"
assert names["id"] == "test-org.hello-world"
# Verify other names were generated from the technical name and publisher
assert names["mf_name"] == get_module_federation_name(
"test-org", "hello-world"
) # "testOrg_helloWorld"
assert names["backend_package"] == "test_org-hello_world"
# Module Federation name should use underscore format with camelCase
assert names["mf_name"] == "testOrg_helloWorld"