From 67e0a92047fce150dd341be3b18cc9da48da03fb Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Thu, 26 Mar 2026 13:43:04 +0100 Subject: [PATCH 01/11] [DBA-141] Add field validations on datasource adding Add frontend validation for datasource names in both the Streamlit UI and CLI workflow. Names with spaces, special characters, and other invalid patterns are rejected with clear error messages before submission instead of failing on the backend. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/images/dba-141-validation-screenshot.png | Bin 0 -> 70564 bytes .../features/datasource/validation.py | 53 +++++++++++++ .../features/ui/components/datasource_form.py | 20 +++++ .../ui/components/datasource_manager.py | 28 +++++-- src/databao_cli/workflows/datasource/add.py | 9 ++- tests/test_datasource_validation.py | 75 ++++++++++++++++++ 6 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 docs/images/dba-141-validation-screenshot.png create mode 100644 src/databao_cli/features/datasource/validation.py create mode 100644 tests/test_datasource_validation.py diff --git a/docs/images/dba-141-validation-screenshot.png b/docs/images/dba-141-validation-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1b3a81a8c795544fbb2c4fd33aeccc655d4f9a76 GIT binary patch literal 70564 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 literal 0 HcmV?d00001 diff --git a/src/databao_cli/features/datasource/validation.py b/src/databao_cli/features/datasource/validation.py new file mode 100644 index 00000000..b04a9218 --- /dev/null +++ b/src/databao_cli/features/datasource/validation.py @@ -0,0 +1,53 @@ +"""Validation rules for datasource names. + +These rules are shared between the CLI workflow and the Streamlit UI so +that users get the same feedback regardless of how they create a +datasource. +""" + +import re + +# Allowed segment characters: alphanumeric, hyphens, underscores, dots. +# Must start and end with an alphanumeric character. +_VALID_SEGMENT_RE = re.compile(r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$") + +MAX_DATASOURCE_NAME_LENGTH = 255 + + +def validate_datasource_name(name: str) -> str | None: + """Return an error message if *name* is invalid, or ``None`` if it is OK. + + Datasource names may contain forward-slash separators to organise + datasources into folders (e.g. ``resources/my_db``). Each segment + between slashes is validated individually. + + Rules per segment: + * Must not be empty or whitespace-only. + * Must only contain letters, digits, hyphens, underscores, and dots. + * Must start and end with a letter or digit. + + Overall rules: + * Total length must not exceed 255 characters. + * Must not contain spaces. + """ + if not name or not name.strip(): + return "Datasource name must not be empty." + + if len(name) > MAX_DATASOURCE_NAME_LENGTH: + return f"Datasource name must be at most {MAX_DATASOURCE_NAME_LENGTH} characters." + + if " " in name: + return "Datasource name must not contain spaces." + + segments = name.split("/") + for segment in segments: + if not segment: + return "Datasource name must not contain empty path segments (double slashes)." + if not _VALID_SEGMENT_RE.match(segment): + return ( + "Datasource name may only contain letters, digits, hyphens, " + "underscores, and dots, and each segment must start and end " + "with a letter or digit." + ) + + return None diff --git a/src/databao_cli/features/ui/components/datasource_form.py b/src/databao_cli/features/ui/components/datasource_form.py index a6662cc4..f2d04d52 100644 --- a/src/databao_cli/features/ui/components/datasource_form.py +++ b/src/databao_cli/features/ui/components/datasource_form.py @@ -17,6 +17,26 @@ SKIP_TOP_LEVEL_KEYS = {"type", "name"} +def get_missing_required_fields( + config_fields: list[ConfigPropertyDefinition], + values: dict[str, Any], +) -> list[str]: + """Return labels of required fields that are empty in *values*. + + Only checks top-level single (leaf) properties that are marked as + required. Skips the ``type`` and ``name`` keys (handled separately). + """ + missing: list[str] = [] + for prop in config_fields: + if prop.property_key in SKIP_TOP_LEVEL_KEYS: + continue + if isinstance(prop, ConfigSinglePropertyDefinition) and not prop.nested_properties and prop.required: + val = values.get(prop.property_key) + if val is None or not str(val).strip(): + missing.append(prop.property_key) + return missing + + def render_datasource_config_form( config_fields: list[ConfigPropertyDefinition], existing_values: dict[str, Any] | None = None, diff --git a/src/databao_cli/features/ui/components/datasource_manager.py b/src/databao_cli/features/ui/components/datasource_manager.py index 4da0f250..cc0fd26a 100644 --- a/src/databao_cli/features/ui/components/datasource_manager.py +++ b/src/databao_cli/features/ui/components/datasource_manager.py @@ -10,9 +10,14 @@ import streamlit as st from databao_context_engine import ConfiguredDatasource, DatasourceConnectionStatus +from databao_context_engine.pluginlib.config import ConfigPropertyDefinition +from databao_cli.features.datasource.validation import validate_datasource_name from databao_cli.features.ui.app import invalidate_agent -from databao_cli.features.ui.components.datasource_form import render_datasource_config_form +from databao_cli.features.ui.components.datasource_form import ( + get_missing_required_fields, + render_datasource_config_form, +) from databao_cli.features.ui.services.dce_operations import ( add_datasource, get_available_datasource_types, @@ -89,6 +94,7 @@ def _render_add_datasource_section(project_dir: Path) -> None: ) config_values: dict[str, Any] = {} + config_fields: list[ConfigPropertyDefinition] = [] if selected_type: try: config_fields = get_datasource_config_fields(selected_type) @@ -105,13 +111,17 @@ def _render_add_datasource_section(project_dir: Path) -> None: with col_add: if st.button("Add datasource", key="add_ds_btn", type="primary", use_container_width=True): - if not ds_name or not ds_name.strip(): - st.error("Please provide a datasource name.") + stripped_name = (ds_name or "").strip() + name_error = validate_datasource_name(stripped_name) + if name_error: + st.error(name_error) elif not selected_type: st.error("Please select a datasource type.") + elif config_fields and (missing := get_missing_required_fields(config_fields, config_values)): + st.error(f"Required fields are empty: {', '.join(missing)}") else: try: - add_datasource(project_dir, selected_type, ds_name.strip(), config_values) + add_datasource(project_dir, selected_type, stripped_name, config_values) _clear_new_datasource_form() st.rerun() except Exception as e: @@ -120,11 +130,15 @@ def _render_add_datasource_section(project_dir: Path) -> None: with col_verify_new: if st.button("Verify connection", key="verify_new_ds_btn", use_container_width=True): - if not ds_name or not ds_name.strip() or not selected_type: - st.error("Please provide a datasource name and type first.") + stripped_name = (ds_name or "").strip() + name_error = validate_datasource_name(stripped_name) + if name_error: + st.error(name_error) + elif not selected_type: + st.error("Please select a datasource type.") else: try: - result = verify_datasource_config(selected_type, ds_name.strip(), config_values) + result = verify_datasource_config(selected_type, stripped_name, config_values) if result.connection_status == DatasourceConnectionStatus.VALID: st.success("Connection valid.") elif result.connection_status == DatasourceConnectionStatus.UNKNOWN: diff --git a/src/databao_cli/workflows/datasource/add.py b/src/databao_cli/workflows/datasource/add.py index 6517998c..835bb78d 100644 --- a/src/databao_cli/workflows/datasource/add.py +++ b/src/databao_cli/workflows/datasource/add.py @@ -9,6 +9,7 @@ DatasourceType, ) +from databao_cli.features.datasource.validation import validate_datasource_name from databao_cli.shared.context_engine_cli import ClickUserInputCallback from databao_cli.shared.project.layout import ProjectLayout from databao_cli.workflows.datasource.check import print_connection_check_results @@ -24,7 +25,13 @@ def add_workflow(project_layout: ProjectLayout, domain: str) -> None: click.echo(f"We will guide you to add a new datasource into {domain} domain, at {domain_dir.resolve()}") datasource_type = _ask_for_datasource_type(plugin_loader.get_all_supported_datasource_types(exclude_file_plugins=True)) - datasource_name = click.prompt("Datasource name?", type=str) + + while True: + datasource_name = click.prompt("Datasource name?", type=str).strip() + name_error = validate_datasource_name(datasource_name) + if name_error is None: + break + click.secho(name_error, fg="red", err=True) overwrite_existing = False existing_id = datasource_config_exists(project_layout, domain, datasource_name) diff --git a/tests/test_datasource_validation.py b/tests/test_datasource_validation.py new file mode 100644 index 00000000..5f4c7cda --- /dev/null +++ b/tests/test_datasource_validation.py @@ -0,0 +1,75 @@ +import pytest + +from databao_cli.features.datasource.validation import ( + MAX_DATASOURCE_NAME_LENGTH, + validate_datasource_name, +) + + +class TestValidateDatasourceName: + """Tests for validate_datasource_name().""" + + @pytest.mark.parametrize( + "name", + [ + "my-datasource", + "my_datasource", + "my.datasource", + "ds1", + "a", + "A", + "Snowflake-Prod", + "test_db.v2", + "ab", + "resources/my_db", + "folder/sub/name", + ], + ) + def test_valid_names(self, name: str) -> None: + assert validate_datasource_name(name) is None + + def test_empty_name(self) -> None: + assert validate_datasource_name("") is not None + + def test_whitespace_only(self) -> None: + assert validate_datasource_name(" ") is not None + + def test_name_with_spaces(self) -> None: + error = validate_datasource_name("my datasource") + assert error is not None + assert "spaces" in error.lower() + + def test_name_with_leading_space(self) -> None: + error = validate_datasource_name(" leading") + assert error is not None + + def test_name_with_trailing_space(self) -> None: + error = validate_datasource_name("trailing ") + assert error is not None + + @pytest.mark.parametrize("name", [".hidden", "-start", "end-", "end."]) + def test_name_starting_or_ending_with_special_char(self, name: str) -> None: + assert validate_datasource_name(name) is not None + + @pytest.mark.parametrize("char", ["@", "#", "$", "%", "!", "?", "\\", ":", "*"]) + def test_forbidden_characters(self, char: str) -> None: + assert validate_datasource_name(f"ds{char}name") is not None + + def test_double_slash_rejected(self) -> None: + assert validate_datasource_name("a//b") is not None + + def test_leading_slash_rejected(self) -> None: + assert validate_datasource_name("/a") is not None + + def test_trailing_slash_rejected(self) -> None: + assert validate_datasource_name("a/") is not None + + def test_name_too_long(self) -> None: + long_name = "a" * (MAX_DATASOURCE_NAME_LENGTH + 1) + error = validate_datasource_name(long_name) + assert error is not None + assert "255" in error + + def test_name_at_max_length(self) -> None: + name = "a" * MAX_DATASOURCE_NAME_LENGTH + assert validate_datasource_name(name) is None From d6e65f1121147eb55b4efd7a872a42d6646261c4 Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Thu, 26 Mar 2026 14:01:37 +0100 Subject: [PATCH 02/11] [DBA-141] Extract shared validation helper in datasource manager Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ui/components/datasource_manager.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/databao_cli/features/ui/components/datasource_manager.py b/src/databao_cli/features/ui/components/datasource_manager.py index cc0fd26a..deb21823 100644 --- a/src/databao_cli/features/ui/components/datasource_manager.py +++ b/src/databao_cli/features/ui/components/datasource_manager.py @@ -61,6 +61,22 @@ def render_datasource_manager(project_dir: Path, *, read_only: bool = False) -> _render_existing_datasource(project_dir, ds, idx, read_only=read_only) +def _validate_new_datasource_inputs(ds_name: str | None, selected_type: str | None) -> str | None: + """Validate the datasource name and type, showing errors via ``st.error``. + + Returns the stripped name on success, or ``None`` if validation failed. + """ + stripped_name = (ds_name or "").strip() + name_error = validate_datasource_name(stripped_name) + if name_error: + st.error(name_error) + return None + if not selected_type: + st.error("Please select a datasource type.") + return None + return stripped_name + + def _get_form_version() -> int: """Get the current form version counter used to reset widget keys.""" if "_new_ds_form_version" not in st.session_state: @@ -111,17 +127,14 @@ def _render_add_datasource_section(project_dir: Path) -> None: with col_add: if st.button("Add datasource", key="add_ds_btn", type="primary", use_container_width=True): - stripped_name = (ds_name or "").strip() - name_error = validate_datasource_name(stripped_name) - if name_error: - st.error(name_error) - elif not selected_type: - st.error("Please select a datasource type.") + validated = _validate_new_datasource_inputs(ds_name, selected_type) + if validated is None: + pass # errors already shown elif config_fields and (missing := get_missing_required_fields(config_fields, config_values)): st.error(f"Required fields are empty: {', '.join(missing)}") else: try: - add_datasource(project_dir, selected_type, stripped_name, config_values) + add_datasource(project_dir, selected_type, validated, config_values) _clear_new_datasource_form() st.rerun() except Exception as e: @@ -130,15 +143,10 @@ def _render_add_datasource_section(project_dir: Path) -> None: with col_verify_new: if st.button("Verify connection", key="verify_new_ds_btn", use_container_width=True): - stripped_name = (ds_name or "").strip() - name_error = validate_datasource_name(stripped_name) - if name_error: - st.error(name_error) - elif not selected_type: - st.error("Please select a datasource type.") - else: + validated = _validate_new_datasource_inputs(ds_name, selected_type) + if validated is not None: try: - result = verify_datasource_config(selected_type, stripped_name, config_values) + result = verify_datasource_config(selected_type, validated, config_values) if result.connection_status == DatasourceConnectionStatus.VALID: st.success("Connection valid.") elif result.connection_status == DatasourceConnectionStatus.UNKNOWN: From f3ca6da9d3a31addf47738de62d28edb4b3fa974 Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Thu, 26 Mar 2026 14:03:34 +0100 Subject: [PATCH 03/11] [DBA-141] Remove screenshot from repo Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/images/dba-141-validation-screenshot.png | Bin 70564 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/images/dba-141-validation-screenshot.png diff --git a/docs/images/dba-141-validation-screenshot.png b/docs/images/dba-141-validation-screenshot.png deleted file mode 100644 index 1b3a81a8c795544fbb2c4fd33aeccc655d4f9a76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70564 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 From 3f2bcdcf1fb92141acc04dd5581a172035b78f67 Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Thu, 26 Mar 2026 14:23:56 +0100 Subject: [PATCH 04/11] [DBA-141] Align name regex with agent, add port and hostname validation Use the agent's source name pattern (^[A-Za-z][A-Za-z0-9_]*$) for the last segment so names work unquoted in SQL. Add validate_port() and validate_hostname() validators, applied automatically to config fields based on their key name and type. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/datasource/validation.py | 84 +++++++++++++---- .../features/ui/components/datasource_form.py | 71 ++++++++++++-- .../ui/components/datasource_manager.py | 7 +- tests/test_datasource_validation.py | 92 +++++++++++++++---- 4 files changed, 205 insertions(+), 49 deletions(-) diff --git a/src/databao_cli/features/datasource/validation.py b/src/databao_cli/features/datasource/validation.py index b04a9218..087bf940 100644 --- a/src/databao_cli/features/datasource/validation.py +++ b/src/databao_cli/features/datasource/validation.py @@ -1,4 +1,4 @@ -"""Validation rules for datasource names. +"""Validation rules for datasource fields. These rules are shared between the CLI workflow and the Streamlit UI so that users get the same feedback regardless of how they create a @@ -7,28 +7,31 @@ import re -# Allowed segment characters: alphanumeric, hyphens, underscores, dots. -# Must start and end with an alphanumeric character. -_VALID_SEGMENT_RE = re.compile(r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$") +# The agent requires source names to match this pattern so they can be +# used unquoted in SQL queries. See databao-agent domain.py. +_SOURCE_NAME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]*$") + +# Folder segments in the datasource path are more permissive — they only +# need to be valid filesystem names. +_FOLDER_SEGMENT_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$") MAX_DATASOURCE_NAME_LENGTH = 255 +MIN_PORT = 1 +MAX_PORT = 65535 + +# Hostname: RFC 952 / RFC 1123 — labels separated by dots. +_HOSTNAME_RE = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(? str | None: """Return an error message if *name* is invalid, or ``None`` if it is OK. Datasource names may contain forward-slash separators to organise - datasources into folders (e.g. ``resources/my_db``). Each segment - between slashes is validated individually. - - Rules per segment: - * Must not be empty or whitespace-only. - * Must only contain letters, digits, hyphens, underscores, and dots. - * Must start and end with a letter or digit. - - Overall rules: - * Total length must not exceed 255 characters. - * Must not contain spaces. + datasources into folders (e.g. ``resources/my_db``). Folder segments + are validated as filesystem-safe names. The final segment (the actual + source name) must match the agent's ``^[A-Za-z][A-Za-z0-9_]*$`` + pattern so it can be used unquoted in SQL queries. """ if not name or not name.strip(): return "Datasource name must not be empty." @@ -43,11 +46,54 @@ def validate_datasource_name(name: str) -> str | None: for segment in segments: if not segment: return "Datasource name must not contain empty path segments (double slashes)." - if not _VALID_SEGMENT_RE.match(segment): + + # Validate folder segments (all but the last). + for segment in segments[:-1]: + if not _FOLDER_SEGMENT_RE.match(segment): return ( - "Datasource name may only contain letters, digits, hyphens, " - "underscores, and dots, and each segment must start and end " - "with a letter or digit." + f"Folder segment '{segment}' may only contain letters, digits, " + "hyphens, underscores, and dots, and must start and end with " + "a letter or digit." ) + # Validate the source name (last segment) against the agent's pattern. + source_name = segments[-1] + if not _SOURCE_NAME_RE.match(source_name): + return "Datasource name must start with a letter and contain only letters, digits, and underscores." + return None + + +def validate_port(value: str) -> str | None: + """Return an error message if *value* is not a valid port number.""" + try: + port = int(value) + except ValueError: + return "Port must be a number." + if port < MIN_PORT or port > MAX_PORT: + return f"Port must be between {MIN_PORT} and {MAX_PORT}." + return None + + +def validate_hostname(value: str) -> str | None: + """Return an error message if *value* is not a valid hostname or IP.""" + value = value.strip() + if not value: + return "Hostname must not be empty." + # Allow localhost and IP addresses as-is. + if value == "localhost" or _is_ip_address(value): + return None + if len(value) > 253: + return "Hostname must not exceed 253 characters." + if not _HOSTNAME_RE.match(value): + return "Hostname contains invalid characters." + return None + + +def _is_ip_address(value: str) -> bool: + """Return True if *value* looks like an IPv4 or bracketed IPv6 address.""" + parts = value.split(".") + if len(parts) == 4: + return all(p.isdigit() and 0 <= int(p) <= 255 for p in parts) + # Bracketed IPv6 or raw IPv6. + return ":" in value diff --git a/src/databao_cli/features/ui/components/datasource_form.py b/src/databao_cli/features/ui/components/datasource_form.py index f2d04d52..09903366 100644 --- a/src/databao_cli/features/ui/components/datasource_form.py +++ b/src/databao_cli/features/ui/components/datasource_form.py @@ -14,27 +14,78 @@ ConfigUnionPropertyDefinition, ) +from databao_cli.features.datasource.validation import validate_hostname, validate_port + SKIP_TOP_LEVEL_KEYS = {"type", "name"} +# Field keys that represent a network host. +_HOST_KEYS = {"host", "hostname"} + +# Field keys that represent a network port. +_PORT_KEYS = {"port"} + -def get_missing_required_fields( +def validate_config_fields( config_fields: list[ConfigPropertyDefinition], values: dict[str, Any], ) -> list[str]: - """Return labels of required fields that are empty in *values*. + """Return human-readable error strings for invalid or missing fields. - Only checks top-level single (leaf) properties that are marked as - required. Skips the ``type`` and ``name`` keys (handled separately). + Checks are applied recursively but only to leaf + ``ConfigSinglePropertyDefinition`` nodes. Checks performed: + + * Required fields must not be empty. + * ``int``-typed fields must be valid integers. + * Fields named ``port`` must be in the 1-65535 range. + * Fields named ``host`` / ``hostname`` must be valid hostnames or IPs. """ - missing: list[str] = [] + return _validate_fields_recursive(config_fields, values) + + +def _validate_fields_recursive( + config_fields: list[ConfigPropertyDefinition], + values: dict[str, Any], + path_prefix: str = "", +) -> list[str]: + errors: list[str] = [] for prop in config_fields: if prop.property_key in SKIP_TOP_LEVEL_KEYS: continue - if isinstance(prop, ConfigSinglePropertyDefinition) and not prop.nested_properties and prop.required: - val = values.get(prop.property_key) - if val is None or not str(val).strip(): - missing.append(prop.property_key) - return missing + + full_key = f"{path_prefix}{prop.property_key}" if path_prefix else prop.property_key + + if isinstance(prop, ConfigSinglePropertyDefinition): + if prop.nested_properties: + nested_vals = values.get(prop.property_key, {}) + if not isinstance(nested_vals, dict): + nested_vals = {} + errors.extend(_validate_fields_recursive(prop.nested_properties, nested_vals, f"{full_key}.")) + continue + + raw = values.get(prop.property_key) + text = str(raw).strip() if raw is not None else "" + + # Required check. + if prop.required and not text: + errors.append(f"{full_key}: required field is empty") + continue + + if not text: + continue + + # Type-specific checks. + if prop.property_type is int or (prop.property_key in _PORT_KEYS): + port_err = validate_port(text) + if port_err: + errors.append(f"{full_key}: {port_err}") + continue + + if prop.property_key in _HOST_KEYS: + host_err = validate_hostname(text) + if host_err: + errors.append(f"{full_key}: {host_err}") + + return errors def render_datasource_config_form( diff --git a/src/databao_cli/features/ui/components/datasource_manager.py b/src/databao_cli/features/ui/components/datasource_manager.py index deb21823..bd14dd99 100644 --- a/src/databao_cli/features/ui/components/datasource_manager.py +++ b/src/databao_cli/features/ui/components/datasource_manager.py @@ -15,8 +15,8 @@ from databao_cli.features.datasource.validation import validate_datasource_name from databao_cli.features.ui.app import invalidate_agent from databao_cli.features.ui.components.datasource_form import ( - get_missing_required_fields, render_datasource_config_form, + validate_config_fields, ) from databao_cli.features.ui.services.dce_operations import ( add_datasource, @@ -130,8 +130,9 @@ def _render_add_datasource_section(project_dir: Path) -> None: validated = _validate_new_datasource_inputs(ds_name, selected_type) if validated is None: pass # errors already shown - elif config_fields and (missing := get_missing_required_fields(config_fields, config_values)): - st.error(f"Required fields are empty: {', '.join(missing)}") + elif config_fields and (field_errors := validate_config_fields(config_fields, config_values)): + for err in field_errors: + st.error(err) else: try: add_datasource(project_dir, selected_type, validated, config_values) diff --git a/tests/test_datasource_validation.py b/tests/test_datasource_validation.py index 5f4c7cda..0c605d8b 100644 --- a/tests/test_datasource_validation.py +++ b/tests/test_datasource_validation.py @@ -3,31 +3,52 @@ from databao_cli.features.datasource.validation import ( MAX_DATASOURCE_NAME_LENGTH, validate_datasource_name, + validate_hostname, + validate_port, ) class TestValidateDatasourceName: - """Tests for validate_datasource_name().""" + """Tests for validate_datasource_name(). + + The last segment must match the agent's pattern: ^[A-Za-z][A-Za-z0-9_]*$ + Folder segments (preceding the last) are more permissive. + """ @pytest.mark.parametrize( "name", [ - "my-datasource", "my_datasource", - "my.datasource", "ds1", "a", "A", - "Snowflake-Prod", - "test_db.v2", + "SnowflakeProd", + "test_db", "ab", "resources/my_db", "folder/sub/name", + "my-folder/my_db", + "v2.data/source1", ], ) def test_valid_names(self, name: str) -> None: assert validate_datasource_name(name) is None + @pytest.mark.parametrize( + "name", + [ + "my-datasource", + "my.datasource", + "Snowflake-Prod", + "test_db.v2", + "1startsWithDigit", + "_leading_underscore", + ], + ) + def test_invalid_source_name_segment(self, name: str) -> None: + """Last segment must match agent pattern: letter then [A-Za-z0-9_]*.""" + assert validate_datasource_name(name) is not None + def test_empty_name(self) -> None: assert validate_datasource_name("") is not None @@ -39,18 +60,6 @@ def test_name_with_spaces(self) -> None: assert error is not None assert "spaces" in error.lower() - def test_name_with_leading_space(self) -> None: - error = validate_datasource_name(" leading") - assert error is not None - - def test_name_with_trailing_space(self) -> None: - error = validate_datasource_name("trailing ") - assert error is not None - - @pytest.mark.parametrize("name", [".hidden", "-start", "end-", "end."]) - def test_name_starting_or_ending_with_special_char(self, name: str) -> None: - assert validate_datasource_name(name) is not None - @pytest.mark.parametrize("char", ["@", "#", "$", "%", "!", "?", "\\", ":", "*"]) def test_forbidden_characters(self, char: str) -> None: assert validate_datasource_name(f"ds{char}name") is not None @@ -64,6 +73,9 @@ def test_leading_slash_rejected(self) -> None: def test_trailing_slash_rejected(self) -> None: assert validate_datasource_name("a/") is not None + def test_invalid_folder_segment(self) -> None: + assert validate_datasource_name(".hidden/my_db") is not None + def test_name_too_long(self) -> None: long_name = "a" * (MAX_DATASOURCE_NAME_LENGTH + 1) error = validate_datasource_name(long_name) @@ -73,3 +85,49 @@ def test_name_too_long(self) -> None: def test_name_at_max_length(self) -> None: name = "a" * MAX_DATASOURCE_NAME_LENGTH assert validate_datasource_name(name) is None + + +class TestValidatePort: + """Tests for validate_port().""" + + @pytest.mark.parametrize("value", ["1", "80", "443", "5432", "65535"]) + def test_valid_ports(self, value: str) -> None: + assert validate_port(value) is None + + @pytest.mark.parametrize("value", ["0", "-1", "65536", "99999"]) + def test_out_of_range(self, value: str) -> None: + error = validate_port(value) + assert error is not None + assert "between" in error.lower() + + @pytest.mark.parametrize("value", ["abc", "12.5", "", " "]) + def test_non_numeric(self, value: str) -> None: + error = validate_port(value) + assert error is not None + assert "number" in error.lower() + + +class TestValidateHostname: + """Tests for validate_hostname().""" + + @pytest.mark.parametrize( + "value", + [ + "localhost", + "127.0.0.1", + "192.168.1.1", + "my-host", + "db.example.com", + "my-db.internal.corp.net", + ], + ) + def test_valid_hostnames(self, value: str) -> None: + assert validate_hostname(value) is None + + def test_empty_hostname(self) -> None: + assert validate_hostname("") is not None + assert validate_hostname(" ") is not None + + @pytest.mark.parametrize("value", ["-leading-hyphen", "trailing-hyphen-"]) + def test_invalid_hostnames(self, value: str) -> None: + assert validate_hostname(value) is not None From cf3edf034f79351a0b914cef17467d53ac641e8a Mon Sep 17 00:00:00 2001 From: Andrei Gasparian Date: Thu, 26 Mar 2026 19:47:31 +0100 Subject: [PATCH 05/11] fixes to pass e2e --- e2e-tests/src/databases/duckdb_utils.py | 2 +- e2e-tests/src/databases/sqlite_utils.py | 2 +- e2e-tests/tests/resources/duckdb_introspections.yaml | 4 ++-- e2e-tests/tests/resources/sqlite_introspections.yaml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/e2e-tests/src/databases/duckdb_utils.py b/e2e-tests/src/databases/duckdb_utils.py index 7d5c1e21..fe54c29a 100644 --- a/e2e-tests/src/databases/duckdb_utils.py +++ b/e2e-tests/src/databases/duckdb_utils.py @@ -6,7 +6,7 @@ @dataclass(frozen=True) class DuckdbDB: - datasource_name: str | None = "test duckdb conn" + datasource_name: str | None = "test_duckdb_conn" datasource_type: str = "duckdb" database_path: Path | None = None check_connection: bool = False diff --git a/e2e-tests/src/databases/sqlite_utils.py b/e2e-tests/src/databases/sqlite_utils.py index 3ebb2e2c..dc12997b 100644 --- a/e2e-tests/src/databases/sqlite_utils.py +++ b/e2e-tests/src/databases/sqlite_utils.py @@ -5,7 +5,7 @@ @dataclass(frozen=True) class SqliteDB: - datasource_name: str | None = "test sqlite conn" + datasource_name: str | None = "test_sqlite_conn" datasource_type: str = "sqlite" database_path: Path | None = None check_connection: bool = False diff --git a/e2e-tests/tests/resources/duckdb_introspections.yaml b/e2e-tests/tests/resources/duckdb_introspections.yaml index eca0a34d..23a95bdf 100644 --- a/e2e-tests/tests/resources/duckdb_introspections.yaml +++ b/e2e-tests/tests/resources/duckdb_introspections.yaml @@ -1,5 +1,5 @@ -# ===== test duckdb conn.yaml ===== -datasource_id: test duckdb conn.yaml +# ===== test_duckdb_conn.yaml ===== +datasource_id: test_duckdb_conn.yaml datasource_type: duckdb context_built_at: 2026-02-27 16:38:36.999625 context: diff --git a/e2e-tests/tests/resources/sqlite_introspections.yaml b/e2e-tests/tests/resources/sqlite_introspections.yaml index b1af0fe5..6c5da4db 100644 --- a/e2e-tests/tests/resources/sqlite_introspections.yaml +++ b/e2e-tests/tests/resources/sqlite_introspections.yaml @@ -1,5 +1,5 @@ -# ===== test sqlite conn.yaml ===== -datasource_id: test sqlite conn.yaml +# ===== test_sqlite_conn.yaml ===== +datasource_id: test_sqlite_conn.yaml datasource_type: sqlite context_built_at: 2026-02-27 16:15:31.010532 context: From 04afd375e41b0e4b4133c2edddfa45e74e14e943 Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Mon, 30 Mar 2026 10:38:17 +0200 Subject: [PATCH 06/11] [DBA-141] Address PR review: stricter validation, union support, verify path - Use stdlib ipaddress for IP validation instead of colon heuristic - Catch all whitespace (tabs, newlines, NBSP) not just spaces - Split int parsing from port range validation (only port-named fields) - Validate union property variants recursively - Apply SKIP_TOP_LEVEL_KEYS only at top level, not nested - Run config field validation on "Verify connection" path - Update PR description to match actual name constraints - Add tests for all changes Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/datasource/validation.py | 17 ++- .../features/ui/components/datasource_form.py | 23 +++- .../ui/components/datasource_manager.py | 7 +- tests/test_datasource_form_validation.py | 122 ++++++++++++++++++ tests/test_datasource_validation.py | 15 +++ 5 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 tests/test_datasource_form_validation.py diff --git a/src/databao_cli/features/datasource/validation.py b/src/databao_cli/features/datasource/validation.py index 087bf940..dce2047a 100644 --- a/src/databao_cli/features/datasource/validation.py +++ b/src/databao_cli/features/datasource/validation.py @@ -5,6 +5,7 @@ datasource. """ +import ipaddress import re # The agent requires source names to match this pattern so they can be @@ -39,7 +40,7 @@ def validate_datasource_name(name: str) -> str | None: if len(name) > MAX_DATASOURCE_NAME_LENGTH: return f"Datasource name must be at most {MAX_DATASOURCE_NAME_LENGTH} characters." - if " " in name: + if re.search(r"\s", name): return "Datasource name must not contain spaces." segments = name.split("/") @@ -91,9 +92,11 @@ def validate_hostname(value: str) -> str | None: def _is_ip_address(value: str) -> bool: - """Return True if *value* looks like an IPv4 or bracketed IPv6 address.""" - parts = value.split(".") - if len(parts) == 4: - return all(p.isdigit() and 0 <= int(p) <= 255 for p in parts) - # Bracketed IPv6 or raw IPv6. - return ":" in value + """Return True if *value* is a valid IPv4 or IPv6 address.""" + # Strip brackets for bracketed IPv6 (e.g. [::1]). + stripped = value.strip("[]") + try: + ipaddress.ip_address(stripped) + return True + except ValueError: + return False diff --git a/src/databao_cli/features/ui/components/datasource_form.py b/src/databao_cli/features/ui/components/datasource_form.py index 09903366..55272ac5 100644 --- a/src/databao_cli/features/ui/components/datasource_form.py +++ b/src/databao_cli/features/ui/components/datasource_form.py @@ -49,11 +49,24 @@ def _validate_fields_recursive( ) -> list[str]: errors: list[str] = [] for prop in config_fields: - if prop.property_key in SKIP_TOP_LEVEL_KEYS: + # Only skip special top-level keys like "type" / "name". + if not path_prefix and prop.property_key in SKIP_TOP_LEVEL_KEYS: continue full_key = f"{path_prefix}{prop.property_key}" if path_prefix else prop.property_key + if isinstance(prop, ConfigUnionPropertyDefinition): + union_vals = values.get(prop.property_key, {}) + if not isinstance(union_vals, dict): + union_vals = {} + type_choices = {t.__name__: t for t in prop.types} + selected_name = union_vals.get("type") or _infer_union_type(union_vals, type_choices, prop.type_properties) + if selected_name and selected_name in type_choices: + selected_type = type_choices[selected_name] + nested_props = prop.type_properties.get(selected_type, []) + errors.extend(_validate_fields_recursive(nested_props, union_vals, f"{full_key}.")) + continue + if isinstance(prop, ConfigSinglePropertyDefinition): if prop.nested_properties: nested_vals = values.get(prop.property_key, {}) @@ -74,11 +87,17 @@ def _validate_fields_recursive( continue # Type-specific checks. - if prop.property_type is int or (prop.property_key in _PORT_KEYS): + if prop.property_key in _PORT_KEYS: port_err = validate_port(text) if port_err: errors.append(f"{full_key}: {port_err}") continue + elif prop.property_type is int: + try: + int(text) + except ValueError: + errors.append(f"{full_key}: must be a valid integer") + continue if prop.property_key in _HOST_KEYS: host_err = validate_hostname(text) diff --git a/src/databao_cli/features/ui/components/datasource_manager.py b/src/databao_cli/features/ui/components/datasource_manager.py index bd14dd99..43558ade 100644 --- a/src/databao_cli/features/ui/components/datasource_manager.py +++ b/src/databao_cli/features/ui/components/datasource_manager.py @@ -145,7 +145,12 @@ def _render_add_datasource_section(project_dir: Path) -> None: with col_verify_new: if st.button("Verify connection", key="verify_new_ds_btn", use_container_width=True): validated = _validate_new_datasource_inputs(ds_name, selected_type) - if validated is not None: + if validated is None: + pass # errors already shown + elif config_fields and (field_errors := validate_config_fields(config_fields, config_values)): + for err in field_errors: + st.error(err) + else: try: result = verify_datasource_config(selected_type, validated, config_values) if result.connection_status == DatasourceConnectionStatus.VALID: diff --git a/tests/test_datasource_form_validation.py b/tests/test_datasource_form_validation.py new file mode 100644 index 00000000..1cb15044 --- /dev/null +++ b/tests/test_datasource_form_validation.py @@ -0,0 +1,122 @@ +"""Tests for validate_config_fields() in datasource_form.py.""" + +from databao_context_engine.pluginlib.config import ( + ConfigPropertyDefinition, + ConfigSinglePropertyDefinition, + ConfigUnionPropertyDefinition, +) + +from databao_cli.features.ui.components.datasource_form import validate_config_fields + + +def _single( + key: str, + *, + required: bool = False, + property_type: type = str, + nested: list[ConfigPropertyDefinition] | None = None, +) -> ConfigSinglePropertyDefinition: + return ConfigSinglePropertyDefinition( + property_key=key, + required=required, + property_type=property_type, + nested_properties=nested, + ) + + +def _fields(*props: ConfigPropertyDefinition) -> list[ConfigPropertyDefinition]: + """Helper to build a correctly-typed field list.""" + return list(props) + + +class TestIntVsPortValidation: + """Port range check only applies to port-named fields, not all int fields.""" + + def test_non_port_int_field_accepts_large_number(self) -> None: + fields = _fields(_single("timeout", property_type=int)) + errors = validate_config_fields(fields, {"timeout": "120000"}) + assert errors == [] + + def test_non_port_int_field_rejects_non_numeric(self) -> None: + fields = _fields(_single("retries", property_type=int)) + errors = validate_config_fields(fields, {"retries": "abc"}) + assert len(errors) == 1 + assert "integer" in errors[0].lower() + + def test_port_field_rejects_out_of_range(self) -> None: + fields = _fields(_single("port", property_type=int)) + errors = validate_config_fields(fields, {"port": "99999"}) + assert len(errors) == 1 + assert "between" in errors[0].lower() + + def test_port_field_accepts_valid_port(self) -> None: + fields = _fields(_single("port", property_type=int)) + errors = validate_config_fields(fields, {"port": "5432"}) + assert errors == [] + + +class TestSkipTopLevelKeysScope: + """SKIP_TOP_LEVEL_KEYS should only apply at the top level.""" + + def test_top_level_type_is_skipped(self) -> None: + fields = _fields(_single("type", required=True), _single("host", required=True)) + errors = validate_config_fields(fields, {"host": ""}) + # "type" should be skipped, only "host" error expected + assert len(errors) == 1 + assert "host" in errors[0] + + def test_nested_name_field_is_validated(self) -> None: + fields = _fields( + _single( + "connection", + nested=[_single("name", required=True)], + ), + ) + errors = validate_config_fields(fields, {"connection": {"name": ""}}) + assert len(errors) == 1 + assert "connection.name" in errors[0] + + +class TestUnionPropertyValidation: + """Union properties should be validated recursively.""" + + def test_union_required_field_missing(self) -> None: + class VariantA: + pass + + union = ConfigUnionPropertyDefinition( + property_key="auth", + types=(VariantA,), + type_properties={ + VariantA: [_single("username", required=True)], + }, + ) + fields = _fields(union) + errors = validate_config_fields(fields, {"auth": {"type": "VariantA", "username": ""}}) + assert len(errors) == 1 + assert "username" in errors[0] + + def test_union_valid_fields_pass(self) -> None: + class VariantA: + pass + + union = ConfigUnionPropertyDefinition( + property_key="auth", + types=(VariantA,), + type_properties={ + VariantA: [_single("username", required=True)], + }, + ) + fields = _fields(union) + errors = validate_config_fields(fields, {"auth": {"type": "VariantA", "username": "admin"}}) + assert errors == [] + + +class TestHostValidation: + """Host field validation within config fields.""" + + def test_hostname_with_port_rejected(self) -> None: + fields = _fields(_single("host")) + errors = validate_config_fields(fields, {"host": "db.example.com:5432"}) + assert len(errors) == 1 + assert "host" in errors[0] diff --git a/tests/test_datasource_validation.py b/tests/test_datasource_validation.py index 0c605d8b..c488fe3d 100644 --- a/tests/test_datasource_validation.py +++ b/tests/test_datasource_validation.py @@ -60,6 +60,13 @@ def test_name_with_spaces(self) -> None: assert error is not None assert "spaces" in error.lower() + @pytest.mark.parametrize("name", ["my\tdatasource", "my\ndatasource", "my\u00a0datasource"]) + def test_name_with_non_space_whitespace(self, name: str) -> None: + """Tabs, newlines, and non-breaking spaces should also be rejected.""" + error = validate_datasource_name(name) + assert error is not None + assert "spaces" in error.lower() + @pytest.mark.parametrize("char", ["@", "#", "$", "%", "!", "?", "\\", ":", "*"]) def test_forbidden_characters(self, char: str) -> None: assert validate_datasource_name(f"ds{char}name") is not None @@ -119,6 +126,9 @@ class TestValidateHostname: "my-host", "db.example.com", "my-db.internal.corp.net", + "::1", + "[::1]", + "2001:db8::1", ], ) def test_valid_hostnames(self, value: str) -> None: @@ -131,3 +141,8 @@ def test_empty_hostname(self) -> None: @pytest.mark.parametrize("value", ["-leading-hyphen", "trailing-hyphen-"]) def test_invalid_hostnames(self, value: str) -> None: assert validate_hostname(value) is not None + + @pytest.mark.parametrize("value", ["not-an-ip:still-not", "db.example.com:5432"]) + def test_colon_strings_not_treated_as_ip(self, value: str) -> None: + """Strings with colons that are not valid IPv6 should not pass as IPs.""" + assert validate_hostname(value) is not None From 35eb85f9cb662bc85c82133d27e3e736960d7146 Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Mon, 30 Mar 2026 10:53:38 +0200 Subject: [PATCH 07/11] [DBA-141] Validate config fields on existing datasource save Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/ui/components/datasource_manager.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/databao_cli/features/ui/components/datasource_manager.py b/src/databao_cli/features/ui/components/datasource_manager.py index 43558ade..0a3f56a0 100644 --- a/src/databao_cli/features/ui/components/datasource_manager.py +++ b/src/databao_cli/features/ui/components/datasource_manager.py @@ -205,12 +205,16 @@ def _render_existing_datasource(project_dir: Path, ds: ConfiguredDatasource, idx disabled=not has_changes, use_container_width=True, ): - try: - save_datasource(project_dir, ds_type, ds_name, edited_values) - st.success("Saved.") - st.rerun() - except Exception as e: - st.error(f"Save failed: {e}") + if config_fields and (field_errors := validate_config_fields(config_fields, edited_values)): + for err in field_errors: + st.error(err) + else: + try: + save_datasource(project_dir, ds_type, ds_name, edited_values) + st.success("Saved.") + st.rerun() + except Exception as e: + st.error(f"Save failed: {e}") with col_verify: if st.button("Verify", key=f"verify_ds_{idx}", use_container_width=True): From bae1a6311297065b9757f96fb9d9ad5655c56918 Mon Sep 17 00:00:00 2001 From: Simon Karan <69362542+SimonKaran13@users.noreply.github.com> Date: Mon, 30 Mar 2026 11:39:18 +0200 Subject: [PATCH 08/11] [DBA-141] use 'whitespace' in the error message instead of 'space' Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/databao_cli/features/datasource/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/databao_cli/features/datasource/validation.py b/src/databao_cli/features/datasource/validation.py index dce2047a..f461333a 100644 --- a/src/databao_cli/features/datasource/validation.py +++ b/src/databao_cli/features/datasource/validation.py @@ -41,7 +41,7 @@ def validate_datasource_name(name: str) -> str | None: return f"Datasource name must be at most {MAX_DATASOURCE_NAME_LENGTH} characters." if re.search(r"\s", name): - return "Datasource name must not contain spaces." + return "Datasource name must not contain whitespace." segments = name.split("/") for segment in segments: From bdaf9e5fd6550b3a39f690d34ddf82c4912dd614 Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Mon, 30 Mar 2026 14:11:07 +0200 Subject: [PATCH 09/11] [DBA-141] Remove bracket handling from IP validation, add single-variant union fallback Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/datasource/validation.py | 4 +--- .../features/ui/components/datasource_form.py | 4 ++++ tests/test_datasource_form_validation.py | 18 ++++++++++++++++++ tests/test_datasource_validation.py | 1 - 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/databao_cli/features/datasource/validation.py b/src/databao_cli/features/datasource/validation.py index f461333a..cd30922b 100644 --- a/src/databao_cli/features/datasource/validation.py +++ b/src/databao_cli/features/datasource/validation.py @@ -93,10 +93,8 @@ def validate_hostname(value: str) -> str | None: def _is_ip_address(value: str) -> bool: """Return True if *value* is a valid IPv4 or IPv6 address.""" - # Strip brackets for bracketed IPv6 (e.g. [::1]). - stripped = value.strip("[]") try: - ipaddress.ip_address(stripped) + ipaddress.ip_address(value) return True except ValueError: return False diff --git a/src/databao_cli/features/ui/components/datasource_form.py b/src/databao_cli/features/ui/components/datasource_form.py index 55272ac5..829fe129 100644 --- a/src/databao_cli/features/ui/components/datasource_form.py +++ b/src/databao_cli/features/ui/components/datasource_form.py @@ -65,6 +65,10 @@ def _validate_fields_recursive( selected_type = type_choices[selected_name] nested_props = prop.type_properties.get(selected_type, []) errors.extend(_validate_fields_recursive(nested_props, union_vals, f"{full_key}.")) + elif len(prop.types) == 1: + sole_type = prop.types[0] + nested_props = prop.type_properties.get(sole_type, []) + errors.extend(_validate_fields_recursive(nested_props, union_vals, f"{full_key}.")) continue if isinstance(prop, ConfigSinglePropertyDefinition): diff --git a/tests/test_datasource_form_validation.py b/tests/test_datasource_form_validation.py index 1cb15044..6642ebd1 100644 --- a/tests/test_datasource_form_validation.py +++ b/tests/test_datasource_form_validation.py @@ -111,6 +111,24 @@ class VariantA: errors = validate_config_fields(fields, {"auth": {"type": "VariantA", "username": "admin"}}) assert errors == [] + def test_single_variant_union_validates_with_empty_dict(self) -> None: + """When there's only one variant and no type discriminator, still validate.""" + + class OnlyVariant: + pass + + union = ConfigUnionPropertyDefinition( + property_key="auth", + types=(OnlyVariant,), + type_properties={ + OnlyVariant: [_single("token", required=True)], + }, + ) + fields = _fields(union) + errors = validate_config_fields(fields, {"auth": {}}) + assert len(errors) == 1 + assert "token" in errors[0] + class TestHostValidation: """Host field validation within config fields.""" diff --git a/tests/test_datasource_validation.py b/tests/test_datasource_validation.py index c488fe3d..71b1ffd7 100644 --- a/tests/test_datasource_validation.py +++ b/tests/test_datasource_validation.py @@ -127,7 +127,6 @@ class TestValidateHostname: "db.example.com", "my-db.internal.corp.net", "::1", - "[::1]", "2001:db8::1", ], ) From 79a0c5fea2329c063a4fabfedaaeaec58da6798a Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Mon, 30 Mar 2026 14:12:37 +0200 Subject: [PATCH 10/11] [DBA-141] Fix test assertions to match updated whitespace error message Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_datasource_validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_datasource_validation.py b/tests/test_datasource_validation.py index 71b1ffd7..e3169b6d 100644 --- a/tests/test_datasource_validation.py +++ b/tests/test_datasource_validation.py @@ -58,14 +58,14 @@ def test_whitespace_only(self) -> None: def test_name_with_spaces(self) -> None: error = validate_datasource_name("my datasource") assert error is not None - assert "spaces" in error.lower() + assert "whitespace" in error.lower() @pytest.mark.parametrize("name", ["my\tdatasource", "my\ndatasource", "my\u00a0datasource"]) def test_name_with_non_space_whitespace(self, name: str) -> None: """Tabs, newlines, and non-breaking spaces should also be rejected.""" error = validate_datasource_name(name) assert error is not None - assert "spaces" in error.lower() + assert "whitespace" in error.lower() @pytest.mark.parametrize("char", ["@", "#", "$", "%", "!", "?", "\\", ":", "*"]) def test_forbidden_characters(self, char: str) -> None: From d9dae9290ebf702ed493ad3b5c87a1c7395d7441 Mon Sep 17 00:00:00 2001 From: Simon Karan Date: Mon, 30 Mar 2026 14:24:21 +0200 Subject: [PATCH 11/11] [DBA-141] Add multi-variant union error, clarify empty-segment message Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/datasource/validation.py | 4 +++- .../features/ui/components/datasource_form.py | 5 +++++ tests/test_datasource_form_validation.py | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/databao_cli/features/datasource/validation.py b/src/databao_cli/features/datasource/validation.py index cd30922b..43448b1b 100644 --- a/src/databao_cli/features/datasource/validation.py +++ b/src/databao_cli/features/datasource/validation.py @@ -46,7 +46,9 @@ def validate_datasource_name(name: str) -> str | None: segments = name.split("/") for segment in segments: if not segment: - return "Datasource name must not contain empty path segments (double slashes)." + return ( + "Datasource name must not contain empty path segments (for example, leading, trailing, or repeated slashes)." + ) # Validate folder segments (all but the last). for segment in segments[:-1]: diff --git a/src/databao_cli/features/ui/components/datasource_form.py b/src/databao_cli/features/ui/components/datasource_form.py index 829fe129..2fcbdd56 100644 --- a/src/databao_cli/features/ui/components/datasource_form.py +++ b/src/databao_cli/features/ui/components/datasource_form.py @@ -69,6 +69,11 @@ def _validate_fields_recursive( sole_type = prop.types[0] nested_props = prop.type_properties.get(sole_type, []) errors.extend(_validate_fields_recursive(nested_props, union_vals, f"{full_key}.")) + elif len(prop.types) > 1: + errors.append( + f"{full_key}: union type could not be determined; " + "select a configuration variant and provide required fields" + ) continue if isinstance(prop, ConfigSinglePropertyDefinition): diff --git a/tests/test_datasource_form_validation.py b/tests/test_datasource_form_validation.py index 6642ebd1..6c45772c 100644 --- a/tests/test_datasource_form_validation.py +++ b/tests/test_datasource_form_validation.py @@ -129,6 +129,28 @@ class OnlyVariant: assert len(errors) == 1 assert "token" in errors[0] + def test_multi_variant_union_errors_when_type_unknown(self) -> None: + """When multiple variants exist and type can't be inferred, emit an error.""" + + class VariantA: + pass + + class VariantB: + pass + + union = ConfigUnionPropertyDefinition( + property_key="auth", + types=(VariantA, VariantB), + type_properties={ + VariantA: [_single("username", required=True)], + VariantB: [_single("token", required=True)], + }, + ) + fields = _fields(union) + errors = validate_config_fields(fields, {"auth": {}}) + assert len(errors) == 1 + assert "could not be determined" in errors[0] + class TestHostValidation: """Host field validation within config fields."""