fix(resource-type): require letter-start for namespace/name in _NAMESPACED_RE #3290

Merged
freemo merged 1 commits from fix/resource-type-namespaced-re-digit-start into master 2026-04-05 21:12:25 +00:00
4 changed files with 141 additions and 3 deletions
@@ -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
@@ -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}"
)
@@ -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
@@ -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_-]*$")