167 lines
6.3 KiB
Python
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)
|