test_client.py 23 KB

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