2026-04-02 16:03:01 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-05-04 09:33:47 +08:00
|
|
|
import os
|
|
|
|
|
from datetime import timedelta
|
2026-04-02 16:03:01 +08:00
|
|
|
|
2026-05-04 09:33:47 +08:00
|
|
|
import jwt
|
|
|
|
|
import pytest
|
|
|
|
|
from yuxi.utils.datetime_utils import utc_now
|
|
|
|
|
|
|
|
|
|
from server.utils.auth_utils import JWT_ALGORITHM, JWT_AUDIENCE, LEGACY_JWT_SECRET_KEY, AuthUtils
|
2026-04-02 16:03:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-05-04 09:33:47 +08:00
|
|
|
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_requires_non_default_secret(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("JWT_SECRET_KEY", LEGACY_JWT_SECRET_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)
|