test_client.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223
  1. """Tests for ChattoClient.
  2. Since the client now goes through generated ConnectRPC service stubs, tests
  3. mock at the service-client method level with ``AsyncMock`` rather than at the
  4. HTTP transport layer. This exercises the request-building and response-parsing
  5. paths without any real network traffic.
  6. """
  7. from __future__ import annotations
  8. import datetime as dt
  9. from unittest.mock import AsyncMock
  10. import pytest
  11. from chattolib._pb.chatto.admin.v1 import (
  12. diagnostics_pb2,
  13. room_layout_pb2,
  14. )
  15. from chattolib._pb.chatto.admin.v1 import (
  16. server_pb2 as admin_server_pb2,
  17. )
  18. from chattolib._pb.chatto.api.v1 import (
  19. account_pb2,
  20. asset_uploads_pb2,
  21. attachments_pb2,
  22. message_search_pb2,
  23. messages_pb2,
  24. notification_preferences_pb2,
  25. notifications_pb2,
  26. presence_pb2,
  27. push_notifications_pb2,
  28. reactions_pb2,
  29. read_state_pb2,
  30. roles_pb2,
  31. room_directory_pb2,
  32. room_timeline_pb2,
  33. rooms_pb2,
  34. threads_pb2,
  35. user_service_pb2,
  36. viewer_pb2,
  37. voice_calls_pb2,
  38. )
  39. from chattolib._pb.chatto.discovery.v1 import server_pb2 as discovery_server_pb2
  40. from chattolib.client import ChattoClient
  41. from chattolib.types import (
  42. ImageFitMode,
  43. ImageTransformOptions,
  44. MessageSearchGroupBy,
  45. MessageSearchOrder,
  46. MessageSearchScope,
  47. MessageSearchState,
  48. NotificationLevel,
  49. PresenceStatus,
  50. RoomKind,
  51. ThreadRootAuthorFilter,
  52. TimeFormat,
  53. )
  54. BASE = "https://chat.chatto.run"
  55. @pytest.fixture
  56. def client():
  57. return ChattoClient(token="test-token", base_url=BASE)
  58. def _mock_method(client: ChattoClient, service: str, method: str, response):
  59. """Replace a service-client method with an AsyncMock returning ``response``."""
  60. svc = getattr(client._svc, service)
  61. setattr(svc, method, AsyncMock(return_value=response))
  62. return getattr(svc, method)
  63. # --- Server discovery ---------------------------------------------------
  64. async def test_get_server(client):
  65. resp = discovery_server_pb2.GetServerResponse()
  66. resp.profile.name = "Chatto HQ"
  67. resp.profile.version = "0.4.2"
  68. resp.login.direct_registration_enabled = True
  69. resp.login.authorize_url = "/oauth"
  70. _mock_method(client, "server_discovery", "get_server", resp)
  71. async with client:
  72. profile, login = await client.get_server()
  73. assert profile.name == "Chatto HQ"
  74. assert profile.version == "0.4.2"
  75. assert login.direct_registration_enabled is True
  76. assert login.authorize_url == "/oauth"
  77. async def test_list_neighborhood_servers(client):
  78. resp = discovery_server_pb2.ListNeighborhoodServersResponse()
  79. s1 = resp.servers.add()
  80. s1.origin = "https://neighbor.example"
  81. s1.profile.name = "Neighbor"
  82. s1.profile.version = "0.5.0"
  83. s1.profile.logo_url = "/logos/neighbor.png"
  84. s1.direct_neighbor = True
  85. s2 = resp.servers.add()
  86. s2.origin = "https://referred.example"
  87. s2.profile.name = "Referred"
  88. s2.direct_neighbor = False
  89. s2.recommended_by_origins.append("https://neighbor.example")
  90. resp.refreshed_at.FromJsonString("2026-09-26T12:00:00Z")
  91. mocked = _mock_method(client, "server_discovery", "list_neighborhood_servers", resp)
  92. async with client:
  93. page = await client.list_neighborhood_servers()
  94. mocked.assert_awaited_once()
  95. assert [s.origin for s in page.servers] == [
  96. "https://neighbor.example",
  97. "https://referred.example",
  98. ]
  99. assert page.servers[0].profile is not None
  100. assert page.servers[0].profile.name == "Neighbor"
  101. assert page.servers[0].profile.logo_url == "/logos/neighbor.png"
  102. assert page.servers[0].direct_neighbor is True
  103. assert page.servers[1].direct_neighbor is False
  104. assert page.servers[1].recommended_by_origins == ["https://neighbor.example"]
  105. assert page.refreshed_at == dt.datetime(2026, 9, 26, 12, 0, tzinfo=dt.UTC)
  106. # --- Viewer -------------------------------------------------------------
  107. async def test_me(client):
  108. resp = viewer_pb2.GetViewerResponse()
  109. resp.user.profile.id = "u1"
  110. resp.user.profile.login = "alice"
  111. resp.user.profile.display_name = "Alice"
  112. resp.user.profile.presence_status = "PRESENCE_STATUS_ONLINE"
  113. resp.user.has_verified_email = True
  114. resp.user.settings.timezone = "UTC"
  115. resp.user.settings.time_format = "TIME_FORMAT_24_HOUR"
  116. resp.user.has_password = True
  117. _mock_method(client, "viewer", "get_viewer", resp)
  118. async with client:
  119. user = await client.me()
  120. assert user.id == "u1"
  121. assert user.login == "alice"
  122. assert user.display_name == "Alice"
  123. assert user.presence_status is PresenceStatus.ONLINE
  124. # --- Room directory ----------------------------------------------------
  125. async def test_list_rooms(client):
  126. resp = room_directory_pb2.ListRoomsResponse()
  127. entry = resp.rooms.add()
  128. entry.room.id = "r1"
  129. entry.room.kind = "ROOM_KIND_CHANNEL"
  130. entry.room.name = "general"
  131. entry.room.group_id = "g1"
  132. entry.viewer_state.is_member = True
  133. entry.viewer_state.has_unread = False
  134. _mock_method(client, "room_directory", "list_rooms", resp)
  135. async with client:
  136. rooms = await client.list_rooms()
  137. assert len(rooms) == 1
  138. assert rooms[0].room is not None
  139. assert rooms[0].room.name == "general"
  140. assert rooms[0].room.kind is RoomKind.CHANNEL
  141. assert rooms[0].viewer_state.is_member is True
  142. async def test_list_rooms_dm_keeps_deleted_participants(client):
  143. # Since Chatto 0.5.0-beta.10, a DM's member_user_ids keeps participants
  144. # whose accounts were deleted; the client must surface the full list
  145. # (resolution to a User then legitimately fails for the deleted one).
  146. resp = room_directory_pb2.ListRoomsResponse()
  147. entry = resp.rooms.add()
  148. entry.room.id = "r-dm"
  149. entry.room.kind = "ROOM_KIND_DM"
  150. entry.room.name = "me & u2"
  151. entry.viewer_state.is_member = True
  152. entry.member_user_ids.extend(["u1", "u-deleted"])
  153. _mock_method(client, "room_directory", "list_rooms", resp)
  154. async with client:
  155. rooms = await client.list_rooms()
  156. assert len(rooms) == 1
  157. assert rooms[0].room is not None
  158. assert rooms[0].room.kind is RoomKind.DM
  159. assert rooms[0].member_user_ids == ["u1", "u-deleted"]
  160. # --- Messages ----------------------------------------------------------
  161. async def test_post_message(client):
  162. resp = messages_pb2.CreateMessageResponse()
  163. resp.message.id = "e1"
  164. resp.message.room_id = "r1"
  165. resp.message.actor_id = "u1"
  166. resp.message.body = "Hello!"
  167. _mock_method(client, "messages", "create_message", resp)
  168. async with client:
  169. message = await client.post_message("r1", "Hello!")
  170. assert message.id == "e1"
  171. assert message.body == "Hello!"
  172. assert message.room_id == "r1"
  173. async def test_update_message(client):
  174. resp = messages_pb2.UpdateMessageResponse()
  175. resp.message.id = "e1"
  176. resp.message.room_id = "r1"
  177. resp.message.actor_id = "u1"
  178. resp.message.body = "edited"
  179. resp.message.updated_at.FromJsonString("2026-01-01T00:01:00Z")
  180. m = _mock_method(client, "messages", "update_message", resp)
  181. async with client:
  182. msg = await client.update_message("r1", "e1", body="edited")
  183. assert msg.body == "edited"
  184. assert msg.updated_at is not None
  185. # verify the request had the optional body field explicitly set
  186. call_args = m.call_args
  187. req = call_args.args[0]
  188. assert isinstance(req, messages_pb2.UpdateMessageRequest)
  189. assert req.HasField("body")
  190. assert req.body == "edited"
  191. async def test_delete_message(client):
  192. _mock_method(client, "messages", "delete_message", messages_pb2.DeleteMessageResponse())
  193. async with client:
  194. assert await client.delete_message("r1", "e1") is None
  195. async def test_add_reaction(client):
  196. resp = reactions_pb2.AddReactionResponse(added=True)
  197. _mock_method(client, "messages", "add_reaction", resp)
  198. async with client:
  199. assert await client.add_reaction("r1", "e1", "thumbsup") is True
  200. async def test_remove_reaction(client):
  201. resp = reactions_pb2.RemoveReactionResponse(removed=True)
  202. _mock_method(client, "messages", "remove_reaction", resp)
  203. async with client:
  204. assert await client.remove_reaction("r1", "e1", "thumbsup") is True
  205. # --- Room lifecycle ---------------------------------------------------
  206. async def test_create_room(client):
  207. resp = rooms_pb2.CreateRoomResponse()
  208. resp.room.id = "r1"
  209. resp.room.kind = "ROOM_KIND_CHANNEL"
  210. resp.room.name = "general"
  211. resp.room.group_id = "g1"
  212. _mock_method(client, "rooms", "create_room", resp)
  213. async with client:
  214. room = await client.create_room("general", "g1")
  215. assert room.id == "r1"
  216. assert room.name == "general"
  217. assert room.group_id == "g1"
  218. async def test_archive_room(client):
  219. resp = rooms_pb2.ArchiveRoomResponse()
  220. resp.room.id = "r1"
  221. resp.room.name = "general"
  222. resp.room.archived = True
  223. _mock_method(client, "rooms", "archive_room", resp)
  224. async with client:
  225. room = await client.archive_room("r1")
  226. assert room.archived is True
  227. async def test_remove_user(client):
  228. resp = rooms_pb2.RemoveUserResponse()
  229. mock = _mock_method(client, "rooms", "remove_user", resp)
  230. async with client:
  231. await client.remove_user("r1", "u2", "spam")
  232. await client.remove_user(
  233. "r1", "u2", "spam", suspension_expires_at=dt.datetime.fromtimestamp(0, dt.UTC)
  234. )
  235. await client.remove_user("r1", "u2", "spam", suspend_indefinitely=True)
  236. calls = [c.args[0] for c in mock.call_args_list]
  237. assert calls[0].WhichOneof("suspension") is None
  238. assert calls[1].WhichOneof("suspension") == "suspension_expires_at"
  239. assert calls[2].WhichOneof("suspension") == "suspend_indefinitely"
  240. async def test_remove_user_rejects_both_suspensions(client):
  241. _mock_method(client, "rooms", "remove_user", rooms_pb2.RemoveUserResponse())
  242. async with client:
  243. with pytest.raises(ValueError, match="not both"):
  244. await client.remove_user(
  245. "r1",
  246. "u2",
  247. "spam",
  248. suspension_expires_at=dt.datetime.fromtimestamp(0, dt.UTC),
  249. suspend_indefinitely=True,
  250. )
  251. async def test_lift_suspension(client):
  252. _mock_method(client, "rooms", "lift_suspension", rooms_pb2.LiftSuspensionResponse())
  253. async with client:
  254. assert await client.lift_suspension("r1", "u2", "reinstated") is None
  255. async def test_list_suspensions(client):
  256. resp = rooms_pb2.ListSuspensionsResponse()
  257. s = resp.suspensions.add()
  258. s.id = "s1"
  259. s.room_id = "r1"
  260. s.user_id = "u2"
  261. _mock_method(client, "rooms", "list_suspensions", resp)
  262. async with client:
  263. suspensions, _ = await client.list_suspensions(room_id="r1")
  264. assert [x.id for x in suspensions] == ["s1"]
  265. async def test_start_dm(client):
  266. resp = rooms_pb2.StartDMResponse()
  267. resp.room.id = "r1"
  268. resp.room.kind = "ROOM_KIND_DM"
  269. _mock_method(client, "rooms", "start_dm", resp)
  270. async with client:
  271. room = await client.start_dm(["u1", "u2"])
  272. assert room.id == "r1"
  273. assert room.kind is RoomKind.DM
  274. # --- Room timeline ----------------------------------------------------
  275. async def test_get_room_events(client):
  276. resp = room_timeline_pb2.GetRoomEventsResponse()
  277. event = resp.page.events.add()
  278. event.id = "e1"
  279. event.actor_id = "u1"
  280. event.created_at.FromJsonString("2026-01-01T00:00:00Z")
  281. event.message_posted.message.id = "e1"
  282. event.message_posted.message.room_id = "r1"
  283. event.message_posted.message.actor_id = "u1"
  284. event.message_posted.message.body = "hi"
  285. event.message_posted.message.created_at.FromJsonString("2026-01-01T00:00:00Z")
  286. resp.page.start_cursor = "cA"
  287. resp.page.end_cursor = "cB"
  288. resp.page.includes.users["u1"].id = "u1"
  289. resp.page.includes.users["u1"].login = "alice"
  290. resp.page.includes.users["u1"].display_name = "Alice"
  291. _mock_method(client, "rooms", "get_room_events", resp)
  292. async with client:
  293. page = await client.get_room_events("r1", limit=50)
  294. assert len(page.events) == 1
  295. assert page.events[0].kind == "message_posted"
  296. assert page.events[0].message is not None
  297. assert page.events[0].message.body == "hi"
  298. assert page.users_by_id["u1"].login == "alice"
  299. async def test_mark_room_as_read(client):
  300. resp = read_state_pb2.MarkRoomAsReadResponse()
  301. resp.last_read_at.FromJsonString("2026-01-01T00:00:00Z")
  302. resp.previous_last_read_at.FromJsonString("2025-12-31T00:00:00Z")
  303. _mock_method(client, "rooms", "mark_room_as_read", resp)
  304. async with client:
  305. last, previous = await client.mark_room_as_read("r1", up_to_event_id="e1")
  306. assert last is not None
  307. assert previous is not None
  308. # --- Profile / account ------------------------------------------------
  309. async def test_update_user_profile(client):
  310. resp = user_service_pb2.UpdateUserProfileResponse()
  311. resp.user.id = "u1"
  312. resp.user.login = "newname"
  313. resp.user.display_name = "New Name"
  314. resp.user.bio = "# Hi"
  315. resp.user.presence_status = "PRESENCE_STATUS_ONLINE"
  316. _mock_method(client, "user", "update_user_profile", resp)
  317. async with client:
  318. user = await client.update_user_profile(
  319. "u1", login="newname", display_name="New Name", bio="# Hi"
  320. )
  321. assert user.id == "u1"
  322. assert user.login == "newname"
  323. assert user.display_name == "New Name"
  324. assert user.bio == "# Hi"
  325. async def test_update_profile(client):
  326. """Self-service update resolves the caller id via the viewer, then
  327. delegates to the new UserService.UpdateUserProfile wire call."""
  328. viewer = viewer_pb2.GetViewerResponse()
  329. viewer.user.profile.id = "u1"
  330. viewer.user.profile.login = "alice"
  331. viewer.user.profile.display_name = "Alice"
  332. _mock_method(client, "viewer", "get_viewer", viewer)
  333. resp = user_service_pb2.UpdateUserProfileResponse()
  334. resp.user.id = "u1"
  335. resp.user.login = "newname"
  336. resp.user.display_name = "New Name"
  337. resp.user.presence_status = "PRESENCE_STATUS_ONLINE"
  338. update = _mock_method(client, "user", "update_user_profile", resp)
  339. async with client:
  340. user = await client.update_profile(login="newname", display_name="New Name")
  341. assert user.login == "newname"
  342. assert user.display_name == "New Name"
  343. (req,) = update.await_args.args
  344. assert req.user_id == "u1"
  345. async def test_set_presence(client):
  346. resp = presence_pb2.SetPresenceResponse()
  347. resp.status = "PRESENCE_STATUS_ONLINE"
  348. _mock_method(client, "account", "set_presence", resp)
  349. async with client:
  350. result = await client.set_presence(PresenceStatus.ONLINE)
  351. assert result is PresenceStatus.ONLINE
  352. async def test_set_presence_offline_rejected(client):
  353. async with client:
  354. with pytest.raises(ValueError):
  355. await client.set_presence(PresenceStatus.OFFLINE)
  356. async def test_get_presence_preference(client):
  357. resp = presence_pb2.GetPresencePreferenceResponse()
  358. resp.preference.status = "PRESENCE_STATUS_OFFLINE"
  359. resp.preference.revision = "rev-1"
  360. _mock_method(client, "account", "get_presence_preference", resp)
  361. async with client:
  362. pref = await client.get_presence_preference()
  363. assert pref is not None
  364. assert pref.status is PresenceStatus.OFFLINE
  365. assert pref.revision == "rev-1"
  366. async def test_get_presence_preference_absent(client):
  367. # No preference saved yet: the response carries no sub-message.
  368. _mock_method(
  369. client, "account", "get_presence_preference", presence_pb2.GetPresencePreferenceResponse()
  370. )
  371. async with client:
  372. assert await client.get_presence_preference() is None
  373. async def test_set_presence_preference(client):
  374. resp = presence_pb2.SetPresencePreferenceResponse()
  375. resp.preference.status = "PRESENCE_STATUS_DO_NOT_DISTURB"
  376. resp.preference.revision = "rev-2"
  377. m = _mock_method(client, "account", "set_presence_preference", resp)
  378. async with client:
  379. pref = await client.set_presence_preference(
  380. PresenceStatus.DO_NOT_DISTURB, expected_revision="rev-1"
  381. )
  382. assert pref.status is PresenceStatus.DO_NOT_DISTURB
  383. assert pref.revision == "rev-2"
  384. req = m.await_args.args[0]
  385. assert req.status == presence_pb2.PresenceStatus.Value("PRESENCE_STATUS_DO_NOT_DISTURB")
  386. assert req.expected_revision == "rev-1"
  387. async def test_set_presence_preference_offline_allowed(client):
  388. # Unlike set_presence, OFFLINE is a valid saved choice ("Appear Offline").
  389. resp = presence_pb2.SetPresencePreferenceResponse()
  390. resp.preference.status = "PRESENCE_STATUS_OFFLINE"
  391. _mock_method(client, "account", "set_presence_preference", resp)
  392. async with client:
  393. assert (await client.set_presence_preference(PresenceStatus.OFFLINE)).status is (
  394. PresenceStatus.OFFLINE
  395. )
  396. async def test_set_presence_preference_unspecified_rejected(client):
  397. async with client:
  398. with pytest.raises(ValueError):
  399. await client.set_presence_preference(PresenceStatus.UNSPECIFIED)
  400. async def test_refresh_presence(client):
  401. resp = presence_pb2.RefreshPresenceResponse()
  402. resp.preference.status = "PRESENCE_STATUS_ONLINE"
  403. resp.preference.revision = "rev-3"
  404. _mock_method(client, "account", "refresh_presence", resp)
  405. async with client:
  406. pref = await client.refresh_presence()
  407. assert pref is not None
  408. assert pref.status is PresenceStatus.ONLINE
  409. assert pref.revision == "rev-3"
  410. async def test_update_settings(client):
  411. resp = account_pb2.UpdateSettingsResponse()
  412. resp.settings.timezone = "Europe/Rome"
  413. resp.settings.time_format = "TIME_FORMAT_24_HOUR"
  414. _mock_method(client, "account", "update_settings", resp)
  415. async with client:
  416. settings = await client.update_settings(
  417. timezone="Europe/Rome", time_format=TimeFormat.HOUR_24
  418. )
  419. assert settings.timezone == "Europe/Rome"
  420. assert settings.time_format is TimeFormat.HOUR_24
  421. # --- Threads ----------------------------------------------------------
  422. async def test_list_followed_threads(client):
  423. resp = threads_pb2.ListFollowedThreadsResponse()
  424. ft = resp.threads.add()
  425. ft.room.id = "r1"
  426. ft.room.name = "general"
  427. ft.root_message.id = "e1"
  428. ft.root_message.room_id = "r1"
  429. ft.root_message.actor_id = "u1"
  430. ft.root_message.body = "root"
  431. ft.root_message.created_at.FromJsonString("2026-01-01T00:00:00Z")
  432. ft.thread.thread_root_event_id = "e1"
  433. ft.thread.reply_count = 3
  434. ft.thread.last_reply_at.FromJsonString("2026-01-02T00:00:00Z")
  435. ft.thread.viewer_state.is_following = True
  436. ft.thread.viewer_state.has_unread_replies = False
  437. resp.page.total_count = 1
  438. resp.page.has_more = False
  439. mocked = _mock_method(client, "threads", "list_followed_threads", resp)
  440. async with client:
  441. page = await client.list_followed_threads()
  442. await client.list_followed_threads(
  443. limit=5,
  444. offset=5,
  445. include_direct_message_threads=True,
  446. unread_only=True,
  447. root_author_filter=ThreadRootAuthorFilter.VIEWER,
  448. )
  449. await client.list_followed_threads(
  450. root_author_filter=ThreadRootAuthorFilter.OTHERS,
  451. )
  452. assert page.page.total_count == 1
  453. assert page.threads[0].thread is not None
  454. assert page.threads[0].thread.reply_count == 3
  455. assert page.threads[0].thread.is_following is True
  456. calls = [c.args[0] for c in mocked.call_args_list]
  457. assert calls[0].include_direct_message_threads is False
  458. assert calls[0].unread_only is False
  459. assert calls[0].root_author_filter == 0
  460. assert calls[1].include_direct_message_threads is True
  461. assert calls[1].unread_only is True
  462. assert calls[1].root_author_filter == threads_pb2.ThreadRootAuthorFilter.Value(
  463. "THREAD_ROOT_AUTHOR_FILTER_VIEWER"
  464. )
  465. assert calls[2].root_author_filter == threads_pb2.ThreadRootAuthorFilter.Value(
  466. "THREAD_ROOT_AUTHOR_FILTER_OTHERS"
  467. )
  468. assert calls[1].page.limit == 5
  469. assert calls[1].page.offset == 5
  470. async def test_follow_and_unfollow_thread(client):
  471. _mock_method(client, "threads", "follow_thread", threads_pb2.FollowThreadResponse())
  472. _mock_method(client, "threads", "unfollow_thread", threads_pb2.UnfollowThreadResponse())
  473. async with client:
  474. assert await client.follow_thread("r1", "e1") is None
  475. assert await client.unfollow_thread("r1", "e1") is None
  476. # --- Notifications ----------------------------------------------------
  477. async def test_list_notification_occurrences(client):
  478. resp = notifications_pb2.ListNotificationOccurrencesResponse()
  479. occ = resp.occurrences.add()
  480. occ.id = "n1"
  481. occ.created_at.FromJsonString("2026-01-01T00:00:00Z")
  482. occ.actor.id = "u1"
  483. occ.actor.login = "alice"
  484. occ.actor.display_name = "Alice"
  485. occ.unread = True
  486. sig = occ.signal.direct_mention_received
  487. sig.message.room.id = "r1"
  488. sig.message.room.name = "general"
  489. sig.message.event_id = "e1"
  490. resp.page.total_count = 1
  491. resp.unread_count = 1
  492. _mock_method(client, "notifications", "list_notification_occurrences", resp)
  493. async with client:
  494. page = await client.list_notification_occurrences()
  495. assert page.page.total_count == 1
  496. assert page.unread_count == 1
  497. assert page.occurrences[0].signal is not None
  498. assert page.occurrences[0].signal.kind == "direct_mention_received"
  499. assert page.occurrences[0].signal.message is not None
  500. assert page.occurrences[0].signal.message.event_id == "e1"
  501. assert page.occurrences[0].signal.message.room is not None
  502. assert page.occurrences[0].signal.message.room.name == "general"
  503. async def test_mark_and_delete_notification_occurrence(client):
  504. read_resp = notifications_pb2.MarkNotificationReadResponse()
  505. read_resp.occurrence.id = "n1"
  506. read_resp.occurrence.unread = False
  507. _mock_method(client, "notifications", "mark_notification_read", read_resp)
  508. del_resp = notifications_pb2.DeleteNotificationOccurrenceResponse(deleted=True)
  509. _mock_method(client, "notifications", "delete_notification_occurrence", del_resp)
  510. all_resp = notifications_pb2.DeleteAllNotificationOccurrencesResponse(deleted_count=3)
  511. _mock_method(client, "notifications", "delete_all_notification_occurrences", all_resp)
  512. async with client:
  513. occ = await client.mark_notification_read("n1")
  514. assert occ is not None and occ.unread is False
  515. assert await client.delete_notification_occurrence("n1") is True
  516. assert await client.delete_all_notification_occurrences() == 3
  517. async def test_notification_preference(client):
  518. resp = notification_preferences_pb2.UpdateNotificationPreferenceResponse()
  519. resp.preference.level = "NOTIFICATION_LEVEL_MUTED"
  520. resp.preference.effective_level = "NOTIFICATION_LEVEL_MUTED"
  521. _mock_method(
  522. client,
  523. "notification_prefs",
  524. "update_room_notification_preference",
  525. resp,
  526. )
  527. async with client:
  528. pref = await client.update_room_notification_preference("r1", NotificationLevel.MUTED)
  529. assert pref.level is NotificationLevel.MUTED
  530. assert pref.effective_level is NotificationLevel.MUTED
  531. # --- Push -------------------------------------------------------------
  532. async def test_subscribe_push(client):
  533. _mock_method(client, "push", "subscribe", push_notifications_pb2.SubscribeResponse())
  534. async with client:
  535. assert await client.subscribe_push("https://push", "k", "a") is True
  536. # --- Voice calls ------------------------------------------------------
  537. async def test_active_calls(client):
  538. resp = voice_calls_pb2.ListActiveCallsResponse()
  539. call = resp.calls.add()
  540. call.call_id = "c1"
  541. call.room.id = "r1"
  542. call.room.name = "voice"
  543. p = call.participants.add()
  544. p.user.id = "u1"
  545. p.user.login = "alice"
  546. p.user.display_name = "Alice"
  547. _mock_method(client, "voice_calls", "list_active_calls", resp)
  548. async with client:
  549. calls = await client.list_active_calls()
  550. assert len(calls) == 1
  551. assert calls[0].call_id == "c1"
  552. assert calls[0].room is not None
  553. assert calls[0].room.name == "voice"
  554. # --- Assets -----------------------------------------------------------
  555. async def test_get_asset_with_thumbnail(client):
  556. resp = attachments_pb2.GetAssetResponse()
  557. resp.asset.id = "a1"
  558. resp.asset.filename = "pic.png"
  559. resp.asset.content_type = "image/png"
  560. resp.asset.size = 1024
  561. resp.asset.asset_url.url = "https://cdn/pic.png"
  562. mock = _mock_method(client, "assets", "get_asset", resp)
  563. async with client:
  564. asset = await client.get_asset(
  565. "r1",
  566. "a1",
  567. thumbnail=ImageTransformOptions(128, 128, ImageFitMode.COVER),
  568. )
  569. assert asset is not None
  570. assert asset.filename == "pic.png"
  571. assert asset.asset_url is not None
  572. assert asset.asset_url.url == "https://cdn/pic.png"
  573. # verify the request encoded thumbnail options
  574. req = mock.call_args.args[0]
  575. assert req.thumbnail.width == 128
  576. assert req.thumbnail.height == 128
  577. # --- Roles ------------------------------------------------------------
  578. async def test_list_roles(client):
  579. resp = roles_pb2.ListRolesResponse()
  580. r1 = resp.roles.add()
  581. r1.name = "everyone"
  582. r1.display_name = "Everyone"
  583. r1.is_system = True
  584. r1.position = 0
  585. r2 = resp.roles.add()
  586. r2.name = "admin"
  587. r2.display_name = "Admin"
  588. r2.position = 10
  589. _mock_method(client, "roles", "list_roles", resp)
  590. async with client:
  591. roles = await client.list_roles()
  592. assert [r.name for r in roles] == ["everyone", "admin"]
  593. assert roles[0].is_system is True
  594. # --- Asset uploads ---------------------------------------------------
  595. async def test_asset_upload_end_to_end(client, tmp_path):
  596. import hashlib
  597. payload = b"hello world" * 100
  598. file = tmp_path / "greet.txt"
  599. file.write_bytes(payload)
  600. sha = hashlib.sha256(payload).hexdigest()
  601. create_resp = asset_uploads_pb2.CreateUploadResponse()
  602. create_resp.upload.upload_id = "up1"
  603. create_resp.upload.room_id = "r1"
  604. create_resp.upload.status = "ASSET_UPLOAD_STATUS_OPEN"
  605. create_resp.upload.committed_offset = 0
  606. create_resp.upload.size = len(payload)
  607. create_resp.upload.max_chunk_size = 4096
  608. create_resp.upload.sha256 = sha
  609. create_mock = _mock_method(client, "asset_uploads", "create_upload", create_resp)
  610. chunk_resp = asset_uploads_pb2.UploadChunkResponse()
  611. chunk_resp.upload.upload_id = "up1"
  612. chunk_resp.upload.room_id = "r1"
  613. chunk_resp.upload.status = "ASSET_UPLOAD_STATUS_OPEN"
  614. chunk_resp.upload.committed_offset = len(payload)
  615. chunk_resp.upload.size = len(payload)
  616. chunk_resp.upload.max_chunk_size = 4096
  617. chunk_resp.upload.sha256 = sha
  618. _mock_method(client, "asset_uploads", "upload_chunk", chunk_resp)
  619. complete_resp = asset_uploads_pb2.CompleteUploadResponse()
  620. complete_resp.upload.upload_id = "up1"
  621. complete_resp.upload.room_id = "r1"
  622. complete_resp.upload.status = "ASSET_UPLOAD_STATUS_COMPLETED"
  623. complete_resp.upload.committed_offset = len(payload)
  624. complete_resp.upload.size = len(payload)
  625. complete_resp.upload.sha256 = sha
  626. complete_resp.upload.asset_id = "a1"
  627. complete_resp.asset.id = "a1"
  628. complete_resp.asset.filename = "greet.txt"
  629. complete_resp.asset.content_type = "text/plain"
  630. complete_resp.asset.size = len(payload)
  631. _mock_method(client, "asset_uploads", "complete_upload", complete_resp)
  632. async with client:
  633. asset = await client.upload_attachment("r1", file, content_type="text/plain")
  634. assert asset.id == "a1"
  635. assert asset.filename == "greet.txt"
  636. create_req = create_mock.call_args.args[0]
  637. assert create_req.sha256 == sha
  638. assert create_req.size == len(payload)
  639. # --- External identities --------------------------------------------
  640. # --- Admin (structural smoke tests) ----------------------------------
  641. async def test_admin_list_room_groups(client):
  642. resp = room_layout_pb2.ListRoomGroupsResponse()
  643. g = resp.groups.add()
  644. g.id = "g1"
  645. g.name = "General"
  646. room_item = g.items.add()
  647. room_item.room.id = "r1"
  648. room_item.room.kind = "ROOM_KIND_CHANNEL"
  649. room_item.room.name = "chat"
  650. link_item = g.items.add()
  651. link_item.sidebar_link.id = "sl1"
  652. link_item.sidebar_link.label = "Docs"
  653. link_item.sidebar_link.url = "https://x"
  654. g.can_create_room = True
  655. _mock_method(client, "admin_room_layout", "list_room_groups", resp)
  656. async with client:
  657. groups = await client.admin_list_room_groups()
  658. assert len(groups) == 1
  659. assert groups[0].name == "General"
  660. assert groups[0].rooms[0].id == "r1"
  661. assert groups[0].sidebar_links[0].label == "Docs"
  662. assert groups[0].can_create_room is True
  663. async def test_admin_create_sidebar_link(client):
  664. resp = room_layout_pb2.CreateSidebarLinkResponse()
  665. resp.sidebar_link.id = "sl1"
  666. resp.sidebar_link.label = "Docs"
  667. resp.sidebar_link.url = "https://x"
  668. _mock_method(client, "admin_room_layout", "create_sidebar_link", resp)
  669. async with client:
  670. result = await client.admin_create_sidebar_link("g1", "Docs", "https://x")
  671. assert result == {"id": "sl1", "label": "Docs", "url": "https://x"}
  672. async def test_admin_move_room_group(client):
  673. resp = room_layout_pb2.MoveRoomGroupResponse()
  674. g = resp.groups.add()
  675. g.id = "g1"
  676. g.name = "General"
  677. mock = _mock_method(client, "admin_room_layout", "move_room_group", resp)
  678. async with client:
  679. groups = await client.admin_move_room_group("g1", before_group_id="g2")
  680. assert [g.id for g in groups] == ["g1"]
  681. sent = mock.call_args.args[0]
  682. assert sent.group_id == "g1"
  683. assert sent.before_group_id == "g2"
  684. async def test_admin_move_room_group_to_end(client):
  685. resp = room_layout_pb2.MoveRoomGroupResponse()
  686. mock = _mock_method(client, "admin_room_layout", "move_room_group", resp)
  687. async with client:
  688. await client.admin_move_room_group("g1")
  689. sent = mock.call_args.args[0]
  690. assert sent.group_id == "g1"
  691. assert not sent.HasField("before_group_id")
  692. async def test_admin_move_sidebar_item(client):
  693. resp = room_layout_pb2.MoveSidebarItemResponse()
  694. resp.group.id = "g1"
  695. resp.group.name = "General"
  696. mock = _mock_method(client, "admin_room_layout", "move_sidebar_item", resp)
  697. async with client:
  698. group = await client.admin_move_sidebar_item(
  699. ("sidebar_link", "sl1"),
  700. "g1",
  701. before=("room", "r1"),
  702. )
  703. assert group.id == "g1"
  704. sent = mock.call_args.args[0]
  705. assert sent.item.sidebar_link_id == "sl1"
  706. assert sent.before.room_id == "r1"
  707. assert sent.group_id == "g1"
  708. async def test_admin_get_system_info_livekit(client):
  709. resp = diagnostics_pb2.GetSystemInfoResponse()
  710. resp.projections_available = True
  711. resp.livekit.enabled = True
  712. resp.livekit.configured = True
  713. resp.livekit.url = "wss://livekit.example"
  714. resp.livekit.api_key = "AP1234"
  715. resp.livekit.separate_webhook_key = True
  716. resp.livekit.webhook_url = "https://chat.example/api/admin/livekit/webhook"
  717. resp.livekit.connection_state = diagnostics_pb2.ADMIN_LIVE_KIT_CONNECTION_STATE_OK
  718. resp.livekit.insecure_url = False
  719. _mock_method(client, "admin_diagnostics", "get_system_info", resp)
  720. async with client:
  721. data = await client.admin_get_system_info()
  722. lk = data["livekit"]
  723. assert lk["enabled"] is True
  724. assert lk["configured"] is True
  725. assert lk["url"] == "wss://livekit.example"
  726. assert lk["apiKey"] == "AP1234"
  727. assert lk["separateWebhookKey"] is True
  728. # enums render as their full name in Connect JSON (use_integers_for_enums=False)
  729. assert lk["connectionState"] == "ADMIN_LIVE_KIT_CONNECTION_STATE_OK"
  730. assert (
  731. diagnostics_pb2.AdminLiveKitConnectionState.Value(
  732. "ADMIN_LIVE_KIT_CONNECTION_STATE_UNREACHABLE"
  733. )
  734. == 3
  735. )
  736. assert "connectionError" not in lk # empty string omitted by MessageToDict
  737. async def test_admin_update_server_config(client):
  738. resp = admin_server_pb2.UpdateServerConfigResponse()
  739. resp.config.server_name = "MyServer"
  740. resp.config.motd = "hi"
  741. resp.public_profile.name = "MyServer"
  742. resp.public_profile.version = "0.4.2"
  743. _mock_method(client, "admin_server", "update_server_config", resp)
  744. async with client:
  745. config, profile = await client.admin_update_server_config(server_name="MyServer", motd="hi")
  746. assert config.server_name == "MyServer"
  747. assert profile.version == "0.4.2"
  748. # --- Neighbors (0.5.0-alpha.3) -----------------------------------------
  749. async def test_list_neighbors_public(client):
  750. resp = discovery_server_pb2.ListNeighborsResponse()
  751. resp.origins.extend(["https://a.example", "https://b.example"])
  752. _mock_method(client, "server_discovery", "list_neighbors", resp)
  753. async with client:
  754. origins = await client.list_neighbors()
  755. assert origins == ["https://a.example", "https://b.example"]
  756. async def test_admin_neighbor_crud(client):
  757. list_resp = admin_server_pb2.ListNeighborsResponse()
  758. n = list_resp.neighbors.add()
  759. n.id = "n1"
  760. n.origin = "https://a.example"
  761. n.revision = "rev1"
  762. _mock_method(client, "admin_server", "list_neighbors", list_resp)
  763. get_resp = admin_server_pb2.GetNeighborResponse()
  764. get_resp.neighbor.id = "n1"
  765. get_resp.neighbor.origin = "https://a.example"
  766. get_resp.neighbor.revision = "rev1"
  767. _mock_method(client, "admin_server", "get_neighbor", get_resp)
  768. create_resp = admin_server_pb2.CreateNeighborResponse()
  769. create_resp.neighbor.id = "n2"
  770. create_resp.neighbor.origin = "https://b.example"
  771. create_resp.neighbor.revision = "rev2"
  772. _mock_method(client, "admin_server", "create_neighbor", create_resp)
  773. update_resp = admin_server_pb2.UpdateNeighborResponse()
  774. update_resp.neighbor.id = "n2"
  775. update_resp.neighbor.origin = "https://b2.example"
  776. update_resp.neighbor.revision = "rev3"
  777. _mock_method(client, "admin_server", "update_neighbor", update_resp)
  778. del_resp = admin_server_pb2.DeleteNeighborResponse()
  779. _mock_method(client, "admin_server", "delete_neighbor", del_resp)
  780. async with client:
  781. listed = await client.admin_list_neighbors()
  782. assert [x.origin for x in listed] == ["https://a.example"]
  783. got = await client.admin_get_neighbor("n1")
  784. assert got is not None and got.revision == "rev1"
  785. created = await client.admin_create_neighbor("https://b.example")
  786. assert created.id == "n2" and created.revision == "rev2"
  787. updated = await client.admin_update_neighbor("n2", "https://b2.example", "rev2")
  788. assert updated.origin == "https://b2.example" and updated.revision == "rev3"
  789. await client.admin_delete_neighbor("n2", "rev3")
  790. async def test_update_settings_share_timezone(client):
  791. resp = account_pb2.UpdateSettingsResponse()
  792. resp.settings.timezone = "Europe/Berlin"
  793. resp.settings.share_timezone = True
  794. _mock_method(client, "account", "update_settings", resp)
  795. async with client:
  796. settings = await client.update_settings(timezone="Europe/Berlin", share_timezone=True)
  797. assert settings.timezone == "Europe/Berlin"
  798. assert settings.share_timezone is True
  799. def test_snake_case_is_acronym_aware():
  800. """``types._snake_case`` must not chop acronym runs (same bug class as
  801. the protoc plugin's old ``StartDM`` -> ``start_d_m``)."""
  802. from chattolib.types import _snake_case
  803. assert _snake_case("messagePosted") == "message_posted"
  804. assert _snake_case("directMessageReceived") == "direct_message_received"
  805. # hypothetical acronym field: DM stays whole, boundary before the word after it
  806. assert _snake_case("startDMReceived") == "start_dm_received"
  807. assert _snake_case("getDMStats") == "get_dm_stats"
  808. # --- Message search -----------------------------------------------------
  809. async def test_get_search_status_reports_disabled_with_no_delay(client):
  810. """Search is off by default, so DISABLED is a state a client has to be
  811. able to read — not an error it has to decode from an exception."""
  812. resp = message_search_pb2.GetStatusResponse()
  813. resp.state = message_search_pb2.MessageSearchState.MESSAGE_SEARCH_STATE_DISABLED
  814. _mock_method(client, "message_search", "get_status", resp)
  815. async with client:
  816. status = await client.get_search_status()
  817. assert status.state is MessageSearchState.DISABLED
  818. assert status.retry_after_seconds is None
  819. assert status.is_ready is False
  820. async def test_get_search_status_carries_the_servers_retry_delay(client):
  821. """INDEXING is transient and the server says how long to wait; dropping
  822. that number would leave the caller to invent a backoff of its own."""
  823. resp = message_search_pb2.GetStatusResponse()
  824. resp.state = message_search_pb2.MessageSearchState.MESSAGE_SEARCH_STATE_INDEXING
  825. resp.retry_after.FromJsonString("3.5s")
  826. _mock_method(client, "message_search", "get_status", resp)
  827. async with client:
  828. status = await client.get_search_status()
  829. assert status.state is MessageSearchState.INDEXING
  830. assert status.retry_after_seconds == pytest.approx(3.5)
  831. assert status.is_ready is False
  832. async def test_degraded_search_still_counts_as_ready(client):
  833. """DEGRADED is documented as "can answer with a known non-fatal
  834. limitation", so a readiness check that excludes it refuses working search."""
  835. resp = message_search_pb2.GetStatusResponse()
  836. resp.state = message_search_pb2.MessageSearchState.MESSAGE_SEARCH_STATE_DEGRADED
  837. _mock_method(client, "message_search", "get_status", resp)
  838. async with client:
  839. status = await client.get_search_status()
  840. assert status.is_ready is True
  841. async def test_search_messages_parses_results_and_page_state(client):
  842. resp = message_search_pb2.SearchMessagesResponse()
  843. result = resp.results.add()
  844. result.message.id = "e1"
  845. result.message.room_id = "r1"
  846. result.message.body = "the migration runs on friday"
  847. result.relevance_score = 0.8
  848. resp.next_cursor = "cursor-2"
  849. resp.includes.users["u1"].login = "alice"
  850. _mock_method(client, "message_search", "search_messages", resp)
  851. async with client:
  852. page = await client.search_messages("migration")
  853. assert len(page.results) == 1
  854. assert page.results[0].message is not None
  855. assert page.results[0].message.body == "the migration runs on friday"
  856. assert page.results[0].relevance_score == pytest.approx(0.8)
  857. assert page.results[0].thread_context is None
  858. assert page.next_cursor == "cursor-2"
  859. assert page.thread_total_count is None
  860. assert page.users_by_id["u1"].login == "alice"
  861. async def test_search_messages_parses_thread_grouping(client):
  862. """With THREAD grouping a result stands for a whole thread: the root and
  863. the latest reply live in thread_context, and the message is only its best
  864. match. Reading them as if they were one message misreports the thread."""
  865. resp = message_search_pb2.SearchMessagesResponse()
  866. result = resp.results.add()
  867. result.message.id = "e2"
  868. result.message.body = "a reply that matched"
  869. result.relevance_score = 0.5
  870. result.thread_context.root_message.id = "e1"
  871. result.thread_context.root_message.body = "the root, which did not match"
  872. result.thread_context.room.id = "r1"
  873. result.thread_context.thread.thread_root_event_id = "e1"
  874. result.thread_context.thread.reply_count = 4
  875. result.thread_context.latest_reply.id = "e9"
  876. result.thread_context.latest_reply.body = "newest reply"
  877. resp.thread_total_count = 1
  878. _mock_method(client, "message_search", "search_messages", resp)
  879. async with client:
  880. page = await client.search_messages("reply", group_by=MessageSearchGroupBy.THREAD)
  881. context = page.results[0].thread_context
  882. assert context is not None
  883. assert context.root_message is not None
  884. assert context.root_message.id == "e1"
  885. assert context.room is not None
  886. assert context.room.id == "r1"
  887. assert context.thread is not None
  888. assert context.thread.reply_count == 4
  889. assert context.latest_reply is not None
  890. assert context.latest_reply.id == "e9"
  891. assert page.thread_total_count == 1
  892. async def test_search_messages_sends_the_full_query_shape(client):
  893. """Every optional field has to reach the wire as the protobuf value the
  894. server validates — enums as their pb2 number, timestamps as Timestamp
  895. messages. A silently dropped scope or filter turns a search into a
  896. different, broader one."""
  897. resp = message_search_pb2.SearchMessagesResponse()
  898. mocked = _mock_method(client, "message_search", "search_messages", resp)
  899. async with client:
  900. await client.search_messages(
  901. "deploy plan",
  902. room_id="r1",
  903. author_id="u1",
  904. created_after=dt.datetime(2026, 1, 1, tzinfo=dt.UTC),
  905. created_before=dt.datetime(2026, 2, 1, tzinfo=dt.UTC),
  906. has_attachments=True,
  907. order=MessageSearchOrder.NEWEST,
  908. page_size=25,
  909. cursor="cursor-1",
  910. scope=MessageSearchScope.FOLLOWED_THREADS,
  911. group_by=MessageSearchGroupBy.THREAD,
  912. root_author_filter=ThreadRootAuthorFilter.VIEWER,
  913. )
  914. req = mocked.call_args.args[0]
  915. assert req.query == "deploy plan"
  916. assert req.room_id == "r1"
  917. assert req.author_id == "u1"
  918. assert req.created_after.ToJsonString() == "2026-01-01T00:00:00Z"
  919. assert req.created_before.ToJsonString() == "2026-02-01T00:00:00Z"
  920. assert req.has_attachments is True
  921. assert req.order == message_search_pb2.MessageSearchOrder.MESSAGE_SEARCH_ORDER_NEWEST
  922. assert req.page_size == 25
  923. assert req.cursor == "cursor-1"
  924. assert req.scope == message_search_pb2.MessageSearchScope.MESSAGE_SEARCH_SCOPE_FOLLOWED_THREADS
  925. assert req.group_by == message_search_pb2.MessageSearchGroupBy.MESSAGE_SEARCH_GROUP_BY_THREAD
  926. assert req.root_author_filter == threads_pb2.ThreadRootAuthorFilter.Value(
  927. "THREAD_ROOT_AUTHOR_FILTER_VIEWER"
  928. )
  929. async def test_search_messages_leaves_unset_fields_at_their_defaults(client):
  930. """Optional filters must stay *unset*, not be written as empty strings or
  931. zero timestamps: the server intersects what it receives with what the caller
  932. can read, and an empty filter is not the same as no filter."""
  933. resp = message_search_pb2.SearchMessagesResponse()
  934. mocked = _mock_method(client, "message_search", "search_messages", resp)
  935. async with client:
  936. await client.search_messages("anything")
  937. req = mocked.call_args.args[0]
  938. assert req.query == "anything"
  939. assert req.room_id == ""
  940. assert req.author_id == ""
  941. assert req.has_attachments is False
  942. assert req.page_size == 0
  943. assert req.cursor == ""
  944. assert req.order == message_search_pb2.MessageSearchOrder.MESSAGE_SEARCH_ORDER_UNSPECIFIED
  945. assert req.HasField("created_after") is False