Repository: TencentBlueKing/bk-user

Issue:
feat: add stringify_pydantic_error util



Discussion:

Resolving patch:
diff --git a/src/bk-user/bkuser/common/passwd/__init__.py b/src/bk-user/bkuser/common/passwd/__init__.py
--- a/src/bk-user/bkuser/common/passwd/__init__.py
+++ b/src/bk-user/bkuser/common/passwd/__init__.py
@@ -8,7 +8,7 @@
 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.
 """
-from .exceptions import PasswordStrengthError
+from .exceptions import PasswordGenerateError, PasswordStrengthError
 from .generator import PasswordGenerator
 from .models import PasswordRule, ValidateResult
 from .validator import PasswordValidator
@@ -24,4 +24,6 @@
     "ValidateResult",
     # 密码强度过低异常
     "PasswordStrengthError",
+    # 不合理的规则导致生成密码失败
+    "PasswordGenerateError",
 ]


Test patch:
diff --git a/src/bk-user/tests/utils/passwd/__init__.py b/src/bk-user/bkuser/utils/pydantic.py
similarity index 58%
rename from src/bk-user/tests/utils/passwd/__init__.py
rename to src/bk-user/bkuser/utils/pydantic.py
--- a/src/bk-user/tests/utils/passwd/__init__.py
+++ b/src/bk-user/bkuser/utils/pydantic.py
@@ -8,3 +8,21 @@
 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.
 """
+from pydantic import ValidationError
+
+
+def stringify_pydantic_error(exc: ValidationError) -> str:
+    """Transform a pydantic Exception object to a one-line string"""
+
+    err_msgs = []
+    for err in exc.errors():
+        # Note: 裁剪掉不必要的 `Value error, ` 前缀
+        msg = err["msg"].lstrip("Value error, ")
+
+        loc_msg = " -> ".join([str(i) for i in err["loc"]])
+        if loc_msg:
+            msg = f"{loc_msg}: {msg}"
+
+        err_msgs.append(msg)
+
+    return ", ".join(err_msgs)
diff --git a/src/bk-user/tests/common/passwd/test_generator.py b/src/bk-user/tests/common/passwd/test_generator.py
--- a/src/bk-user/tests/common/passwd/test_generator.py
+++ b/src/bk-user/tests/common/passwd/test_generator.py
@@ -8,7 +8,8 @@
 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.
 """
-from bkuser.common.passwd import PasswordGenerator
+import pytest
+from bkuser.common.passwd import PasswordGenerateError, PasswordGenerator
 
 
 class TestPasswordGenerator:
@@ -23,3 +24,11 @@ def test_generate_with_simple_passwd_rule(self, simple_passwd_rule):
         # PasswordGenerator 内置 PasswordValidator，因此此处检查确实生成了密码即可
         password = PasswordGenerator(simple_passwd_rule).generate()
         assert password != ""
+
+    def test_generate_with_bad_passwd_rule(self, strict_passwd_rule):
+        """限制只使用六位的密码，但是有诸多的限制，导致无法生成可行的密码"""
+        strict_passwd_rule.min_length = 6
+        strict_passwd_rule.max_length = 6
+
+        with pytest.raises(PasswordGenerateError):
+            PasswordGenerator(strict_passwd_rule).generate()
diff --git a/src/bk-user/tests/utils/test_pydantic.py b/src/bk-user/tests/utils/test_pydantic.py
new file mode 100644
--- /dev/null
+++ b/src/bk-user/tests/utils/test_pydantic.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+"""
+TencentBlueKing is pleased to support the open source community by making 蓝鲸智云-用户管理(Bk-User) available.
+Copyright (C) 2017-2021 THL A29 Limited, a Tencent company. All rights reserved.
+Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
+You may obtain a copy of the License at http://opensource.org/licenses/MIT
+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 bkuser.utils.pydantic import stringify_pydantic_error
+from pydantic import BaseModel, Field, ValidationError, model_validator
+
+
+@pytest.mark.parametrize(
+    ("foo", "bar", "err_msg"),
+    [
+        (9, 10, "foo: Input should be greater than 10, bar: Input should be less than 10"),
+        (11, 10, "bar: Input should be less than 10"),
+        # 由于 Field 的校验提前异常，不会触发 validate_attrs 的校验
+        (12, 10, "bar: Input should be less than 10"),
+        (12, 8, "foo can't be even"),
+        (11, 9, "bar can't be odd"),
+    ],
+)
+def test_stringify_pydantic_error(foo, bar, err_msg):
+    class TModel(BaseModel):
+        foo: int = Field(gt=10)
+        bar: int = Field(lt=10)
+
+        @model_validator(mode="after")
+        def validate_attrs(self):
+            if not self.foo & 1:
+                raise ValueError("foo can't be even")
+
+            if self.bar & 1:
+                raise ValueError("bar can't be odd")
+
+            return self
+
+    try:
+        TModel(foo=foo, bar=bar)
+    except ValidationError as e:
+        assert stringify_pydantic_error(e) == err_msg  # noqa: PT017