集成家长短信通知
This commit is contained in:
130
talkingq-url/tests/test_parent_phone_authorization.py
Normal file
130
talkingq-url/tests/test_parent_phone_authorization.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from banban.service.parent import ParentService
|
||||
from banban.service.wechat_login import WechatAuthService
|
||||
|
||||
|
||||
class FakeSession:
|
||||
async def close(self):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FakeWechatPhone:
|
||||
phone_number: str
|
||||
pure_phone_number: str
|
||||
country_code: str | None = None
|
||||
|
||||
|
||||
class FakeWechatAuthService:
|
||||
def __init__(self):
|
||||
self.codes = []
|
||||
|
||||
async def exchange_phone_code(self, code):
|
||||
self.codes.append(code)
|
||||
return FakeWechatPhone(
|
||||
phone_number="+8613800138000",
|
||||
pure_phone_number="13800138000",
|
||||
country_code="86",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_phone_from_wechat_code_persists_current_parent_phone(monkeypatch):
|
||||
service = ParentService()
|
||||
wechat_auth_service = FakeWechatAuthService()
|
||||
updates = []
|
||||
|
||||
async def fake_get_session():
|
||||
return FakeSession()
|
||||
|
||||
async def fake_update(self, user_id, nickname=None, avatar_url=None, phone=None):
|
||||
updates.append(
|
||||
{
|
||||
"user_id": user_id,
|
||||
"nickname": nickname,
|
||||
"avatar_url": avatar_url,
|
||||
"phone": phone,
|
||||
}
|
||||
)
|
||||
|
||||
async def fake_get(user_id):
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"openid": "openid_demo",
|
||||
"unionid": None,
|
||||
"nickname": "家长",
|
||||
"avatar_url": None,
|
||||
"phone": "13800138000",
|
||||
"status": 1,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fake_get_session)
|
||||
monkeypatch.setattr("banban.service.parent.ParentDAO.update", fake_update)
|
||||
monkeypatch.setattr(service, "get", fake_get)
|
||||
|
||||
parent = await service.update_phone_from_wechat_code(
|
||||
user_id=9,
|
||||
code="phone-code-demo",
|
||||
wechat_auth_service=wechat_auth_service,
|
||||
)
|
||||
|
||||
assert wechat_auth_service.codes == ["phone-code-demo"]
|
||||
assert updates == [
|
||||
{
|
||||
"user_id": 9,
|
||||
"nickname": None,
|
||||
"avatar_url": None,
|
||||
"phone": "13800138000",
|
||||
}
|
||||
]
|
||||
assert parent["phone"] == "13800138000"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exchange_phone_code_reuses_cached_access_token(monkeypatch):
|
||||
requests = []
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
requests.append((request.method, request.url.path, str(request.url.params)))
|
||||
if request.url.path == "/cgi-bin/token":
|
||||
return httpx.Response(200, json={"access_token": "wechat-access-token", "expires_in": 7200})
|
||||
if request.url.path == "/wxa/business/getuserphonenumber":
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"errcode": 0,
|
||||
"phone_info": {
|
||||
"phoneNumber": "+8613800138000",
|
||||
"purePhoneNumber": "13800138000",
|
||||
"countryCode": "86",
|
||||
},
|
||||
},
|
||||
)
|
||||
return httpx.Response(404, json={"errcode": 404})
|
||||
|
||||
monkeypatch.setattr(WechatAuthService, "_cached_access_token", None)
|
||||
monkeypatch.setattr(WechatAuthService, "_cached_access_token_expires_at", 0)
|
||||
monkeypatch.setattr("banban.service.wechat_login.settings.wechat_app_id", "wechat-app-id")
|
||||
monkeypatch.setattr("banban.service.wechat_login.settings.wechat_app_secret", "wechat-app-secret")
|
||||
monkeypatch.setattr("banban.service.wechat_login.settings.wechat_api_base_url", "https://api.weixin.qq.com")
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="https://api.weixin.qq.com") as client:
|
||||
uncached_service = WechatAuthService()
|
||||
uncached_service._request_access_token = WechatAuthService(client)._request_access_token
|
||||
uncached_service._request_phone_number = WechatAuthService(client)._request_phone_number
|
||||
|
||||
first = await uncached_service.exchange_phone_code("phone-code-1")
|
||||
second = await uncached_service.exchange_phone_code("phone-code-2")
|
||||
|
||||
assert first.pure_phone_number == "13800138000"
|
||||
assert second.pure_phone_number == "13800138000"
|
||||
token_requests = [item for item in requests if item[1] == "/cgi-bin/token"]
|
||||
phone_requests = [item for item in requests if item[1] == "/wxa/business/getuserphonenumber"]
|
||||
assert len(token_requests) == 1
|
||||
assert len(phone_requests) == 2
|
||||
assert all("access_token=wechat-access-token" in item[2] for item in phone_requests)
|
||||
136
talkingq-url/tests/test_sms_notification.py
Normal file
136
talkingq-url/tests/test_sms_notification.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import pytest
|
||||
|
||||
from banban.service.sms_notification import SmsNotificationService
|
||||
|
||||
|
||||
class FakeSession:
|
||||
async def execute(self, statement, params):
|
||||
del statement, params
|
||||
return FakeResult()
|
||||
|
||||
async def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeResult:
|
||||
def mappings(self):
|
||||
return self
|
||||
|
||||
def all(self):
|
||||
return [
|
||||
{"phone": "13800138000"},
|
||||
{"phone": "13800138000"},
|
||||
{"phone": ""},
|
||||
{"phone": "13900139000"},
|
||||
]
|
||||
|
||||
def first(self):
|
||||
return {
|
||||
"child_name": "孩子",
|
||||
"address": "杭州市",
|
||||
"lat": None,
|
||||
"lng": None,
|
||||
}
|
||||
|
||||
|
||||
def configure_enabled_sms(monkeypatch):
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.sms_enabled", True)
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.sms_provider", "aliyun")
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.sms_dry_run", False)
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.sms_alarm_dedup_seconds", 300)
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.sms_leave_message_dedup_seconds", 60)
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.aliyun_sms_access_key_id", "access-key-id")
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.aliyun_sms_access_key_secret", "access-key-secret")
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.aliyun_sms_sign_name", "短信签名")
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.aliyun_sms_template_code", "SMS_507185027")
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.aliyun_sms_alarm_template_code", "")
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.aliyun_sms_leave_message_template_code", "")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notify_alarm_sends_to_unique_family_phone_numbers(monkeypatch):
|
||||
configure_enabled_sms(monkeypatch)
|
||||
service = SmsNotificationService()
|
||||
sent = []
|
||||
|
||||
async def fake_get_session():
|
||||
return FakeSession()
|
||||
|
||||
async def fake_send_sms(*, phone_number, template_code, template_params):
|
||||
sent.append(
|
||||
{
|
||||
"phone_number": phone_number,
|
||||
"template_code": template_code,
|
||||
"template_params": template_params,
|
||||
}
|
||||
)
|
||||
return {"ok": True, "code": "OK", "message": "OK", "request_id": "request-id"}
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fake_get_session)
|
||||
monkeypatch.setattr(service, "_send_sms", fake_send_sms)
|
||||
|
||||
await service.notify_alarm(
|
||||
device_id="TalkingQ_XQSN00001005",
|
||||
alarm_id=7,
|
||||
child_name="孩子",
|
||||
address="杭州市",
|
||||
)
|
||||
|
||||
assert [item["phone_number"] for item in sent] == ["13800138000", "13900139000"]
|
||||
assert all(item["template_code"] == "SMS_507185027" for item in sent)
|
||||
assert sent[0]["template_params"]["conference"] == "孩子"
|
||||
assert sent[0]["template_params"]["address"] == "杭州市"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notify_alarm_dedupes_same_device_and_phone(monkeypatch):
|
||||
configure_enabled_sms(monkeypatch)
|
||||
service = SmsNotificationService()
|
||||
sent = []
|
||||
|
||||
async def fake_get_session():
|
||||
return FakeSession()
|
||||
|
||||
async def fake_send_sms(*, phone_number, template_code, template_params):
|
||||
del template_code, template_params
|
||||
sent.append(phone_number)
|
||||
return {"ok": True, "code": "OK", "message": "OK", "request_id": "request-id"}
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fake_get_session)
|
||||
monkeypatch.setattr(service, "_send_sms", fake_send_sms)
|
||||
|
||||
await service.notify_alarm(device_id="TalkingQ_XQSN00001005", alarm_id=7)
|
||||
await service.notify_alarm(device_id="TalkingQ_XQSN00001005", alarm_id=8)
|
||||
|
||||
assert sent == ["13800138000", "13900139000"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notify_leave_message_disabled_skips_database_and_send(monkeypatch):
|
||||
monkeypatch.setattr("banban.service.sms_notification.settings.sms_enabled", False)
|
||||
service = SmsNotificationService()
|
||||
|
||||
async def fail_get_session():
|
||||
raise AssertionError("disabled SMS should not query database")
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fail_get_session)
|
||||
|
||||
await service.notify_leave_message(device_id="TalkingQ_XQSN00001005", child_name="孩子")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_best_effort_swallows_send_errors(monkeypatch):
|
||||
configure_enabled_sms(monkeypatch)
|
||||
service = SmsNotificationService()
|
||||
|
||||
async def fake_get_session():
|
||||
return FakeSession()
|
||||
|
||||
async def fake_send_sms(*, phone_number, template_code, template_params):
|
||||
del phone_number, template_code, template_params
|
||||
raise RuntimeError("provider failed")
|
||||
|
||||
monkeypatch.setattr(service, "get_session", fake_get_session)
|
||||
monkeypatch.setattr(service, "_send_sms", fake_send_sms)
|
||||
|
||||
await service.notify_alarm_best_effort(device_id="TalkingQ_XQSN00001005", alarm_id=7)
|
||||
Reference in New Issue
Block a user