Repository: getmoto/moto (python)

Issue:
SecretsManager List Secrets Filter with Key all deviated from boto3
## How to reproduce

```python
@pytest.fixture
def secretsmanager_client():
  with mock_aws():
    yield boto3.client("secretsmanager", region_name="us-east-1")
    
def test_list_secrets(secretsmanager_client):
  secretsmanager_client.create_secret(Name="ThisTestSecret")
  secretsmanager_client.create_secret(Name="RealSecret")
  secretsmanager_client.create_secret(Name="NexttestSecret")
  
  response = secretsmanager_client.list_secrets(
    Filters=[{"Key": "all", "Values": ["test"]}]
  )
```

With the Filter using the `Key` of `all` boto3 documentation states:
> Breaks the filter value string into words and then searches all attributes for matches. Not case-sensitive.

The response from moto returns zero results (`'SecretList': []`) when it should return two secret objects with the Names: `ThisTestSecret` and `NexttestSecret`. 

I've tested this against an AWS environment and boto3 works as described and does return the expected results.

## Versions

Installed using poetry:
- moto v5.0.24
- boto3 v1.35.83

Pull request:
SecretsManager: list_secrets() now filters values with special chars correctly
Followup to #8469, words are now also split by special characters (`/`, `+`, and a few others).

Fixes #8421 

Whether this is the final form of this algorithm, I don't know - it's quite possible that I've missed some use cases. It is currently closer to AWS parity than before though, with the (AWS-validated) tests to prove it.

Patch:
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index 383d77679..3bada2e53 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -879,6 +879,7 @@ class DynamoHandler(BaseResponse):
             result["LastEvaluatedKey"] = last_evaluated_key
         return dynamo_json_dump(result)
 
+    @include_consumed_capacity()
     def delete_item(self) -> str:
         name = self.body["TableName"]
         key = self.body["Key"]
diff --git a/moto/secretsmanager/list_secrets/filters.py b/moto/secretsmanager/list_secrets/filters.py
index 2d0a5b828..57e979014 100644
--- a/moto/secretsmanager/list_secrets/filters.py
+++ b/moto/secretsmanager/list_secrets/filters.py
@@ -1,5 +1,5 @@
 import re
-from typing import TYPE_CHECKING, List
+from typing import TYPE_CHECKING, Iterator, List, Union
 
 if TYPE_CHECKING:
     from ..models import FakeSecret
@@ -76,6 +76,8 @@ def _match_pattern(
             return value.startswith(pattern)
     else:
         pattern_words = split_words(pattern)
+        if not pattern_words:
+            return False
         value_words = split_words(value)
         if not case_sensitive:
             pattern_words = [p.lower() for p in pattern_words]
@@ -90,9 +92,40 @@ def _match_pattern(
 
 
 def split_words(s: str) -> List[str]:
+    """
+    Secrets are split by special characters first (/, +, _, etc)
+    Partial results are then split again by UpperCasing
+    """
+    special_chars = ["/", "-", "_", "+", "=", ".", "@"]
+
+    if s in special_chars:
+        # Special case: this does not return any values
+        return []
+
+    for char in special_chars:
+        if char in s:
+            others = special_chars.copy()
+            others.remove(char)
+            contains_other = any([c in s for c in others])
+            if contains_other:
+                # Secret contains two different characters, i.e. my/secret+value
+                # Values like this will not be split
+                return [s]
+            else:
+                return list(split_by_uppercase(s.split(char)))
+    return list(split_by_uppercase(s))
+
+
+def split_by_uppercase(s: Union[str, List[str]]) -> Iterator[str]:
     """
     Split a string into words. Words are recognized by upper case letters, i.e.:
     test   -> [test]
     MyTest -> [My, Test]
     """
-    return [x.strip() for x in re.split(r"([^a-z][a-z]+)", s) if x]
+    if isinstance(s, str):
+        for x in re.split(r"([^a-z][a-z]+)", s):
+            if x:
+                yield x.strip()
+    else:
+        for word in s:
+            yield from split_by_uppercase(word)


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index 0dfe0966f..c9e3f4289 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@28c4deda893d5a96a6b2d958c5b47fc18d65c9d3
+        uses: ruby/setup-ruby@1287d2b408066abada82d5ad1c63652e758428d9
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.8
diff --git a/tests/test_secretsmanager/test_list_secrets.py b/tests/test_secretsmanager/test_list_secrets.py
index ba26bba7c..b5c297579 100644
--- a/tests/test_secretsmanager/test_list_secrets.py
+++ b/tests/test_secretsmanager/test_list_secrets.py
@@ -214,6 +214,115 @@ def test_with_all_filter():
         conn.delete_secret(SecretId=no_match, ForceDeleteWithoutRecovery=True)
 
 
+@aws_verified
+# Verified, but not marked because it's flaky - AWS can take up to 5 minutes before secrets are listed
+def test_with_all_filter_special_characters():
+    unique_name = str(uuid4())[0:6]
+    secret_with_slash = f"prod/AppBeta/{unique_name}"
+    secret_with_under = f"prod_AppBeta_{unique_name}"
+    secret_with_plus = f"prod+AppBeta+{unique_name}"
+    secret_with_equal = f"prod=AppBeta={unique_name}"
+    secret_with_dot = f"prod.AppBeta.{unique_name}"
+    secret_with_at = f"prod@AppBeta@{unique_name}"
+    secret_with_dash = f"prod-AppBeta-{unique_name}"
+    # Note that this secret is never found, because the pattern is unknown
+    secret_with_dash_and_slash = f"prod-AppBeta/{unique_name}"
+    full_uppercase = f"uat/COMPANY/{unique_name}"
+    partial_uppercase = f"uat/COMPANYthings/{unique_name}"
+
+    all_special_char_names = [
+        secret_with_slash,
+        secret_with_under,
+        secret_with_plus,
+        secret_with_equal,
+        secret_with_dot,
+        secret_with_at,
+        secret_with_dash,
+    ]
+
+    conn = boto_client()
+
+    conn.create_secret(Name=secret_with_slash, SecretString="s")
+    conn.create_secret(Name=secret_with_under, SecretString="s")
+    conn.create_secret(Name=secret_with_plus, SecretString="s")
+    conn.create_secret(Name=secret_with_equal, SecretString="s")
+    conn.create_secret(Name=secret_with_dot, SecretString="s")
+    conn.create_secret(Name=secret_with_at, SecretString="s")
+    conn.create_secret(Name=secret_with_dash, SecretString="s")
+    conn.create_secret(Name=full_uppercase, SecretString="s")
+    conn.create_secret(Name=partial_uppercase, SecretString="s")
+
+    try:
+        # Partial Match
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["AppBeta"]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert secret_names == all_special_char_names
+
+        # Partial Match
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["Beta"]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert secret_names == all_special_char_names
+
+        secrets = conn.list_secrets(
+            Filters=[{"Key": "all", "Values": ["AppBeta", "prod"]}]
+        )["SecretList"]
+        secret_names = [s["Name"] for s in secrets]
+        assert secret_names == all_special_char_names
+
+        # Search for special character itself
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["+"]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert not secret_names
+
+        # Search for unique postfix
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": [unique_name]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert secret_names == (
+            all_special_char_names + [full_uppercase, partial_uppercase]
+        )
+
+        # Search for unique postfix
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["company"]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert secret_names == [full_uppercase]
+
+        # This on it's own is not a word
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["things"]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert secret_names == []
+
+        # This is valid, because it's split as COMPAN + Ythings
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["Ythings"]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert secret_names == [partial_uppercase]
+
+        # Note that individual letters from COMPANY are not searchable,
+        # indicating that AWS splits by terms, rather than each individual upper case
+        # COMPANYThings --> COMPAN, YThings
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["N"]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert secret_names == []
+
+        #
+        secrets = conn.list_secrets(Filters=[{"Key": "all", "Values": ["pany"]}])
+        secret_names = [s["Name"] for s in secrets["SecretList"]]
+        assert secret_names == []
+
+    finally:
+        conn.delete_secret(SecretId=secret_with_slash, ForceDeleteWithoutRecovery=True)
+        conn.delete_secret(SecretId=secret_with_under, ForceDeleteWithoutRecovery=True)
+        conn.delete_secret(SecretId=secret_with_plus, ForceDeleteWithoutRecovery=True)
+        conn.delete_secret(SecretId=secret_with_equal, ForceDeleteWithoutRecovery=True)
+        conn.delete_secret(SecretId=secret_with_dot, ForceDeleteWithoutRecovery=True)
+        conn.delete_secret(SecretId=secret_with_dash, ForceDeleteWithoutRecovery=True)
+        conn.delete_secret(
+            SecretId=secret_with_dash_and_slash, ForceDeleteWithoutRecovery=True
+        )
+        conn.delete_secret(SecretId=secret_with_at, ForceDeleteWithoutRecovery=True)
+        conn.delete_secret(SecretId=full_uppercase, ForceDeleteWithoutRecovery=True)
+        conn.delete_secret(SecretId=partial_uppercase, ForceDeleteWithoutRecovery=True)
+
+
 @mock_aws
 def test_with_no_filter_key():
     conn = boto_client()
@@ -441,6 +550,7 @@ def test_with_include_planned_deleted_secrets():
         ("MyTestPhrase", ["My", "Test", "Phrase"]),
         ("myTest", ["my", "Test"]),
         ("my test", ["my", "test"]),
+        ("my/test", ["my", "test"]),
     ],
 )
 def test_word_splitter(input, output):