ForcePilot/backend/test/unit/yuxi/channel/common/test_common.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

167 lines
6.3 KiB
Python

from __future__ import annotations
import base64
import hashlib
import hmac
import json
import time
from unittest.mock import AsyncMock, MagicMock
import httpx
import pytest
from yuxi.channel.common.crypto import (
hmac_sha256_base64,
hmac_sha256_sign,
sha1_sorted_sign,
verify_hmac_digest,
verify_timestamp,
)
from yuxi.channel.common.fake_request import FakeRequest
from yuxi.channel.common.http import ChannelHttpClient
from yuxi.channel.common.schemas import COMMON_DM_POLICY_SCHEMA, COMMON_GROUP_POLICY_SCHEMA
class TestCrypto:
def test_hmac_sha256_sign_with_str_and_bytes(self):
secret = "secret"
message = "message"
expected = hmac.new(secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).digest()
assert hmac_sha256_sign(secret, message) == expected
assert hmac_sha256_sign(secret.encode("utf-8"), message.encode("utf-8")) == expected
def test_hmac_sha256_base64(self):
signature = hmac_sha256_base64("secret", "message")
expected = base64.b64encode(hmac_sha256_sign("secret", "message")).decode("utf-8")
assert signature == expected
def test_verify_hmac_digest_with_valid_signature(self):
secret = "secret"
message = "message"
signature = hmac_sha256_sign(secret, message)
assert verify_hmac_digest(secret, message, signature) is True
def test_verify_hmac_digest_with_invalid_signature(self):
assert verify_hmac_digest("secret", "message", b"wrong") is False
def test_verify_hmac_digest_with_none_signature(self):
assert verify_hmac_digest("secret", "message", None) is False
def test_sha1_sorted_sign(self):
expected = hashlib.sha1("".join(sorted(["b", "a", "c"])).encode("utf-8")).hexdigest()
assert sha1_sorted_sign("a", "b", "c") == expected
def test_verify_timestamp_within_tolerance(self):
now = int(time.time())
assert verify_timestamp(now) is True
assert verify_timestamp(str(now)) is True
def test_verify_timestamp_outside_tolerance(self):
now = int(time.time())
assert verify_timestamp(now - 600) is False
assert verify_timestamp("invalid") is False
class TestChannelHttpClient:
async def test_internal_client_is_closed_on_exit(self):
client = ChannelHttpClient()
inner = client._client
assert client._own_client is True
async with client:
pass
assert inner.is_closed
async def test_external_client_is_not_closed_on_exit(self):
external = httpx.AsyncClient()
client = ChannelHttpClient(client=external)
assert client._own_client is False
async with client:
pass
assert not external.is_closed
await external.aclose()
async def test_request_json_returns_parsed_response(self):
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.json.return_value = {"ok": True}
mock_response.text = ""
mock_client.request = AsyncMock(return_value=mock_response)
client = ChannelHttpClient(client=mock_client)
result = await client.request_json("GET", "https://example.com/api")
assert result == {"ok": True}
mock_response.raise_for_status.assert_called_once()
async def test_request_json_raises_on_http_status_error(self):
mock_client = MagicMock()
mock_client.request = AsyncMock(
side_effect=httpx.HTTPStatusError(
"error", request=MagicMock(), response=MagicMock(status_code=500, text="boom")
)
)
client = ChannelHttpClient(client=mock_client)
with pytest.raises(httpx.HTTPStatusError):
await client.request_json("GET", "https://example.com/api")
async def test_request_json_raises_on_invalid_json(self):
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.json.side_effect = json.JSONDecodeError("boom", "not json", 0)
mock_response.text = "not json"
mock_client.request = AsyncMock(return_value=mock_response)
client = ChannelHttpClient(client=mock_client)
with pytest.raises(json.JSONDecodeError):
await client.request_json("GET", "https://example.com/api")
class TestFakeRequest:
async def test_fake_request_carries_config_and_body(self):
config = {"account_id": "acc"}
body = {"event": {"type": "message"}}
request = FakeRequest(config=config, body=body)
assert request.state.channel_config == config
assert request.state.channel_raw_body == body
assert await request.json() == body
assert await request.body() == json.dumps(body).encode("utf-8")
async def test_fake_request_uses_channel_raw_body_when_provided(self):
config = {"account_id": "acc"}
body = {"normalized": True}
raw = {"raw": True}
request = FakeRequest(config=config, body=body, channel_raw_body=raw)
assert await request.json() == body
assert request.state.channel_raw_body == raw
async def test_fake_request_defaults_empty_params(self):
request = FakeRequest(config={}, body={})
assert request.headers == {}
assert request.query_params == {}
assert request.path_params == {}
class TestSchemas:
@pytest.fixture(scope="class")
def validator(self):
jsonschema = pytest.importorskip("jsonschema")
return jsonschema.Draft202012Validator
def test_dm_policy_schema_allows_known_fields(self, validator):
instance = {"mode": "open", "allow_list": ["user1"], "deny_list": ["user2"]}
assert validator(COMMON_DM_POLICY_SCHEMA).is_valid(instance)
def test_dm_policy_schema_rejects_unknown_fields(self, validator):
instance = {"mode": "open", "unknown": True}
assert not validator(COMMON_DM_POLICY_SCHEMA).is_valid(instance)
def test_group_policy_schema_allows_known_fields(self, validator):
instance = {"require_mention": True, "allow_groups": ["g1"], "deny_groups": ["g2"]}
assert validator(COMMON_GROUP_POLICY_SCHEMA).is_valid(instance)
def test_group_policy_schema_rejects_unknown_fields(self, validator):
instance = {"require_mention": False, "unknown": True}
assert not validator(COMMON_GROUP_POLICY_SCHEMA).is_valid(instance)