128 lines
4.7 KiB
Python
128 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import os
|
|
from datetime import timedelta
|
|
|
|
import jwt
|
|
import pytest
|
|
from yuxi.utils.datetime_utils import utc_now
|
|
|
|
from yuxi.utils.auth_utils import JWT_ALGORITHM, JWT_AUDIENCE, AuthUtils
|
|
|
|
|
|
def test_generate_api_key_returns_secret_hash_and_prefix():
|
|
full_key, key_hash, key_prefix = AuthUtils.generate_api_key()
|
|
|
|
assert full_key.startswith("yxkey_")
|
|
assert len(full_key) == len("yxkey_") + 48
|
|
assert key_prefix == full_key[:12]
|
|
assert key_hash == hashlib.sha256(full_key.encode()).hexdigest()
|
|
|
|
|
|
def test_hash_password_uses_argon2():
|
|
hashed = AuthUtils.hash_password("secret-password")
|
|
|
|
assert hashed.startswith("$argon2")
|
|
assert AuthUtils.verify_password(hashed, "secret-password") is True
|
|
assert AuthUtils.verify_password(hashed, "wrong-password") is False
|
|
|
|
|
|
def test_access_token_contains_instance_claims(monkeypatch):
|
|
monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness")
|
|
monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance")
|
|
|
|
token = AuthUtils.create_access_token({"sub": "1"})
|
|
payload = AuthUtils.verify_access_token(token)
|
|
|
|
assert payload["sub"] == "1"
|
|
assert payload["iss"] == "yuxi-know:pytest-instance"
|
|
assert payload["aud"] == JWT_AUDIENCE
|
|
|
|
|
|
def test_access_token_auto_generates_dev_secret(monkeypatch):
|
|
monkeypatch.setenv("YUXI_ENV", "development")
|
|
monkeypatch.delenv("JWT_SECRET_KEY", raising=False)
|
|
monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance")
|
|
|
|
token = AuthUtils.create_access_token({"sub": "1"})
|
|
|
|
assert AuthUtils.verify_access_token(token)["sub"] == "1"
|
|
assert len(os.environ["JWT_SECRET_KEY"]) == 64
|
|
|
|
|
|
def test_access_token_requires_configured_secret_in_production(monkeypatch):
|
|
monkeypatch.setenv("YUXI_ENV", "production")
|
|
monkeypatch.delenv("JWT_SECRET_KEY", raising=False)
|
|
monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance")
|
|
|
|
with pytest.raises(ValueError, match="JWT_SECRET_KEY"):
|
|
AuthUtils.create_access_token({"sub": "1"})
|
|
|
|
|
|
def test_access_token_rejects_public_default_secret_in_production(monkeypatch):
|
|
monkeypatch.setenv("YUXI_ENV", "production")
|
|
monkeypatch.setenv("JWT_SECRET_KEY", "yuxi_know_secure_key")
|
|
monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance")
|
|
|
|
with pytest.raises(ValueError, match="公开默认密钥"):
|
|
AuthUtils.create_access_token({"sub": "1"})
|
|
|
|
|
|
def test_access_token_auto_generates_dev_instance_id(monkeypatch):
|
|
monkeypatch.setenv("YUXI_ENV", "development")
|
|
monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness")
|
|
monkeypatch.delenv("YUXI_INSTANCE_ID", raising=False)
|
|
|
|
token = AuthUtils.create_access_token({"sub": "1"})
|
|
|
|
assert AuthUtils.verify_access_token(token)["iss"].startswith("yuxi-know:instance-")
|
|
assert os.environ["YUXI_INSTANCE_ID"].startswith("instance-")
|
|
|
|
|
|
def test_access_token_requires_instance_id_in_production(monkeypatch):
|
|
monkeypatch.setenv("YUXI_ENV", "production")
|
|
monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness")
|
|
monkeypatch.delenv("YUXI_INSTANCE_ID", raising=False)
|
|
|
|
with pytest.raises(ValueError, match="YUXI_INSTANCE_ID"):
|
|
AuthUtils.create_access_token({"sub": "1"})
|
|
|
|
|
|
def test_verify_access_token_rejects_wrong_issuer(monkeypatch):
|
|
monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness")
|
|
monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance")
|
|
token = jwt.encode(
|
|
{"sub": "1", "exp": utc_now() + timedelta(minutes=5), "iss": "yuxi-know:other", "aud": JWT_AUDIENCE},
|
|
"test-secret-key-with-enough-randomness",
|
|
algorithm=JWT_ALGORITHM,
|
|
)
|
|
|
|
assert AuthUtils.decode_token(token) is None
|
|
|
|
|
|
def test_verify_access_token_rejects_wrong_audience(monkeypatch):
|
|
monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness")
|
|
monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance")
|
|
token = jwt.encode(
|
|
{"sub": "1", "exp": utc_now() + timedelta(minutes=5), "iss": "yuxi-know:pytest-instance", "aud": "other-api"},
|
|
"test-secret-key-with-enough-randomness",
|
|
algorithm=JWT_ALGORITHM,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="无效的令牌"):
|
|
AuthUtils.verify_access_token(token)
|
|
|
|
|
|
def test_verify_access_token_requires_claims(monkeypatch):
|
|
monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-key-with-enough-randomness")
|
|
monkeypatch.setenv("YUXI_INSTANCE_ID", "pytest-instance")
|
|
token = jwt.encode(
|
|
{"sub": "1", "exp": utc_now() + timedelta(minutes=5)},
|
|
"test-secret-key-with-enough-randomness",
|
|
algorithm=JWT_ALGORITHM,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="无效的令牌"):
|
|
AuthUtils.verify_access_token(token)
|