From 5eb1213041f8fedb5cfd153090fa04e3c47973e1 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 30 Jul 2026 00:42:44 +0800 Subject: [PATCH 1/2] =?UTF-8?q?test(wave208):=20=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E5=93=88=E5=B8=8C=E4=B8=8E=E9=AA=8C=E8=AF=81=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=8D=95=E6=B5=8B=20+50=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PasswordValidator: 24测(长度/大小写/数字/特殊字符/各种配置组合) - PasswordHasher: 18测(哈希/验证/needs_rehash/rounds边界) - PasswordHandler: 8测(委托层/全局实例配置) --- tests/unit/test_password_auth.py | 318 +++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100755 tests/unit/test_password_auth.py diff --git a/tests/unit/test_password_auth.py b/tests/unit/test_password_auth.py new file mode 100755 index 000000000..fc398f945 --- /dev/null +++ b/tests/unit/test_password_auth.py @@ -0,0 +1,318 @@ +"""密码哈希与验证模块单元测试.""" + +import pytest + +from packages.application.auth.password_hasher import ( + PasswordHasher, + PasswordValidator, +) +from packages.application.auth.password_handler import ( + PasswordHandler, + configure_password_handler, + get_password_handler, +) + +# ==================== PasswordValidator ==================== + + +class TestPasswordValidator: + @pytest.fixture + def default_validator(self): + return PasswordValidator() + + @pytest.fixture + def strict_validator(self): + return PasswordValidator( + min_length=12, + require_uppercase=True, + require_lowercase=True, + require_digit=True, + require_special=True, + ) + + class TestBasicValidation: + def test_valid_password(self, default_validator): + ok, msg = default_validator.validate("SecurePass123") + assert ok is True + assert msg is None + + def test_empty_password(self, default_validator): + ok, msg = default_validator.validate("") + assert ok is False + assert "empty" in msg.lower() + + def test_none_password_treated_as_empty(self, default_validator): + # None被not判定为falsy,返回空密码错误 + ok, msg = default_validator.validate(None) + assert ok is False + assert "empty" in msg.lower() + + class TestMinLength: + def test_too_short(self, default_validator): + ok, msg = default_validator.validate("Ab1") + assert ok is False + assert "8" in msg + + def test_exact_min_length(self, default_validator): + # 刚好8个字符 + ok, _ = default_validator.validate("Abcdefg1") + assert ok is True + + def test_custom_min_length(self, strict_validator): + ok, msg = strict_validator.validate("Short1!") + assert ok is False + assert "12" in msg + + class TestUppercase: + def test_no_uppercase(self, default_validator): + ok, msg = default_validator.validate("password123") + assert ok is False + assert "uppercase" in msg.lower() + + def test_with_uppercase(self, default_validator): + ok, _ = default_validator.validate("Password123") + assert ok is True + + def test_disabled_requirement(self): + v = PasswordValidator(require_uppercase=False) + ok, _ = v.validate("password123") + assert ok is True + + class TestLowercase: + def test_no_lowercase(self, default_validator): + ok, msg = default_validator.validate("PASSWORD123") + assert ok is False + assert "lowercase" in msg.lower() + + def test_with_lowercase(self, default_validator): + ok, _ = default_validator.validate("Password123") + assert ok is True + + def test_disabled_requirement(self): + v = PasswordValidator(require_lowercase=False) + ok, _ = v.validate("PASSWORD123") + assert ok is True + + class TestDigit: + def test_no_digit(self, default_validator): + ok, msg = default_validator.validate("Passworddd") + assert ok is False + assert "digit" in msg.lower() + + def test_with_digit(self, default_validator): + ok, _ = default_validator.validate("Password1") + assert ok is True + + def test_disabled_requirement(self): + v = PasswordValidator(require_digit=False) + ok, _ = v.validate("Passworddd") + assert ok is True + + class TestSpecialChar: + def test_no_special_when_not_required(self, default_validator): + ok, _ = default_validator.validate("Password123") + assert ok is True + + def test_no_special_when_required(self, strict_validator): + ok, msg = strict_validator.validate("Password1234") + assert ok is False + assert "special" in msg.lower() + + def test_with_special(self, strict_validator): + ok, _ = strict_validator.validate("Password123!") + assert ok is True + + def test_various_special_chars(self, strict_validator): + for char in "!@#$%^&*()_+-=[]{}|;:,.<>?~": + pw = f"LongPassword1{char}" # 13字符,含大小写数字特殊 + ok, msg = strict_validator.validate(pw) + assert ok is True, f"special char {char} should be valid: {msg}" + + class TestAllDisabled: + def test_all_disabled_min_length_only(self): + v = PasswordValidator( + min_length=1, + require_uppercase=False, + require_lowercase=False, + require_digit=False, + require_special=False, + ) + ok, _ = v.validate("a") + assert ok is True + + def test_all_disabled_empty_still_fails(self): + v = PasswordValidator( + min_length=1, + require_uppercase=False, + require_lowercase=False, + require_digit=False, + require_special=False, + ) + ok, _ = v.validate("") + assert ok is False + + +# ==================== PasswordHasher ==================== + + +class TestPasswordHasher: + @pytest.fixture + def hasher(self): + return PasswordHasher(rounds=4) # 用最低rounds加速测试 + + class TestHashPassword: + def test_hash_returns_string(self, hasher): + result = hasher.hash_password("testpassword") + assert isinstance(result, str) + assert len(result) > 0 + + def test_hash_starts_with_bcrypt_prefix(self, hasher): + result = hasher.hash_password("testpassword") + assert result.startswith("$2") + + def test_hash_contains_rounds(self, hasher): + result = hasher.hash_password("testpassword") + parts = result.split("$") + assert parts[2] == "04" # bcrypt rounds格式是两位数 + + def test_hash_different_salts(self, hasher): + # 同一密码两次哈希结果不同(因为salt随机) + h1 = hasher.hash_password("samepassword") + h2 = hasher.hash_password("samepassword") + assert h1 != h2 + + def test_hash_empty_password_raises(self, hasher): + with pytest.raises(ValueError, match="empty"): + hasher.hash_password("") + + def test_hash_unicode_password(self, hasher): + result = hasher.hash_password("密码Pass123!") + assert isinstance(result, str) + assert len(result) > 0 + + def test_hash_long_password(self, hasher): + long_pw = "a" * 72 # bcrypt最大72字节 + result = hasher.hash_password(long_pw) + assert isinstance(result, str) + + class TestVerifyPassword: + def test_verify_correct_password(self, hasher): + hashed = hasher.hash_password("CorrectPass123") + assert hasher.verify_password("CorrectPass123", hashed) is True + + def test_verify_wrong_password(self, hasher): + hashed = hasher.hash_password("CorrectPass123") + assert hasher.verify_password("WrongPass123", hashed) is False + + def test_verify_empty_password(self, hasher): + hashed = hasher.hash_password("testpass") + assert hasher.verify_password("", hashed) is False + + def test_verify_empty_hash(self, hasher): + assert hasher.verify_password("testpass", "") is False + + def test_verify_invalid_hash_format(self, hasher): + assert hasher.verify_password("testpass", "invalid-hash-format") is False + + def test_verify_none_hash(self, hasher): + assert hasher.verify_password("testpass", None) is False + + def test_verify_unicode_password(self, hasher): + pw = "密码Pass123!" + hashed = hasher.hash_password(pw) + assert hasher.verify_password(pw, hashed) is True + + class TestNeedsRehash: + def test_same_rounds_no_rehash(self, hasher): + hashed = hasher.hash_password("testpass") + assert hasher.needs_rehash(hashed) is False + + def test_lower_rounds_needs_rehash(self): + hasher_low = PasswordHasher(rounds=4) + hashed = hasher_low.hash_password("testpass") + + hasher_high = PasswordHasher(rounds=5) + assert hasher_high.needs_rehash(hashed) is True + + def test_higher_rounds_needs_rehash(self): + hasher_high = PasswordHasher(rounds=5) + hashed = hasher_high.hash_password("testpass") + + hasher_low = PasswordHasher(rounds=4) + assert hasher_low.needs_rehash(hashed) is True + + def test_invalid_hash_no_rehash(self, hasher): + assert hasher.needs_rehash("invalid-format") is False + + def test_empty_hash_no_rehash(self, hasher): + assert hasher.needs_rehash("") is False + + class TestInit: + def test_rounds_below_min_raises(self): + with pytest.raises(ValueError): + PasswordHasher(rounds=3) + + def test_rounds_above_max_raises(self): + with pytest.raises(ValueError): + PasswordHasher(rounds=32) + + def test_min_rounds_ok(self): + h = PasswordHasher(rounds=4) + assert h.rounds == 4 + + def test_max_rounds_ok(self): + h = PasswordHasher(rounds=31) + assert h.rounds == 31 + + +# ==================== PasswordHandler ==================== + + +class TestPasswordHandler: + @pytest.fixture + def handler(self): + return PasswordHandler(rounds=4) + + def test_hash_and_verify_roundtrip(self, handler): + hashed = handler.hash_password("MySecurePass123") + assert handler.verify_password("MySecurePass123", hashed) is True + assert handler.verify_password("WrongPass", hashed) is False + + def test_needs_rehash(self, handler): + # 用当前rounds哈希,不需要rehash + hashed = handler.hash_password("testpass") + assert handler.needs_rehash(hashed) is False + + def test_validate_strength(self, handler): + # 强密码通过 + ok, msg = handler.validate_strength("StrongPass123") + assert ok is True + assert msg is None + + # 弱密码不通过 + ok, msg = handler.validate_strength("weak") + assert ok is False + assert msg is not None + + def test_hash_empty_raises(self, handler): + with pytest.raises(ValueError): + handler.hash_password("") + + +class TestGlobalHandler: + def test_get_password_handler_returns_instance(self): + # 重置全局实例 + import packages.application.auth.password_handler as ph + + ph._default_handler = None + + handler = get_password_handler() + assert isinstance(handler, PasswordHandler) + + def test_configure_password_handler(self): + handler = configure_password_handler(rounds=4) + assert isinstance(handler, PasswordHandler) + + # get应该返回同一个配置好的实例 + same_handler = get_password_handler() + assert same_handler is handler -- 2.54.0 From 70c8bb7022159522fafd704833bd543ea926d0ac Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 29 Jul 2026 16:46:35 +0000 Subject: [PATCH 2/2] style: auto-format with black + isort + prettier [skip ci-format-check] --- tests/unit/test_password_auth.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_password_auth.py b/tests/unit/test_password_auth.py index fc398f945..9c8e0b969 100755 --- a/tests/unit/test_password_auth.py +++ b/tests/unit/test_password_auth.py @@ -2,15 +2,15 @@ import pytest -from packages.application.auth.password_hasher import ( - PasswordHasher, - PasswordValidator, -) from packages.application.auth.password_handler import ( PasswordHandler, configure_password_handler, get_password_handler, ) +from packages.application.auth.password_hasher import ( + PasswordHasher, + PasswordValidator, +) # ==================== PasswordValidator ==================== -- 2.54.0