test_client.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  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. from unittest.mock import AsyncMock
  9. import pytest
  10. from chattolib._pb.chatto.admin.v1 import (
  11. room_layout_pb2,
  12. )
  13. from chattolib._pb.chatto.admin.v1 import (
  14. server_pb2 as admin_server_pb2,
  15. )
  16. from chattolib._pb.chatto.api.v1 import (
  17. account_pb2,
  18. asset_uploads_pb2,
  19. attachments_pb2,
  20. member_directory_pb2,
  21. messages_pb2,
  22. notification_preferences_pb2,
  23. notifications_pb2,
  24. presence_pb2,
  25. push_notifications_pb2,
  26. reactions_pb2,
  27. read_state_pb2,
  28. roles_pb2,
  29. room_directory_pb2,
  30. room_timeline_pb2,
  31. rooms_pb2,
  32. threads_pb2,
  33. viewer_pb2,
  34. voice_calls_pb2,
  35. )
  36. from chattolib._pb.chatto.discovery.v1 import server_pb2 as discovery_server_pb2
  37. from chattolib.client import ChattoClient
  38. from chattolib.types import (
  39. AdminRoomLayoutItemKind,
  40. ImageFitMode,
  41. ImageTransformOptions,
  42. NotificationLevel,
  43. PresenceStatus,
  44. RoomKind,
  45. TimeFormat,
  46. )
  47. BASE = "https://chat.chatto.run"
  48. @pytest.fixture
  49. def client():
  50. return ChattoClient(token="test-token", base_url=BASE)
  51. def _mock_method(client: ChattoClient, service: str, method: str, response):
  52. """Replace a service-client method with an AsyncMock returning ``response``."""
  53. svc = getattr(client._svc, service)
  54. setattr(svc, method, AsyncMock(return_value=response))
  55. return getattr(svc, method)
  56. # --- Server discovery ---------------------------------------------------
  57. async def test_get_server(client):
  58. resp = discovery_server_pb2.GetServerResponse()
  59. resp.profile.name = "Chatto HQ"
  60. resp.profile.version = "0.4.2"
  61. resp.login.direct_registration_enabled = True
  62. resp.login.authorize_url = "/oauth"
  63. _mock_method(client, "server_discovery", "get_server", resp)
  64. async with client:
  65. profile, login = await client.get_server()
  66. assert profile.name == "Chatto HQ"
  67. assert profile.version == "0.4.2"
  68. assert login.direct_registration_enabled is True
  69. assert login.authorize_url == "/oauth"
  70. # --- Viewer -------------------------------------------------------------
  71. async def test_me(client):
  72. resp = viewer_pb2.GetViewerResponse()
  73. resp.user.profile.id = "u1"
  74. resp.user.profile.login = "alice"
  75. resp.user.profile.display_name = "Alice"
  76. resp.user.profile.presence_status = "PRESENCE_STATUS_ONLINE"
  77. resp.user.has_verified_email = True
  78. resp.user.settings.timezone = "UTC"
  79. resp.user.settings.time_format = "TIME_FORMAT_24_HOUR"
  80. resp.user.has_password = True
  81. _mock_method(client, "viewer", "get_viewer", resp)
  82. async with client:
  83. user = await client.me()
  84. assert user.id == "u1"
  85. assert user.login == "alice"
  86. assert user.display_name == "Alice"
  87. assert user.presence_status is PresenceStatus.ONLINE
  88. # --- Room directory ----------------------------------------------------
  89. async def test_list_rooms(client):
  90. resp = room_directory_pb2.ListRoomsResponse()
  91. entry = resp.rooms.add()
  92. entry.room.id = "r1"
  93. entry.room.kind = "ROOM_KIND_CHANNEL"
  94. entry.room.name = "general"
  95. entry.room.group_id = "g1"
  96. entry.viewer_state.is_member = True
  97. entry.viewer_state.has_unread = False
  98. _mock_method(client, "room_directory", "list_rooms", resp)
  99. async with client:
  100. rooms = await client.list_rooms()
  101. assert len(rooms) == 1
  102. assert rooms[0].room is not None
  103. assert rooms[0].room.name == "general"
  104. assert rooms[0].room.kind is RoomKind.CHANNEL
  105. assert rooms[0].viewer_state.is_member is True
  106. # --- Messages ----------------------------------------------------------
  107. async def test_post_message(client):
  108. resp = messages_pb2.CreateMessageResponse()
  109. resp.message.id = "e1"
  110. resp.message.room_id = "r1"
  111. resp.message.actor_id = "u1"
  112. resp.message.body = "Hello!"
  113. _mock_method(client, "messages", "create_message", resp)
  114. async with client:
  115. message = await client.post_message("r1", "Hello!")
  116. assert message.id == "e1"
  117. assert message.body == "Hello!"
  118. assert message.room_id == "r1"
  119. async def test_update_message(client):
  120. resp = messages_pb2.UpdateMessageResponse()
  121. resp.message.id = "e1"
  122. resp.message.room_id = "r1"
  123. resp.message.actor_id = "u1"
  124. resp.message.body = "edited"
  125. resp.message.updated_at.FromJsonString("2026-01-01T00:01:00Z")
  126. m = _mock_method(client, "messages", "update_message", resp)
  127. async with client:
  128. msg = await client.update_message("r1", "e1", body="edited")
  129. assert msg.body == "edited"
  130. assert msg.updated_at is not None
  131. # verify the request had the optional body field explicitly set
  132. call_args = m.call_args
  133. req = call_args.args[0]
  134. assert isinstance(req, messages_pb2.UpdateMessageRequest)
  135. assert req.HasField("body")
  136. assert req.body == "edited"
  137. async def test_delete_message(client):
  138. resp = messages_pb2.DeleteMessageResponse(deleted=True)
  139. _mock_method(client, "messages", "delete_message", resp)
  140. async with client:
  141. assert await client.delete_message("r1", "e1") is True
  142. async def test_add_reaction(client):
  143. resp = reactions_pb2.AddReactionResponse(added=True)
  144. _mock_method(client, "messages", "add_reaction", resp)
  145. async with client:
  146. assert await client.add_reaction("r1", "e1", "thumbsup") is True
  147. async def test_remove_reaction(client):
  148. resp = reactions_pb2.RemoveReactionResponse(removed=True)
  149. _mock_method(client, "messages", "remove_reaction", resp)
  150. async with client:
  151. assert await client.remove_reaction("r1", "e1", "thumbsup") is True
  152. # --- Room lifecycle ---------------------------------------------------
  153. async def test_create_room(client):
  154. resp = rooms_pb2.CreateRoomResponse()
  155. resp.room.id = "r1"
  156. resp.room.kind = "ROOM_KIND_CHANNEL"
  157. resp.room.name = "general"
  158. resp.room.group_id = "g1"
  159. _mock_method(client, "rooms", "create_room", resp)
  160. async with client:
  161. room = await client.create_room("general", "g1")
  162. assert room.id == "r1"
  163. assert room.name == "general"
  164. assert room.group_id == "g1"
  165. async def test_archive_room(client):
  166. resp = rooms_pb2.ArchiveRoomResponse()
  167. resp.room.id = "r1"
  168. resp.room.name = "general"
  169. resp.room.archived = True
  170. _mock_method(client, "rooms", "archive_room", resp)
  171. async with client:
  172. room = await client.archive_room("r1")
  173. assert room.archived is True
  174. async def test_ban_member(client):
  175. resp = rooms_pb2.BanMemberResponse(banned=True)
  176. _mock_method(client, "rooms", "ban_member", resp)
  177. async with client:
  178. assert await client.ban_member("r1", "u2", "spam") is True
  179. async def test_start_dm(client):
  180. resp = rooms_pb2.StartDMResponse()
  181. resp.room.id = "r1"
  182. resp.room.kind = "ROOM_KIND_DM"
  183. _mock_method(client, "rooms", "start_dm", resp)
  184. async with client:
  185. room = await client.start_dm(["u1", "u2"])
  186. assert room.id == "r1"
  187. assert room.kind is RoomKind.DM
  188. # --- Room timeline ----------------------------------------------------
  189. async def test_get_room_events(client):
  190. resp = room_timeline_pb2.GetRoomEventsResponse()
  191. event = resp.page.events.add()
  192. event.id = "e1"
  193. event.actor_id = "u1"
  194. event.created_at.FromJsonString("2026-01-01T00:00:00Z")
  195. event.message_posted.message.id = "e1"
  196. event.message_posted.message.room_id = "r1"
  197. event.message_posted.message.actor_id = "u1"
  198. event.message_posted.message.body = "hi"
  199. event.message_posted.message.created_at.FromJsonString("2026-01-01T00:00:00Z")
  200. resp.page.start_cursor = "cA"
  201. resp.page.end_cursor = "cB"
  202. resp.page.includes.users["u1"].id = "u1"
  203. resp.page.includes.users["u1"].login = "alice"
  204. resp.page.includes.users["u1"].display_name = "Alice"
  205. _mock_method(client, "rooms", "get_room_events", resp)
  206. async with client:
  207. page = await client.get_room_events("r1", limit=50)
  208. assert len(page.events) == 1
  209. assert page.events[0].kind == "message_posted"
  210. assert page.events[0].message is not None
  211. assert page.events[0].message.body == "hi"
  212. assert page.users_by_id["u1"].login == "alice"
  213. async def test_mark_room_as_read(client):
  214. resp = read_state_pb2.MarkRoomAsReadResponse()
  215. resp.last_read_at.FromJsonString("2026-01-01T00:00:00Z")
  216. resp.previous_last_read_at.FromJsonString("2025-12-31T00:00:00Z")
  217. _mock_method(client, "rooms", "mark_room_as_read", resp)
  218. async with client:
  219. last, previous = await client.mark_room_as_read("r1", up_to_event_id="e1")
  220. assert last is not None
  221. assert previous is not None
  222. # --- Profile / account ------------------------------------------------
  223. async def test_update_profile(client):
  224. resp = account_pb2.UpdateProfileResponse()
  225. resp.user.id = "u1"
  226. resp.user.login = "newname"
  227. resp.user.display_name = "New Name"
  228. resp.user.presence_status = "PRESENCE_STATUS_ONLINE"
  229. _mock_method(client, "account", "update_profile", resp)
  230. async with client:
  231. user = await client.update_profile(login="newname", display_name="New Name")
  232. assert user.login == "newname"
  233. assert user.display_name == "New Name"
  234. async def test_upload_avatar(client, tmp_path):
  235. resp = account_pb2.UploadAvatarResponse()
  236. resp.user.id = "u1"
  237. resp.user.login = "alice"
  238. resp.user.display_name = "Alice"
  239. resp.user.avatar_url = "https://example.com/avatar.jpg"
  240. mock = _mock_method(client, "account", "upload_avatar", resp)
  241. avatar = tmp_path / "avatar.png"
  242. avatar.write_bytes(b"\x89PNGfake")
  243. async with client:
  244. user = await client.upload_avatar(str(avatar), content_type="image/png")
  245. assert user.avatar_url == "https://example.com/avatar.jpg"
  246. req = mock.call_args.args[0]
  247. assert req.image.image == b"\x89PNGfake"
  248. assert req.image.filename == "avatar.png"
  249. assert req.image.content_type == "image/png"
  250. async def test_update_presence(client):
  251. resp = presence_pb2.UpdatePresenceResponse()
  252. resp.status = "PRESENCE_STATUS_ONLINE"
  253. _mock_method(client, "account", "update_presence", resp)
  254. async with client:
  255. result = await client.update_presence(PresenceStatus.ONLINE)
  256. assert result is PresenceStatus.ONLINE
  257. async def test_update_presence_offline_rejected(client):
  258. async with client:
  259. with pytest.raises(ValueError):
  260. await client.update_presence(PresenceStatus.OFFLINE)
  261. async def test_update_settings(client):
  262. resp = account_pb2.UpdateSettingsResponse()
  263. resp.settings.timezone = "Europe/Rome"
  264. resp.settings.time_format = "TIME_FORMAT_24_HOUR"
  265. _mock_method(client, "account", "update_settings", resp)
  266. async with client:
  267. settings = await client.update_settings(
  268. timezone="Europe/Rome", time_format=TimeFormat.HOUR_24
  269. )
  270. assert settings.timezone == "Europe/Rome"
  271. assert settings.time_format is TimeFormat.HOUR_24
  272. # --- Users -------------------------------------------------------------
  273. async def test_get_user_by_id(client):
  274. resp = member_directory_pb2.GetUserResponse()
  275. resp.user.user.id = "u1"
  276. resp.user.user.login = "alice"
  277. resp.user.user.display_name = "Alice"
  278. resp.user.user.presence_status = "PRESENCE_STATUS_ONLINE"
  279. resp.user.roles.append("everyone")
  280. _mock_method(client, "users", "get_user", resp)
  281. async with client:
  282. member = await client.get_user(user_id="u1")
  283. assert member is not None
  284. assert member.user is not None
  285. assert member.user.login == "alice"
  286. assert "everyone" in member.roles
  287. async def test_get_user_requires_one_target(client):
  288. with pytest.raises(ValueError):
  289. await client.get_user()
  290. with pytest.raises(ValueError):
  291. await client.get_user(user_id="u1", login="alice")
  292. # --- Threads ----------------------------------------------------------
  293. async def test_list_followed_threads(client):
  294. resp = threads_pb2.ListFollowedThreadsResponse()
  295. ft = resp.threads.add()
  296. ft.room.id = "r1"
  297. ft.room.name = "general"
  298. ft.root_message.id = "e1"
  299. ft.root_message.room_id = "r1"
  300. ft.root_message.actor_id = "u1"
  301. ft.root_message.body = "root"
  302. ft.root_message.created_at.FromJsonString("2026-01-01T00:00:00Z")
  303. ft.thread.thread_root_event_id = "e1"
  304. ft.thread.reply_count = 3
  305. ft.thread.last_reply_at.FromJsonString("2026-01-02T00:00:00Z")
  306. ft.thread.viewer_state.is_following = True
  307. ft.thread.viewer_state.has_unread = False
  308. resp.page.total_count = 1
  309. resp.page.has_more = False
  310. _mock_method(client, "threads", "list_followed_threads", resp)
  311. async with client:
  312. page = await client.list_followed_threads()
  313. assert page.page.total_count == 1
  314. assert page.threads[0].thread is not None
  315. assert page.threads[0].thread.reply_count == 3
  316. assert page.threads[0].thread.is_following is True
  317. async def test_follow_and_unfollow_thread(client):
  318. follow_resp = threads_pb2.FollowThreadResponse(following=True)
  319. unfollow_resp = threads_pb2.UnfollowThreadResponse(following=False)
  320. _mock_method(client, "threads", "follow_thread", follow_resp)
  321. _mock_method(client, "threads", "unfollow_thread", unfollow_resp)
  322. async with client:
  323. assert await client.follow_thread("r1", "e1") is True
  324. assert await client.unfollow_thread("r1", "e1") is False
  325. # --- Notifications ----------------------------------------------------
  326. async def test_list_notification_occurrences(client):
  327. resp = notifications_pb2.ListNotificationOccurrencesResponse()
  328. occ = resp.occurrences.add()
  329. occ.id = "n1"
  330. occ.created_at.FromJsonString("2026-01-01T00:00:00Z")
  331. occ.actor.id = "u1"
  332. occ.actor.login = "alice"
  333. occ.actor.display_name = "Alice"
  334. occ.unread = True
  335. sig = occ.signal.direct_mention_received
  336. sig.message.room.id = "r1"
  337. sig.message.room.name = "general"
  338. sig.message.event_id = "e1"
  339. resp.page.total_count = 1
  340. resp.unread_count = 1
  341. _mock_method(client, "notifications", "list_notification_occurrences", resp)
  342. async with client:
  343. page = await client.list_notification_occurrences()
  344. assert page.page.total_count == 1
  345. assert page.unread_count == 1
  346. assert page.occurrences[0].signal is not None
  347. assert page.occurrences[0].signal.kind == "direct_mention_received"
  348. assert page.occurrences[0].signal.message is not None
  349. assert page.occurrences[0].signal.message.event_id == "e1"
  350. assert page.occurrences[0].signal.message.room is not None
  351. assert page.occurrences[0].signal.message.room.name == "general"
  352. async def test_mark_and_delete_notification_occurrence(client):
  353. read_resp = notifications_pb2.MarkNotificationReadResponse()
  354. read_resp.occurrence.id = "n1"
  355. read_resp.occurrence.unread = False
  356. _mock_method(client, "notifications", "mark_notification_read", read_resp)
  357. del_resp = notifications_pb2.DeleteNotificationOccurrenceResponse(deleted=True)
  358. _mock_method(client, "notifications", "delete_notification_occurrence", del_resp)
  359. all_resp = notifications_pb2.DeleteAllNotificationOccurrencesResponse(deleted_count=3)
  360. _mock_method(client, "notifications", "delete_all_notification_occurrences", all_resp)
  361. async with client:
  362. occ = await client.mark_notification_read("n1")
  363. assert occ is not None and occ.unread is False
  364. assert await client.delete_notification_occurrence("n1") is True
  365. assert await client.delete_all_notification_occurrences() == 3
  366. async def test_notification_preference(client):
  367. resp = notification_preferences_pb2.UpdateNotificationPreferenceResponse()
  368. resp.preference.level = "NOTIFICATION_LEVEL_MUTED"
  369. resp.preference.effective_level = "NOTIFICATION_LEVEL_MUTED"
  370. _mock_method(
  371. client,
  372. "notification_prefs",
  373. "update_room_notification_preference",
  374. resp,
  375. )
  376. async with client:
  377. pref = await client.update_room_notification_preference("r1", NotificationLevel.MUTED)
  378. assert pref.level is NotificationLevel.MUTED
  379. assert pref.effective_level is NotificationLevel.MUTED
  380. # --- Push -------------------------------------------------------------
  381. async def test_subscribe_push(client):
  382. resp = push_notifications_pb2.SubscribePushResponse(subscribed=True)
  383. _mock_method(client, "push", "subscribe", resp)
  384. async with client:
  385. assert await client.subscribe_push("https://push", "k", "a") is True
  386. # --- Voice calls ------------------------------------------------------
  387. async def test_active_calls(client):
  388. resp = voice_calls_pb2.ListActiveCallsResponse()
  389. call = resp.calls.add()
  390. call.call_id = "c1"
  391. call.room.id = "r1"
  392. call.room.name = "voice"
  393. p = call.participants.add()
  394. p.user.id = "u1"
  395. p.user.login = "alice"
  396. p.user.display_name = "Alice"
  397. _mock_method(client, "voice_calls", "list_active_calls", resp)
  398. async with client:
  399. calls = await client.list_active_calls()
  400. assert len(calls) == 1
  401. assert calls[0].call_id == "c1"
  402. assert calls[0].room is not None
  403. assert calls[0].room.name == "voice"
  404. # --- Assets -----------------------------------------------------------
  405. async def test_get_asset_with_thumbnail(client):
  406. resp = attachments_pb2.GetAssetResponse()
  407. resp.asset.id = "a1"
  408. resp.asset.filename = "pic.png"
  409. resp.asset.content_type = "image/png"
  410. resp.asset.size = 1024
  411. resp.asset.asset_url.url = "https://cdn/pic.png"
  412. mock = _mock_method(client, "assets", "get_asset", resp)
  413. async with client:
  414. asset = await client.get_asset(
  415. "r1",
  416. "a1",
  417. thumbnail=ImageTransformOptions(128, 128, ImageFitMode.COVER),
  418. )
  419. assert asset is not None
  420. assert asset.filename == "pic.png"
  421. assert asset.asset_url is not None
  422. assert asset.asset_url.url == "https://cdn/pic.png"
  423. # verify the request encoded thumbnail options
  424. req = mock.call_args.args[0]
  425. assert req.thumbnail.width == 128
  426. assert req.thumbnail.height == 128
  427. # --- Roles ------------------------------------------------------------
  428. async def test_list_roles(client):
  429. resp = roles_pb2.ListRolesResponse()
  430. r1 = resp.roles.add()
  431. r1.name = "everyone"
  432. r1.display_name = "Everyone"
  433. r1.is_system = True
  434. r1.position = 0
  435. r2 = resp.roles.add()
  436. r2.name = "admin"
  437. r2.display_name = "Admin"
  438. r2.position = 10
  439. _mock_method(client, "roles", "list_roles", resp)
  440. async with client:
  441. roles = await client.list_roles()
  442. assert [r.name for r in roles] == ["everyone", "admin"]
  443. assert roles[0].is_system is True
  444. # --- Asset uploads ---------------------------------------------------
  445. async def test_asset_upload_end_to_end(client, tmp_path):
  446. import hashlib
  447. payload = b"hello world" * 100
  448. file = tmp_path / "greet.txt"
  449. file.write_bytes(payload)
  450. sha = hashlib.sha256(payload).hexdigest()
  451. create_resp = asset_uploads_pb2.CreateUploadResponse()
  452. create_resp.upload.upload_id = "up1"
  453. create_resp.upload.room_id = "r1"
  454. create_resp.upload.status = "ASSET_UPLOAD_STATUS_OPEN"
  455. create_resp.upload.committed_offset = 0
  456. create_resp.upload.size = len(payload)
  457. create_resp.upload.max_chunk_size = 4096
  458. create_resp.upload.sha256 = sha
  459. create_mock = _mock_method(client, "asset_uploads", "create_upload", create_resp)
  460. chunk_resp = asset_uploads_pb2.UploadChunkResponse()
  461. chunk_resp.upload.upload_id = "up1"
  462. chunk_resp.upload.room_id = "r1"
  463. chunk_resp.upload.status = "ASSET_UPLOAD_STATUS_OPEN"
  464. chunk_resp.upload.committed_offset = len(payload)
  465. chunk_resp.upload.size = len(payload)
  466. chunk_resp.upload.max_chunk_size = 4096
  467. chunk_resp.upload.sha256 = sha
  468. _mock_method(client, "asset_uploads", "upload_chunk", chunk_resp)
  469. complete_resp = asset_uploads_pb2.CompleteUploadResponse()
  470. complete_resp.upload.upload_id = "up1"
  471. complete_resp.upload.room_id = "r1"
  472. complete_resp.upload.status = "ASSET_UPLOAD_STATUS_COMPLETED"
  473. complete_resp.upload.committed_offset = len(payload)
  474. complete_resp.upload.size = len(payload)
  475. complete_resp.upload.sha256 = sha
  476. complete_resp.upload.asset_id = "a1"
  477. complete_resp.asset.id = "a1"
  478. complete_resp.asset.filename = "greet.txt"
  479. complete_resp.asset.content_type = "text/plain"
  480. complete_resp.asset.size = len(payload)
  481. _mock_method(client, "asset_uploads", "complete_upload", complete_resp)
  482. async with client:
  483. asset = await client.upload_attachment("r1", file, content_type="text/plain")
  484. assert asset.id == "a1"
  485. assert asset.filename == "greet.txt"
  486. create_req = create_mock.call_args.args[0]
  487. assert create_req.sha256 == sha
  488. assert create_req.size == len(payload)
  489. # --- External identities --------------------------------------------
  490. # --- Admin (structural smoke tests) ----------------------------------
  491. async def test_admin_list_room_groups(client):
  492. resp = room_layout_pb2.ListRoomGroupsResponse()
  493. g = resp.groups.add()
  494. g.id = "g1"
  495. g.name = "General"
  496. room_item = g.items.add()
  497. room_item.room.id = "r1"
  498. room_item.room.kind = "ROOM_KIND_CHANNEL"
  499. room_item.room.name = "chat"
  500. link_item = g.items.add()
  501. link_item.sidebar_link.id = "sl1"
  502. link_item.sidebar_link.label = "Docs"
  503. link_item.sidebar_link.url = "https://x"
  504. g.can_create_room = True
  505. _mock_method(client, "admin_room_layout", "list_room_groups", resp)
  506. async with client:
  507. groups = await client.admin_list_room_groups()
  508. assert len(groups) == 1
  509. assert groups[0].name == "General"
  510. assert groups[0].rooms[0].id == "r1"
  511. assert groups[0].sidebar_links[0].label == "Docs"
  512. assert groups[0].can_create_room is True
  513. async def test_admin_create_sidebar_link(client):
  514. resp = room_layout_pb2.CreateSidebarLinkResponse()
  515. resp.sidebar_link.id = "sl1"
  516. resp.sidebar_link.label = "Docs"
  517. resp.sidebar_link.url = "https://x"
  518. _mock_method(client, "admin_room_layout", "create_sidebar_link", resp)
  519. async with client:
  520. result = await client.admin_create_sidebar_link("g1", "Docs", "https://x")
  521. assert result == {"id": "sl1", "label": "Docs", "url": "https://x"}
  522. async def test_admin_move_room_group(client):
  523. resp = room_layout_pb2.MoveRoomGroupResponse()
  524. g = resp.groups.add()
  525. g.id = "g1"
  526. g.name = "General"
  527. mock = _mock_method(client, "admin_room_layout", "move_room_group", resp)
  528. async with client:
  529. groups = await client.admin_move_room_group("g1", before_group_id="g2")
  530. assert [g.id for g in groups] == ["g1"]
  531. sent = mock.call_args.args[0]
  532. assert sent.group_id == "g1"
  533. assert sent.before_group_id == "g2"
  534. async def test_admin_move_room_group_to_end(client):
  535. resp = room_layout_pb2.MoveRoomGroupResponse()
  536. mock = _mock_method(client, "admin_room_layout", "move_room_group", resp)
  537. async with client:
  538. await client.admin_move_room_group("g1")
  539. sent = mock.call_args.args[0]
  540. assert sent.group_id == "g1"
  541. assert not sent.HasField("before_group_id")
  542. async def test_admin_move_sidebar_item(client):
  543. resp = room_layout_pb2.MoveSidebarItemResponse()
  544. resp.group.id = "g1"
  545. resp.group.name = "General"
  546. mock = _mock_method(client, "admin_room_layout", "move_sidebar_item", resp)
  547. async with client:
  548. group = await client.admin_move_sidebar_item(
  549. (AdminRoomLayoutItemKind.SIDEBAR_LINK, "sl1"),
  550. "g1",
  551. before=(AdminRoomLayoutItemKind.ROOM, "r1"),
  552. )
  553. assert group.id == "g1"
  554. sent = mock.call_args.args[0]
  555. item_kind = room_layout_pb2.AdminRoomLayoutItemKind
  556. assert sent.item.id == "sl1"
  557. assert sent.item.kind == item_kind.ADMIN_ROOM_LAYOUT_ITEM_KIND_SIDEBAR_LINK
  558. assert sent.before.id == "r1"
  559. assert sent.before.kind == item_kind.ADMIN_ROOM_LAYOUT_ITEM_KIND_ROOM
  560. assert sent.group_id == "g1"
  561. async def test_admin_update_server_config(client):
  562. resp = admin_server_pb2.UpdateServerConfigResponse()
  563. resp.config.server_name = "MyServer"
  564. resp.config.motd = "hi"
  565. resp.public_profile.name = "MyServer"
  566. resp.public_profile.version = "0.4.2"
  567. _mock_method(client, "admin_server", "update_server_config", resp)
  568. async with client:
  569. config, profile = await client.admin_update_server_config(server_name="MyServer", motd="hi")
  570. assert config.server_name == "MyServer"
  571. assert profile.version == "0.4.2"
  572. # --- Neighbors (0.5.0-alpha.3) -----------------------------------------
  573. async def test_list_neighbors_public(client):
  574. resp = discovery_server_pb2.ListNeighborsResponse()
  575. resp.origins.extend(["https://a.example", "https://b.example"])
  576. _mock_method(client, "server_discovery", "list_neighbors", resp)
  577. async with client:
  578. origins = await client.list_neighbors()
  579. assert origins == ["https://a.example", "https://b.example"]
  580. async def test_admin_neighbor_crud(client):
  581. list_resp = admin_server_pb2.ListNeighborsResponse()
  582. n = list_resp.neighbors.add()
  583. n.id = "n1"
  584. n.origin = "https://a.example"
  585. n.revision = "rev1"
  586. _mock_method(client, "admin_server", "list_neighbors", list_resp)
  587. get_resp = admin_server_pb2.GetNeighborResponse()
  588. get_resp.neighbor.id = "n1"
  589. get_resp.neighbor.origin = "https://a.example"
  590. get_resp.neighbor.revision = "rev1"
  591. _mock_method(client, "admin_server", "get_neighbor", get_resp)
  592. create_resp = admin_server_pb2.CreateNeighborResponse()
  593. create_resp.neighbor.id = "n2"
  594. create_resp.neighbor.origin = "https://b.example"
  595. create_resp.neighbor.revision = "rev2"
  596. _mock_method(client, "admin_server", "create_neighbor", create_resp)
  597. update_resp = admin_server_pb2.UpdateNeighborResponse()
  598. update_resp.neighbor.id = "n2"
  599. update_resp.neighbor.origin = "https://b2.example"
  600. update_resp.neighbor.revision = "rev3"
  601. _mock_method(client, "admin_server", "update_neighbor", update_resp)
  602. del_resp = admin_server_pb2.DeleteNeighborResponse()
  603. _mock_method(client, "admin_server", "delete_neighbor", del_resp)
  604. async with client:
  605. listed = await client.admin_list_neighbors()
  606. assert [x.origin for x in listed] == ["https://a.example"]
  607. got = await client.admin_get_neighbor("n1")
  608. assert got is not None and got.revision == "rev1"
  609. created = await client.admin_create_neighbor("https://b.example")
  610. assert created.id == "n2" and created.revision == "rev2"
  611. updated = await client.admin_update_neighbor("n2", "https://b2.example", "rev2")
  612. assert updated.origin == "https://b2.example" and updated.revision == "rev3"
  613. await client.admin_delete_neighbor("n2", "rev3")
  614. async def test_update_settings_share_timezone(client):
  615. resp = account_pb2.UpdateSettingsResponse()
  616. resp.settings.timezone = "Europe/Berlin"
  617. resp.settings.share_timezone = True
  618. _mock_method(client, "account", "update_settings", resp)
  619. async with client:
  620. settings = await client.update_settings(timezone="Europe/Berlin", share_timezone=True)
  621. assert settings.timezone == "Europe/Berlin"
  622. assert settings.share_timezone is True