diff --git a/features/resource_type_digit_start_validation.feature b/features/resource_type_digit_start_validation.feature new file mode 100644 index 000000000..0b71ca62d --- /dev/null +++ b/features/resource_type_digit_start_validation.feature @@ -0,0 +1,46 @@ +Feature: Resource type name/namespace must start with a letter + The spec requires that namespace and name components in the + ``[[server:]namespace/]name`` format start with a letter. + The ``_NAMESPACED_RE`` pattern in both ``resource_type.py`` and + ``_resource_type_validation.py`` must enforce this constraint. + + Background: + Given the resource type model module is imported + + # -- Rejection of digit-starting namespace -------------------------------- + + Scenario: Custom resource type with digit-starting namespace is rejected + When I create a custom resource type with name "123abc/my-type" + Then a validation error should be raised mentioning invalid resource type name + + Scenario: Custom resource type with digit-starting name component is rejected + When I create a custom resource type with name "local/456-type" + Then a validation error should be raised mentioning invalid resource type name + + Scenario: Custom resource type with digit-starting both components is rejected + When I create a custom resource type with name "1ns/2name" + Then a validation error should be raised mentioning invalid resource type name + + # -- Valid namespaced names still accepted -------------------------------- + + Scenario: Custom resource type with letter-starting namespace and name is accepted + When I create a custom resource type with name "local/my-type" + Then no validation error should be raised + + Scenario: Custom resource type with org-style name is accepted + When I create a custom resource type with name "myorg/custom-db" + Then no validation error should be raised + + Scenario: Custom resource type with underscore in namespace is accepted + When I create a custom resource type with name "my_org/my_type" + Then no validation error should be raised + + # -- Digit-starting names rejected via inherits field --------------------- + + Scenario: inherits field with digit-starting namespace is rejected + When I create a resource type spec with inherits set to "123abc/parent-type" + Then a validation error should be raised mentioning invalid inherits type name + + Scenario: inherits field with digit-starting name component is rejected + When I create a resource type spec with inherits set to "myorg/456-parent" + Then a validation error should be raised mentioning invalid inherits type name diff --git a/features/steps/resource_type_digit_start_validation_steps.py b/features/steps/resource_type_digit_start_validation_steps.py new file mode 100644 index 000000000..bcbf96fb5 --- /dev/null +++ b/features/steps/resource_type_digit_start_validation_steps.py @@ -0,0 +1,92 @@ +"""Step definitions for resource_type_digit_start_validation.feature. + +Tests that _NAMESPACED_RE in resource_type.py and _resource_type_validation.py +correctly rejects namespace/name components that start with a digit. + +Spec requirement: namespace and name components must start with a letter. + +Note: The step "a validation error should be raised mentioning invalid inherits +type name" is defined in resource_type_model_coverage_steps.py and reused here. +""" + +from __future__ import annotations + +from behave import then, when +from pydantic import ValidationError + +from cleveragents.domain.models.core.resource_type import ( + ResourceKind, + ResourceTypeSpec, + SandboxStrategy, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_custom_physical_spec(**overrides) -> dict: + """Build a minimal valid custom (non-built-in) physical ResourceTypeSpec dict.""" + defaults = dict( + name="myorg/my-type", + resource_kind=ResourceKind.PHYSICAL, + sandbox_strategy=SandboxStrategy.GIT_WORKTREE, + built_in=False, + ) + defaults.update(overrides) + return defaults + + +# --------------------------------------------------------------------------- +# WHEN steps +# --------------------------------------------------------------------------- + + +@when('I create a custom resource type with name "{name}"') +def step_create_custom_resource_type(context, name: str) -> None: + """Attempt to create a custom ResourceTypeSpec with the given name.""" + try: + ResourceTypeSpec(**_make_custom_physical_spec(name=name)) + context.rt_error = None + except ValidationError as exc: + context.rt_error = exc + + +@when('I create a resource type spec with inherits set to "{inherits_name}"') +def step_create_with_inherits_digit(context, inherits_name: str) -> None: + """Attempt to create a ResourceTypeSpec with the given inherits value.""" + try: + ResourceTypeSpec(**_make_custom_physical_spec(inherits=inherits_name)) + context.rt_error = None + except ValidationError as exc: + context.rt_error = exc + + +# --------------------------------------------------------------------------- +# THEN steps +# --------------------------------------------------------------------------- + + +@then("a validation error should be raised mentioning invalid resource type name") +def step_verify_invalid_name_error(context) -> None: + """Assert a ValidationError was raised mentioning invalid resource type name.""" + assert context.rt_error is not None, ( + "Expected a ValidationError for an invalid resource type name, " + "but no exception was raised." + ) + assert isinstance(context.rt_error, ValidationError), ( + f"Expected ValidationError, got {type(context.rt_error).__name__}: " + f"{context.rt_error}" + ) + error_str = str(context.rt_error) + assert "Invalid resource type name" in error_str, ( + f"Expected 'Invalid resource type name' in error message, got: {error_str}" + ) + + +@then("no validation error should be raised") +def step_verify_no_error(context) -> None: + """Assert no ValidationError was raised.""" + assert context.rt_error is None, ( + f"Expected no ValidationError, but got: {context.rt_error}" + ) diff --git a/src/cleveragents/domain/models/core/_resource_type_validation.py b/src/cleveragents/domain/models/core/_resource_type_validation.py index 846676f6b..f9bae83b5 100644 --- a/src/cleveragents/domain/models/core/_resource_type_validation.py +++ b/src/cleveragents/domain/models/core/_resource_type_validation.py @@ -11,7 +11,7 @@ import re from typing import Any _BUILTIN_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$") -_NAMESPACED_RE = re.compile(r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$") +_NAMESPACED_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*/[a-zA-Z][a-zA-Z0-9_-]*$") MAX_SCAN_DEPTH = 10 diff --git a/src/cleveragents/domain/models/core/resource_type.py b/src/cleveragents/domain/models/core/resource_type.py index 24de259b8..2850d0857 100644 --- a/src/cleveragents/domain/models/core/resource_type.py +++ b/src/cleveragents/domain/models/core/resource_type.py @@ -44,8 +44,8 @@ from cleveragents.domain.models.core._resource_type_validation import ( #: CLI-safe argument name: lowercase alphanumeric, hyphens, underscores. _CLI_ARG_NAME_RE = re.compile(r"^[a-z][a-z0-9_-]*$") -#: Namespaced name: ``namespace/name`` with alphanumeric, hyphens, underscores. -_NAMESPACED_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*/[a-zA-Z0-9][a-zA-Z0-9_-]*$") +#: Namespaced name: ``namespace/name`` — both components must start with a letter. +_NAMESPACED_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*/[a-zA-Z][a-zA-Z0-9_-]*$") #: Built-in (unnamespaced) name: simple alphanumeric + hyphens/underscores. _BUILTIN_NAME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]*$")