test_technician_mini_app.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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.upsert_directory_membership(
  52. bot_id="primary",
  53. chat_id=-100200,
  54. user_id=user_id,
  55. status="member",
  56. active=True,
  57. verified=True,
  58. )
  59. await directory_db.submit_teacher_application(
  60. user_id=user_id,
  61. source="test",
  62. bot_id="primary",
  63. chat_id=-100200,
  64. )
  65. await directory_db.decide_teacher_application(
  66. user_id=user_id,
  67. action="approve",
  68. actor_id="admin",
  69. actor_name="admin",
  70. source="test",
  71. )
  72. async def test_builtin_templates_are_seeded_without_overwriting_admin_changes(
  73. app_modules,
  74. ):
  75. service_db = app_modules.load("wbb.utils.dbservice")
  76. first = await service_db.ensure_builtin_package_templates()
  77. assert [item["template_id"] for item in first] == [
  78. "builtin_quick_at_store",
  79. "builtin_quick_onsite",
  80. "builtin_quick_hourly",
  81. ]
  82. await app_modules.wbb.control_db.service_package_templates.update_one(
  83. {"template_id": "builtin_quick_at_store"},
  84. {"$set": {"name": "管理员修改后的到店模板", "status": "disabled"}},
  85. )
  86. await service_db.ensure_builtin_package_templates()
  87. stored = await app_modules.wbb.control_db.service_package_templates.find_one(
  88. {"template_id": "builtin_quick_at_store"}
  89. )
  90. assert stored["name"] == "管理员修改后的到店模板"
  91. assert stored["status"] == "disabled"
  92. async def test_technician_mini_app_uses_signed_identity_without_admin_login(
  93. app_modules,
  94. ):
  95. user_id = 700
  96. await _approved_technician(app_modules, user_id)
  97. bot_config = app_modules.load("wbb.admin.bot_config")
  98. token = "123456:abcdefghijklmnopqrstuvwxyzABCDE"
  99. bot = bot_config.create_bot_profile(
  100. app_modules.wbb.BOT_PROFILES_PATH,
  101. {
  102. "label": "技师服务 Bot",
  103. "bot_token": token,
  104. "enabled": True,
  105. },
  106. )
  107. admin_api = app_modules.load("wbb.admin.api")
  108. application = admin_api.build_admin_application()
  109. await application["admin_api"].initialize()
  110. client = TestClient(TestServer(application))
  111. await client.start_server()
  112. headers = {
  113. "X-Telegram-Bot-Id": bot["bot_id"],
  114. "X-Telegram-Init-Data": _signed_init_data(token, user_id),
  115. }
  116. try:
  117. invalid = await client.get(
  118. "/api/technician/v1/bootstrap",
  119. headers={**headers, "X-Telegram-Init-Data": f"{headers['X-Telegram-Init-Data']}x"},
  120. )
  121. assert invalid.status == 401
  122. assert (await invalid.json())["error"]["code"] == "mini_app_auth_failed"
  123. expired = await client.get(
  124. "/api/technician/v1/bootstrap",
  125. headers={
  126. **headers,
  127. "X-Telegram-Init-Data": _signed_init_data(
  128. token,
  129. user_id,
  130. auth_date=datetime.now(UTC) - timedelta(hours=2),
  131. ),
  132. },
  133. )
  134. assert expired.status == 401
  135. loaded = await client.get("/api/technician/v1/bootstrap", headers=headers)
  136. assert loaded.status == 200
  137. payload = (await loaded.json())["data"]
  138. assert payload["technician"]["user_id"] == str(user_id)
  139. assert payload["technician"]["eligibility"]["ready"] is True
  140. assert [item["name"] for item in payload["templates"]] == [
  141. "快速到店服务",
  142. "快速上门服务",
  143. "快速按小时服务",
  144. ]
  145. assert all("admin_note" not in item for item in payload["templates"])
  146. package = payload["templates"][0]["package"]
  147. package.update(
  148. {
  149. "package_id": "self_service_package",
  150. "source_template_id": payload["templates"][0]["template_id"],
  151. "source_template_version": payload["templates"][0]["version"],
  152. }
  153. )
  154. published = await client.put(
  155. "/api/technician/v1/profile",
  156. headers=headers,
  157. json={
  158. "user_id": "999999",
  159. "service_profile": {
  160. "headline": "一分钟发布套餐",
  161. "bio": package["description"],
  162. "tags": package["tags"],
  163. "contact_hours": "请通过 Telegram 私聊确认",
  164. "public_area_text": "测试城区及附近",
  165. "venue": {
  166. "name": "测试工作地点",
  167. "address_hint": "测试路 100 号",
  168. },
  169. "onsite_policy": {"description": ""},
  170. "packages": [package],
  171. "accepting_requests": True,
  172. },
  173. },
  174. )
  175. assert published.status == 200
  176. published_data = (await published.json())["data"]
  177. assert published_data["user_id"] == str(user_id)
  178. assert published_data["service_profile"]["packages"][0]["price_unit"] == "per_service"
  179. assert (
  180. await app_modules.wbb.control_db.directory_profiles.find_one(
  181. {"user_id": 999999}
  182. )
  183. ) is None
  184. paused = await client.patch(
  185. "/api/technician/v1/availability",
  186. headers=headers,
  187. json={"accepting_requests": False},
  188. )
  189. assert paused.status == 200
  190. assert (
  191. (await paused.json())["data"]["service_profile"]["accepting_requests"]
  192. is False
  193. )
  194. finally:
  195. await client.close()