Files
2026-05-24 21:53:04 +03:00

200 lines
6.7 KiB
Python

"""测试加密模块 (crypto.py)。
覆盖:RC4、nonce、签名、加密/解密往返。
"""
from __future__ import annotations
import base64
import json
from mi_fitness.crypto import (
_build_sig_message,
_rc4_crypt,
_sha1_b64,
build_encrypted_params,
compute_signed_nonce,
decrypt_data,
decrypt_response,
encrypt_data,
generate_nonce,
)
class TestRC4:
"""RC4 加密/解密测试。"""
def test_encrypt_decrypt_roundtrip(self) -> None:
"""加密后解密应还原明文。"""
key = b"test_key_123456"
plaintext = b"Hello, MiSDK!"
encrypted = _rc4_crypt(key, plaintext)
decrypted = _rc4_crypt(key, encrypted)
assert decrypted == plaintext
def test_different_keys_produce_different_output(self) -> None:
"""不同密钥应产生不同密文。"""
data = b"same data"
enc1 = _rc4_crypt(b"key_aaa", data)
enc2 = _rc4_crypt(b"key_bbb", data)
assert enc1 != enc2
def test_empty_data(self) -> None:
"""空数据加密应返回空。"""
result = _rc4_crypt(b"key", b"")
assert result == b""
def test_skip_parameter(self) -> None:
"""skip=0 和 skip=1024 应产生不同结果。"""
key = b"test"
data = b"data"
r1 = _rc4_crypt(key, data, skip=0)
r2 = _rc4_crypt(key, data, skip=1024)
assert r1 != r2
def test_large_data(self) -> None:
"""大数据块也能正确往返。"""
key = b"large_key"
data = b"x" * 100_000
assert _rc4_crypt(key, _rc4_crypt(key, data)) == data
class TestNonce:
"""nonce 生成测试。"""
def test_nonce_is_base64(self) -> None:
"""nonce 应为有效的 base64 字符串。"""
nonce = generate_nonce()
decoded = base64.b64decode(nonce)
assert len(decoded) == 12 # 8 random + 4 time
def test_nonce_unique(self) -> None:
"""连续生成的 nonce 应不同(随机部分不同)。"""
nonces = {generate_nonce() for _ in range(10)}
assert len(nonces) == 10
class TestSignedNonce:
"""compute_signed_nonce 测试。"""
def test_deterministic(self, ssecurity: str) -> None:
"""相同输入应产生相同结果。"""
nonce = generate_nonce()
r1 = compute_signed_nonce(ssecurity, nonce)
r2 = compute_signed_nonce(ssecurity, nonce)
assert r1 == r2
def test_output_is_base64(self, ssecurity: str) -> None:
"""输出应为有效 base64。"""
nonce = generate_nonce()
snonce = compute_signed_nonce(ssecurity, nonce)
decoded = base64.b64decode(snonce)
assert len(decoded) == 32 # SHA256
class TestSignature:
"""签名生成测试(纯 SHA1 + Base64)。"""
def test_sha1_b64_output(self) -> None:
"""SHA1 base64 输出应为 28 字符。"""
result = _sha1_b64("test message")
decoded = base64.b64decode(result)
assert len(decoded) == 20 # SHA1
def test_sig_msg_format(self) -> None:
"""签名消息应为 METHOD&/path&k=v&signedNonce 格式。"""
msg = _build_sig_message("GET", "/test/path", {"data": "hello"}, "snonce123")
assert msg == "GET&/test/path&data=hello&snonce123"
def test_sig_msg_method_uppercase(self) -> None:
"""方法名应转为大写。"""
msg = _build_sig_message("get", "/path", {}, "sn")
assert msg.startswith("GET&")
def test_sig_msg_adds_leading_slash(self) -> None:
"""路径没有前导 / 时应自动添加。"""
msg = _build_sig_message("GET", "path", {}, "sn")
assert "&/path&" in msg
def test_sig_msg_sorted_params(self) -> None:
"""参数应按 key 字典序排序。"""
msg = _build_sig_message("GET", "/p", {"z": "1", "a": "2"}, "sn")
assert msg == "GET&/p&a=2&z=1&sn"
class TestEncryptDecryptData:
"""encrypt_data / decrypt_data 测试。"""
def test_roundtrip(self, ssecurity: str) -> None:
"""加密后解密应还原。"""
nonce = generate_nonce()
snonce = compute_signed_nonce(ssecurity, nonce)
plaintext = '{"key": "value", "中文": "测试"}'
encrypted = encrypt_data(snonce, plaintext)
decrypted = decrypt_data(snonce, encrypted)
assert decrypted == plaintext
def test_encrypted_is_base64(self, ssecurity: str) -> None:
"""密文应为 base64 编码。"""
nonce = generate_nonce()
snonce = compute_signed_nonce(ssecurity, nonce)
encrypted = encrypt_data(snonce, "test")
base64.b64decode(encrypted) # 不抛异常即可
class TestBuildEncryptedParams:
"""build_encrypted_params 集成测试。"""
def test_has_required_keys(self, ssecurity: str) -> None:
"""返回应包含 data, _nonce, signature, rc4_hash__。"""
result = build_encrypted_params(
"GET",
"/app/v1/relatives/get_relative_list",
ssecurity,
{"relative_uid": 123},
)
assert "data" in result
assert "_nonce" in result
assert "signature" in result
assert "rc4_hash__" in result
def test_no_params_no_data_key(self, ssecurity: str) -> None:
"""无参数时不应有 data 字段。"""
result = build_encrypted_params(
"GET",
"/app/v1/relatives/get_relative_list",
ssecurity,
)
assert "data" not in result
assert "_nonce" in result
def test_can_decrypt_own_params(self, ssecurity: str) -> None:
"""能解密自己加密的参数。"""
params = {"test": "hello", "num": 42}
result = build_encrypted_params("POST", "/test", ssecurity, params)
snonce = compute_signed_nonce(ssecurity, result["_nonce"])
decrypted = decrypt_data(snonce, result["data"])
parsed = json.loads(decrypted)
assert parsed == params
class TestDecryptResponse:
"""decrypt_response 集成测试。"""
def test_roundtrip(self, ssecurity: str) -> None:
"""构造加密响应并解密。"""
nonce = generate_nonce()
snonce = compute_signed_nonce(ssecurity, nonce)
original = {"code": 0, "result": {"data": "test"}}
encrypted = encrypt_data(snonce, json.dumps(original))
decrypted = decrypt_response(ssecurity, nonce, encrypted)
assert decrypted == original
def test_non_json_returns_string(self, ssecurity: str) -> None:
"""非 JSON 响应应返回字符串。"""
nonce = generate_nonce()
snonce = compute_signed_nonce(ssecurity, nonce)
encrypted = encrypt_data(snonce, "not json at all")
result = decrypt_response(ssecurity, nonce, encrypted)
assert result == "not json at all"