Files

133 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""issue #150 本地账号认证单元测试(pytest / 纯标准库亦可 unittest 跑)。
覆盖:
- 密码哈希:hash/verify、恒定时间、盐随机(同密码两次哈希不同)
- UserStore:create/authenticate/角色校验/重复用户名/禁用账号/改密
- session:签发/校验/过期/篡改签名/伪造
- 守卫:require_auth 未登录 401、can_write readonly 403
"""
import os
import sys
import time
import unittest
# 让 tests 能 import core.auth(仓库根在 ../../.. )
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
from core.auth import (UserStore, hash_password, verify_password,
issue_token, parse_token, require_auth, can_write, AuthError)
from core.auth.users import User
class TestPasswordHash(unittest.TestCase):
def test_hash_then_verify(self):
h = hash_password("S3cretPwd!")
self.assertTrue(h.startswith("pbkdf2_sha256$"))
self.assertTrue(verify_password("S3cretPwd!", h))
self.assertFalse(verify_password("wrong", h))
def test_salt_random(self):
# 同一密码两次哈希应不同(盐随机)
self.assertNotEqual(hash_password("S3cretPwd!"), hash_password("S3cretPwd!"))
def test_tampered_store_rejected(self):
h = hash_password("S3cretPwd!")
# 篡改 hash 段
scheme, it, salt, _ = h.split("$")
self.assertFalse(verify_password("S3cretPwd!", "%s$%s$%s$AAAA" % (scheme, it, salt)))
def test_empty_password(self):
with self.assertRaises(ValueError):
hash_password("")
class TestUserStore(unittest.TestCase):
def setUp(self):
self.store = UserStore()
self.user = self.store.create("alice", "password1", role="engineer")
def test_authenticate_success(self):
u = self.store.authenticate("alice", "password1")
self.assertIsNotNone(u)
self.assertEqual(u.id, self.user.id)
self.assertIsNotNone(u.last_login_at)
def test_authenticate_wrong_password(self):
self.assertIsNone(self.store.authenticate("alice", "nope"))
def test_authenticate_unknown_user(self):
self.assertIsNone(self.store.authenticate("bob", "password1"))
def test_duplicate_username(self):
with self.assertRaises(ValueError):
self.store.create("alice", "password2")
def test_short_password(self):
with self.assertRaises(ValueError):
self.store.create("carol", "123")
def test_invalid_role(self):
with self.assertRaises(ValueError):
self.store.create("dave", "password1", role="superuser")
def test_deactivate_blocks_login(self):
self.store.set_active(self.user.id, False)
self.assertIsNone(self.store.authenticate("alice", "password1"))
def test_set_password(self):
self.store.set_password(self.user.id, "brand-new-pwd")
self.assertIsNone(self.store.authenticate("alice", "password1"))
self.assertIsNotNone(self.store.authenticate("alice", "brand-new-pwd"))
def test_set_role(self):
self.store.set_role(self.user.id, "admin")
self.assertEqual(self.store.get(self.user.id).role, "admin")
class TestSession(unittest.TestCase):
def test_issue_and_parse(self):
tok = issue_token(42)
sess = parse_token(tok)
self.assertIsNotNone(sess)
self.assertEqual(sess.user_id, 42)
def test_expired(self):
tok = issue_token(1, ttl=-1) # 已过期
self.assertIsNone(parse_token(tok))
def test_tampered_sig(self):
tok = issue_token(1)
uid, exp, sig = tok.split(".")
bad = ".".join([uid, exp, "A" * len(sig)])
self.assertIsNone(parse_token(bad))
def test_garbage(self):
self.assertIsNone(parse_token("not.a.token"))
self.assertIsNone(parse_token(""))
class TestGuards(unittest.TestCase):
def setUp(self):
self.store = UserStore()
self.admin = self.store.create("admin", "password1", role="admin")
self.viewer = self.store.create("viewer", "password1", role="readonly")
def test_require_auth_no_token(self):
with self.assertRaises(AuthError):
require_auth({}, self.store)
def test_can_write_viewer_forbidden(self):
with self.assertRaises(AuthError):
can_write(self.viewer)
def test_can_write_admin_ok(self):
can_write(self.admin) # 不抛即通过
if __name__ == "__main__":
unittest.main(verbosity=2)