test_technician_mini_app.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. from __future__ import annotations
  2. import hashlib
  3. import hmac
  4. import json
  5. from datetime import UTC, datetime, timedelta
  6. from urllib.parse import urlencode
  7. from aiohttp.test_utils import TestClient, TestServer
  8. def _signed_init_data(
  9. token: str,
  10. user_id: int,
  11. *,
  12. auth_date: datetime | None = None,
  13. ) -> str:
  14. values = {
  15. "auth_date": str(int((auth_date or datetime.now(UTC)).timestamp())),
  16. "query_id": "AAE-test-query",
  17. "user": json.dumps(
  18. {
  19. "id": user_id,
  20. "first_name": "认证",
  21. "last_name": "技师",
  22. "username": f"technician{user_id}",
  23. "language_code": "zh-hans",
  24. },
  25. ensure_ascii=False,
  26. separators=(",", ":"),
  27. ),
  28. }
  29. data_check_string = "\n".join(
  30. f"{key}={value}" for key, value in sorted(values.items())
  31. )
  32. secret_key = hmac.new(
  33. b"WebAppData",
  34. token.encode(),
  35. hashlib.sha256,
  36. ).digest()
  37. values["hash"] = hmac.new(
  38. secret_key,
  39. data_check_string.encode(),
  40. hashlib.sha256,
  41. ).hexdigest()
  42. return urlencode(values)
  43. async def _approved_technician(app_modules, user_id: int) -> None:
  44. directory_db = app_modules.load("wbb.utils.dbdirectory")
  45. await directory_db.upsert_directory_identity(
  46. user_id=user_id,
  47. username=f"technician{user_id}",
  48. first_name="认证",
  49. last_name="技师",
  50. )
  51. await directory_db.save_directory_location(
  52. user_id=user_id,
  53. longitude=116.4074,
  54. latitude=39.9042,
  55. source="test",
  56. actor_id=user_id,
  57. actor_name="认证技师",
  58. )
  59. await directory_db.upsert_directory_membership(
  60. bot_id="primary",
  61. chat_id=-100200,
  62. user_id=user_id,
  63. status="member",
  64. active=True,
  65. verified=True,
  66. )
  67. await directory_db.submit_teacher_application(
  68. user_id=user_id,
  69. source="test",
  70. bot_id="primary",
  71. chat_id=-100200,
  72. )
  73. await directory_db.decide_teacher_application(
  74. user_id=user_id,
  75. action="approve",
  76. actor_id="admin",
  77. actor_name="admin",
  78. source="test",
  79. )
  80. async def test_builtin_templates_are_seeded_without_overwriting_admin_changes(
  81. app_modules,
  82. ):
  83. service_db = app_modules.load("wbb.utils.dbservice")
  84. first = await service_db.ensure_builtin_package_templates()
  85. assert [item["template_id"] for item in first] == [
  86. "builtin_quick_at_store",
  87. "builtin_quick_onsite",
  88. "builtin_quick_hourly",
  89. ]
  90. await app_modules.wbb.control_db.service_package_templates.update_one(
  91. {"template_id": "builtin_quick_at_store"},
  92. {"$set": {"name": "管理员修改后的到店模板", "status": "disabled"}},
  93. )
  94. await service_db.ensure_builtin_package_templates()
  95. stored = await app_modules.wbb.control_db.service_package_templates.find_one(
  96. {"template_id": "builtin_quick_at_store"}
  97. )
  98. assert stored["name"] == "管理员修改后的到店模板"
  99. assert stored["status"] == "disabled"
  100. async def test_technician_mini_app_uses_signed_identity_without_admin_login(
  101. app_modules,
  102. ):
  103. user_id = 700
  104. await _approved_technician(app_modules, user_id)
  105. bot_config = app_modules.load("wbb.admin.bot_config")
  106. token = "123456:abcdefghijklmnopqrstuvwxyzABCDE"
  107. bot = bot_config.create_bot_profile(
  108. app_modules.wbb.BOT_PROFILES_PATH,
  109. {
  110. "label": "技师服务 Bot",
  111. "bot_token": token,
  112. "enabled": True,
  113. },
  114. )
  115. admin_api = app_modules.load("wbb.admin.api")
  116. application = admin_api.build_admin_application()
  117. await application["admin_api"].initialize()
  118. client = TestClient(TestServer(application))
  119. await client.start_server()
  120. headers = {
  121. "X-Telegram-Bot-Id": bot["bot_id"],
  122. "X-Telegram-Init-Data": _signed_init_data(token, user_id),
  123. }
  124. try:
  125. invalid = await client.get(
  126. "/api/technician/v1/bootstrap",
  127. headers={**headers, "X-Telegram-Init-Data": f"{headers['X-Telegram-Init-Data']}x"},
  128. )
  129. assert invalid.status == 401
  130. assert (await invalid.json())["error"]["code"] == "mini_app_auth_failed"
  131. expired = await client.get(
  132. "/api/technician/v1/bootstrap",
  133. headers={
  134. **headers,
  135. "X-Telegram-Init-Data": _signed_init_data(
  136. token,
  137. user_id,
  138. auth_date=datetime.now(UTC) - timedelta(hours=2),
  139. ),
  140. },
  141. )
  142. assert expired.status == 401
  143. loaded = await client.get("/api/technician/v1/bootstrap", headers=headers)
  144. assert loaded.status == 200
  145. payload = (await loaded.json())["data"]
  146. assert payload["technician"]["user_id"] == str(user_id)
  147. assert payload["technician"]["eligibility"]["ready"] is True
  148. assert [item["name"] for item in payload["templates"]] == [
  149. "快速到店服务",
  150. "快速上门服务",
  151. "快速按小时服务",
  152. ]
  153. assert all("admin_note" not in item for item in payload["templates"])
  154. package = payload["templates"][0]["package"]
  155. package.update(
  156. {
  157. "package_id": "self_service_package",
  158. "source_template_id": payload["templates"][0]["template_id"],
  159. "source_template_version": payload["templates"][0]["version"],
  160. }
  161. )
  162. published = await client.put(
  163. "/api/technician/v1/profile",
  164. headers=headers,
  165. json={
  166. "user_id": "999999",
  167. "service_profile": {
  168. "headline": "一分钟发布套餐",
  169. "bio": package["description"],
  170. "tags": package["tags"],
  171. "contact_hours": "请通过 Telegram 私聊确认",
  172. "public_area_text": "测试城区及附近",
  173. "venue": {
  174. "name": "测试工作地点",
  175. "address_hint": "测试路 100 号",
  176. },
  177. "onsite_policy": {"description": ""},
  178. "packages": [package],
  179. "accepting_requests": True,
  180. },
  181. },
  182. )
  183. assert published.status == 200
  184. published_data = (await published.json())["data"]
  185. assert published_data["user_id"] == str(user_id)
  186. assert published_data["service_profile"]["packages"][0]["price_unit"] == "per_service"
  187. assert (
  188. await app_modules.wbb.control_db.directory_profiles.find_one(
  189. {"user_id": 999999}
  190. )
  191. ) is None
  192. paused = await client.patch(
  193. "/api/technician/v1/availability",
  194. headers=headers,
  195. json={"accepting_requests": False},
  196. )
  197. assert paused.status == 200
  198. assert (
  199. (await paused.json())["data"]["service_profile"]["accepting_requests"]
  200. is False
  201. )
  202. finally:
  203. await client.close()