"""Tests for ChattoClient. Since the client now goes through generated ConnectRPC service stubs, tests mock at the service-client method level with ``AsyncMock`` rather than at the HTTP transport layer. This exercises the request-building and response-parsing paths without any real network traffic. """ from __future__ import annotations import datetime as dt from unittest.mock import AsyncMock import pytest from chattolib._pb.chatto.admin.v1 import ( diagnostics_pb2, room_layout_pb2, ) from chattolib._pb.chatto.admin.v1 import ( server_pb2 as admin_server_pb2, ) from chattolib._pb.chatto.api.v1 import ( account_pb2, asset_uploads_pb2, attachments_pb2, messages_pb2, notification_preferences_pb2, notifications_pb2, presence_pb2, push_notifications_pb2, reactions_pb2, read_state_pb2, roles_pb2, room_directory_pb2, room_timeline_pb2, rooms_pb2, threads_pb2, user_service_pb2, viewer_pb2, voice_calls_pb2, ) from chattolib._pb.chatto.discovery.v1 import server_pb2 as discovery_server_pb2 from chattolib.client import ChattoClient from chattolib.types import ( ImageFitMode, ImageTransformOptions, NotificationLevel, PresenceStatus, RoomKind, ThreadRootAuthorFilter, TimeFormat, ) BASE = "https://chat.chatto.run" @pytest.fixture def client(): return ChattoClient(token="test-token", base_url=BASE) def _mock_method(client: ChattoClient, service: str, method: str, response): """Replace a service-client method with an AsyncMock returning ``response``.""" svc = getattr(client._svc, service) setattr(svc, method, AsyncMock(return_value=response)) return getattr(svc, method) # --- Server discovery --------------------------------------------------- async def test_get_server(client): resp = discovery_server_pb2.GetServerResponse() resp.profile.name = "Chatto HQ" resp.profile.version = "0.4.2" resp.login.direct_registration_enabled = True resp.login.authorize_url = "/oauth" _mock_method(client, "server_discovery", "get_server", resp) async with client: profile, login = await client.get_server() assert profile.name == "Chatto HQ" assert profile.version == "0.4.2" assert login.direct_registration_enabled is True assert login.authorize_url == "/oauth" async def test_list_neighborhood_servers(client): resp = discovery_server_pb2.ListNeighborhoodServersResponse() s1 = resp.servers.add() s1.origin = "https://neighbor.example" s1.profile.name = "Neighbor" s1.profile.version = "0.5.0" s1.profile.logo_url = "/logos/neighbor.png" s1.direct_neighbor = True s2 = resp.servers.add() s2.origin = "https://referred.example" s2.profile.name = "Referred" s2.direct_neighbor = False s2.recommended_by_origins.append("https://neighbor.example") resp.refreshed_at.FromJsonString("2026-09-26T12:00:00Z") mocked = _mock_method(client, "server_discovery", "list_neighborhood_servers", resp) async with client: page = await client.list_neighborhood_servers() mocked.assert_awaited_once() assert [s.origin for s in page.servers] == [ "https://neighbor.example", "https://referred.example", ] assert page.servers[0].profile is not None assert page.servers[0].profile.name == "Neighbor" assert page.servers[0].profile.logo_url == "/logos/neighbor.png" assert page.servers[0].direct_neighbor is True assert page.servers[1].direct_neighbor is False assert page.servers[1].recommended_by_origins == ["https://neighbor.example"] assert page.refreshed_at == dt.datetime(2026, 9, 26, 12, 0, tzinfo=dt.UTC) # --- Viewer ------------------------------------------------------------- async def test_me(client): resp = viewer_pb2.GetViewerResponse() resp.user.profile.id = "u1" resp.user.profile.login = "alice" resp.user.profile.display_name = "Alice" resp.user.profile.presence_status = "PRESENCE_STATUS_ONLINE" resp.user.has_verified_email = True resp.user.settings.timezone = "UTC" resp.user.settings.time_format = "TIME_FORMAT_24_HOUR" resp.user.has_password = True _mock_method(client, "viewer", "get_viewer", resp) async with client: user = await client.me() assert user.id == "u1" assert user.login == "alice" assert user.display_name == "Alice" assert user.presence_status is PresenceStatus.ONLINE # --- Room directory ---------------------------------------------------- async def test_list_rooms(client): resp = room_directory_pb2.ListRoomsResponse() entry = resp.rooms.add() entry.room.id = "r1" entry.room.kind = "ROOM_KIND_CHANNEL" entry.room.name = "general" entry.room.group_id = "g1" entry.viewer_state.is_member = True entry.viewer_state.has_unread = False _mock_method(client, "room_directory", "list_rooms", resp) async with client: rooms = await client.list_rooms() assert len(rooms) == 1 assert rooms[0].room is not None assert rooms[0].room.name == "general" assert rooms[0].room.kind is RoomKind.CHANNEL assert rooms[0].viewer_state.is_member is True async def test_list_rooms_dm_keeps_deleted_participants(client): # Since Chatto 0.5.0-beta.10, a DM's member_user_ids keeps participants # whose accounts were deleted; the client must surface the full list # (resolution to a User then legitimately fails for the deleted one). resp = room_directory_pb2.ListRoomsResponse() entry = resp.rooms.add() entry.room.id = "r-dm" entry.room.kind = "ROOM_KIND_DM" entry.room.name = "me & u2" entry.viewer_state.is_member = True entry.member_user_ids.extend(["u1", "u-deleted"]) _mock_method(client, "room_directory", "list_rooms", resp) async with client: rooms = await client.list_rooms() assert len(rooms) == 1 assert rooms[0].room is not None assert rooms[0].room.kind is RoomKind.DM assert rooms[0].member_user_ids == ["u1", "u-deleted"] # --- Messages ---------------------------------------------------------- async def test_post_message(client): resp = messages_pb2.CreateMessageResponse() resp.message.id = "e1" resp.message.room_id = "r1" resp.message.actor_id = "u1" resp.message.body = "Hello!" _mock_method(client, "messages", "create_message", resp) async with client: message = await client.post_message("r1", "Hello!") assert message.id == "e1" assert message.body == "Hello!" assert message.room_id == "r1" async def test_update_message(client): resp = messages_pb2.UpdateMessageResponse() resp.message.id = "e1" resp.message.room_id = "r1" resp.message.actor_id = "u1" resp.message.body = "edited" resp.message.updated_at.FromJsonString("2026-01-01T00:01:00Z") m = _mock_method(client, "messages", "update_message", resp) async with client: msg = await client.update_message("r1", "e1", body="edited") assert msg.body == "edited" assert msg.updated_at is not None # verify the request had the optional body field explicitly set call_args = m.call_args req = call_args.args[0] assert isinstance(req, messages_pb2.UpdateMessageRequest) assert req.HasField("body") assert req.body == "edited" async def test_delete_message(client): _mock_method(client, "messages", "delete_message", messages_pb2.DeleteMessageResponse()) async with client: assert await client.delete_message("r1", "e1") is None async def test_add_reaction(client): resp = reactions_pb2.AddReactionResponse(added=True) _mock_method(client, "messages", "add_reaction", resp) async with client: assert await client.add_reaction("r1", "e1", "thumbsup") is True async def test_remove_reaction(client): resp = reactions_pb2.RemoveReactionResponse(removed=True) _mock_method(client, "messages", "remove_reaction", resp) async with client: assert await client.remove_reaction("r1", "e1", "thumbsup") is True # --- Room lifecycle --------------------------------------------------- async def test_create_room(client): resp = rooms_pb2.CreateRoomResponse() resp.room.id = "r1" resp.room.kind = "ROOM_KIND_CHANNEL" resp.room.name = "general" resp.room.group_id = "g1" _mock_method(client, "rooms", "create_room", resp) async with client: room = await client.create_room("general", "g1") assert room.id == "r1" assert room.name == "general" assert room.group_id == "g1" async def test_archive_room(client): resp = rooms_pb2.ArchiveRoomResponse() resp.room.id = "r1" resp.room.name = "general" resp.room.archived = True _mock_method(client, "rooms", "archive_room", resp) async with client: room = await client.archive_room("r1") assert room.archived is True async def test_remove_user(client): resp = rooms_pb2.RemoveUserResponse() mock = _mock_method(client, "rooms", "remove_user", resp) async with client: await client.remove_user("r1", "u2", "spam") await client.remove_user( "r1", "u2", "spam", suspension_expires_at=dt.datetime.fromtimestamp(0, dt.UTC) ) await client.remove_user("r1", "u2", "spam", suspend_indefinitely=True) calls = [c.args[0] for c in mock.call_args_list] assert calls[0].WhichOneof("suspension") is None assert calls[1].WhichOneof("suspension") == "suspension_expires_at" assert calls[2].WhichOneof("suspension") == "suspend_indefinitely" async def test_remove_user_rejects_both_suspensions(client): _mock_method(client, "rooms", "remove_user", rooms_pb2.RemoveUserResponse()) async with client: with pytest.raises(ValueError, match="not both"): await client.remove_user( "r1", "u2", "spam", suspension_expires_at=dt.datetime.fromtimestamp(0, dt.UTC), suspend_indefinitely=True, ) async def test_lift_suspension(client): _mock_method(client, "rooms", "lift_suspension", rooms_pb2.LiftSuspensionResponse()) async with client: assert await client.lift_suspension("r1", "u2", "reinstated") is None async def test_list_suspensions(client): resp = rooms_pb2.ListSuspensionsResponse() s = resp.suspensions.add() s.id = "s1" s.room_id = "r1" s.user_id = "u2" _mock_method(client, "rooms", "list_suspensions", resp) async with client: suspensions, _ = await client.list_suspensions(room_id="r1") assert [x.id for x in suspensions] == ["s1"] async def test_start_dm(client): resp = rooms_pb2.StartDMResponse() resp.room.id = "r1" resp.room.kind = "ROOM_KIND_DM" _mock_method(client, "rooms", "start_dm", resp) async with client: room = await client.start_dm(["u1", "u2"]) assert room.id == "r1" assert room.kind is RoomKind.DM # --- Room timeline ---------------------------------------------------- async def test_get_room_events(client): resp = room_timeline_pb2.GetRoomEventsResponse() event = resp.page.events.add() event.id = "e1" event.actor_id = "u1" event.created_at.FromJsonString("2026-01-01T00:00:00Z") event.message_posted.message.id = "e1" event.message_posted.message.room_id = "r1" event.message_posted.message.actor_id = "u1" event.message_posted.message.body = "hi" event.message_posted.message.created_at.FromJsonString("2026-01-01T00:00:00Z") resp.page.start_cursor = "cA" resp.page.end_cursor = "cB" resp.page.includes.users["u1"].id = "u1" resp.page.includes.users["u1"].login = "alice" resp.page.includes.users["u1"].display_name = "Alice" _mock_method(client, "rooms", "get_room_events", resp) async with client: page = await client.get_room_events("r1", limit=50) assert len(page.events) == 1 assert page.events[0].kind == "message_posted" assert page.events[0].message is not None assert page.events[0].message.body == "hi" assert page.users_by_id["u1"].login == "alice" async def test_mark_room_as_read(client): resp = read_state_pb2.MarkRoomAsReadResponse() resp.last_read_at.FromJsonString("2026-01-01T00:00:00Z") resp.previous_last_read_at.FromJsonString("2025-12-31T00:00:00Z") _mock_method(client, "rooms", "mark_room_as_read", resp) async with client: last, previous = await client.mark_room_as_read("r1", up_to_event_id="e1") assert last is not None assert previous is not None # --- Profile / account ------------------------------------------------ async def test_update_user_profile(client): resp = user_service_pb2.UpdateUserProfileResponse() resp.user.id = "u1" resp.user.login = "newname" resp.user.display_name = "New Name" resp.user.bio = "# Hi" resp.user.presence_status = "PRESENCE_STATUS_ONLINE" _mock_method(client, "user", "update_user_profile", resp) async with client: user = await client.update_user_profile( "u1", login="newname", display_name="New Name", bio="# Hi" ) assert user.id == "u1" assert user.login == "newname" assert user.display_name == "New Name" assert user.bio == "# Hi" async def test_update_profile(client): """Self-service update resolves the caller id via the viewer, then delegates to the new UserService.UpdateUserProfile wire call.""" viewer = viewer_pb2.GetViewerResponse() viewer.user.profile.id = "u1" viewer.user.profile.login = "alice" viewer.user.profile.display_name = "Alice" _mock_method(client, "viewer", "get_viewer", viewer) resp = user_service_pb2.UpdateUserProfileResponse() resp.user.id = "u1" resp.user.login = "newname" resp.user.display_name = "New Name" resp.user.presence_status = "PRESENCE_STATUS_ONLINE" update = _mock_method(client, "user", "update_user_profile", resp) async with client: user = await client.update_profile(login="newname", display_name="New Name") assert user.login == "newname" assert user.display_name == "New Name" (req,) = update.await_args.args assert req.user_id == "u1" async def test_set_presence(client): resp = presence_pb2.SetPresenceResponse() resp.status = "PRESENCE_STATUS_ONLINE" _mock_method(client, "account", "set_presence", resp) async with client: result = await client.set_presence(PresenceStatus.ONLINE) assert result is PresenceStatus.ONLINE async def test_set_presence_offline_rejected(client): async with client: with pytest.raises(ValueError): await client.set_presence(PresenceStatus.OFFLINE) async def test_get_presence_preference(client): resp = presence_pb2.GetPresencePreferenceResponse() resp.preference.status = "PRESENCE_STATUS_OFFLINE" resp.preference.revision = "rev-1" _mock_method(client, "account", "get_presence_preference", resp) async with client: pref = await client.get_presence_preference() assert pref is not None assert pref.status is PresenceStatus.OFFLINE assert pref.revision == "rev-1" async def test_get_presence_preference_absent(client): # No preference saved yet: the response carries no sub-message. _mock_method( client, "account", "get_presence_preference", presence_pb2.GetPresencePreferenceResponse() ) async with client: assert await client.get_presence_preference() is None async def test_set_presence_preference(client): resp = presence_pb2.SetPresencePreferenceResponse() resp.preference.status = "PRESENCE_STATUS_DO_NOT_DISTURB" resp.preference.revision = "rev-2" m = _mock_method(client, "account", "set_presence_preference", resp) async with client: pref = await client.set_presence_preference( PresenceStatus.DO_NOT_DISTURB, expected_revision="rev-1" ) assert pref.status is PresenceStatus.DO_NOT_DISTURB assert pref.revision == "rev-2" req = m.await_args.args[0] assert req.status == presence_pb2.PresenceStatus.Value("PRESENCE_STATUS_DO_NOT_DISTURB") assert req.expected_revision == "rev-1" async def test_set_presence_preference_offline_allowed(client): # Unlike set_presence, OFFLINE is a valid saved choice ("Appear Offline"). resp = presence_pb2.SetPresencePreferenceResponse() resp.preference.status = "PRESENCE_STATUS_OFFLINE" _mock_method(client, "account", "set_presence_preference", resp) async with client: assert (await client.set_presence_preference(PresenceStatus.OFFLINE)).status is ( PresenceStatus.OFFLINE ) async def test_set_presence_preference_unspecified_rejected(client): async with client: with pytest.raises(ValueError): await client.set_presence_preference(PresenceStatus.UNSPECIFIED) async def test_refresh_presence(client): resp = presence_pb2.RefreshPresenceResponse() resp.preference.status = "PRESENCE_STATUS_ONLINE" resp.preference.revision = "rev-3" _mock_method(client, "account", "refresh_presence", resp) async with client: pref = await client.refresh_presence() assert pref is not None assert pref.status is PresenceStatus.ONLINE assert pref.revision == "rev-3" async def test_update_settings(client): resp = account_pb2.UpdateSettingsResponse() resp.settings.timezone = "Europe/Rome" resp.settings.time_format = "TIME_FORMAT_24_HOUR" _mock_method(client, "account", "update_settings", resp) async with client: settings = await client.update_settings( timezone="Europe/Rome", time_format=TimeFormat.HOUR_24 ) assert settings.timezone == "Europe/Rome" assert settings.time_format is TimeFormat.HOUR_24 # --- Threads ---------------------------------------------------------- async def test_list_followed_threads(client): resp = threads_pb2.ListFollowedThreadsResponse() ft = resp.threads.add() ft.room.id = "r1" ft.room.name = "general" ft.root_message.id = "e1" ft.root_message.room_id = "r1" ft.root_message.actor_id = "u1" ft.root_message.body = "root" ft.root_message.created_at.FromJsonString("2026-01-01T00:00:00Z") ft.thread.thread_root_event_id = "e1" ft.thread.reply_count = 3 ft.thread.last_reply_at.FromJsonString("2026-01-02T00:00:00Z") ft.thread.viewer_state.is_following = True ft.thread.viewer_state.has_unread_replies = False resp.page.total_count = 1 resp.page.has_more = False mocked = _mock_method(client, "threads", "list_followed_threads", resp) async with client: page = await client.list_followed_threads() await client.list_followed_threads( limit=5, offset=5, include_direct_message_threads=True, unread_only=True, root_author_filter=ThreadRootAuthorFilter.VIEWER, ) await client.list_followed_threads( root_author_filter=ThreadRootAuthorFilter.OTHERS, ) assert page.page.total_count == 1 assert page.threads[0].thread is not None assert page.threads[0].thread.reply_count == 3 assert page.threads[0].thread.is_following is True calls = [c.args[0] for c in mocked.call_args_list] assert calls[0].include_direct_message_threads is False assert calls[0].unread_only is False assert calls[0].root_author_filter == 0 assert calls[1].include_direct_message_threads is True assert calls[1].unread_only is True assert calls[1].root_author_filter == threads_pb2.ThreadRootAuthorFilter.Value( "THREAD_ROOT_AUTHOR_FILTER_VIEWER" ) assert calls[2].root_author_filter == threads_pb2.ThreadRootAuthorFilter.Value( "THREAD_ROOT_AUTHOR_FILTER_OTHERS" ) assert calls[1].page.limit == 5 assert calls[1].page.offset == 5 async def test_follow_and_unfollow_thread(client): _mock_method(client, "threads", "follow_thread", threads_pb2.FollowThreadResponse()) _mock_method(client, "threads", "unfollow_thread", threads_pb2.UnfollowThreadResponse()) async with client: assert await client.follow_thread("r1", "e1") is None assert await client.unfollow_thread("r1", "e1") is None # --- Notifications ---------------------------------------------------- async def test_list_notification_occurrences(client): resp = notifications_pb2.ListNotificationOccurrencesResponse() occ = resp.occurrences.add() occ.id = "n1" occ.created_at.FromJsonString("2026-01-01T00:00:00Z") occ.actor.id = "u1" occ.actor.login = "alice" occ.actor.display_name = "Alice" occ.unread = True sig = occ.signal.direct_mention_received sig.message.room.id = "r1" sig.message.room.name = "general" sig.message.event_id = "e1" resp.page.total_count = 1 resp.unread_count = 1 _mock_method(client, "notifications", "list_notification_occurrences", resp) async with client: page = await client.list_notification_occurrences() assert page.page.total_count == 1 assert page.unread_count == 1 assert page.occurrences[0].signal is not None assert page.occurrences[0].signal.kind == "direct_mention_received" assert page.occurrences[0].signal.message is not None assert page.occurrences[0].signal.message.event_id == "e1" assert page.occurrences[0].signal.message.room is not None assert page.occurrences[0].signal.message.room.name == "general" async def test_mark_and_delete_notification_occurrence(client): read_resp = notifications_pb2.MarkNotificationReadResponse() read_resp.occurrence.id = "n1" read_resp.occurrence.unread = False _mock_method(client, "notifications", "mark_notification_read", read_resp) del_resp = notifications_pb2.DeleteNotificationOccurrenceResponse(deleted=True) _mock_method(client, "notifications", "delete_notification_occurrence", del_resp) all_resp = notifications_pb2.DeleteAllNotificationOccurrencesResponse(deleted_count=3) _mock_method(client, "notifications", "delete_all_notification_occurrences", all_resp) async with client: occ = await client.mark_notification_read("n1") assert occ is not None and occ.unread is False assert await client.delete_notification_occurrence("n1") is True assert await client.delete_all_notification_occurrences() == 3 async def test_notification_preference(client): resp = notification_preferences_pb2.UpdateNotificationPreferenceResponse() resp.preference.level = "NOTIFICATION_LEVEL_MUTED" resp.preference.effective_level = "NOTIFICATION_LEVEL_MUTED" _mock_method( client, "notification_prefs", "update_room_notification_preference", resp, ) async with client: pref = await client.update_room_notification_preference("r1", NotificationLevel.MUTED) assert pref.level is NotificationLevel.MUTED assert pref.effective_level is NotificationLevel.MUTED # --- Push ------------------------------------------------------------- async def test_subscribe_push(client): _mock_method(client, "push", "subscribe", push_notifications_pb2.SubscribeResponse()) async with client: assert await client.subscribe_push("https://push", "k", "a") is True # --- Voice calls ------------------------------------------------------ async def test_active_calls(client): resp = voice_calls_pb2.ListActiveCallsResponse() call = resp.calls.add() call.call_id = "c1" call.room.id = "r1" call.room.name = "voice" p = call.participants.add() p.user.id = "u1" p.user.login = "alice" p.user.display_name = "Alice" _mock_method(client, "voice_calls", "list_active_calls", resp) async with client: calls = await client.list_active_calls() assert len(calls) == 1 assert calls[0].call_id == "c1" assert calls[0].room is not None assert calls[0].room.name == "voice" # --- Assets ----------------------------------------------------------- async def test_get_asset_with_thumbnail(client): resp = attachments_pb2.GetAssetResponse() resp.asset.id = "a1" resp.asset.filename = "pic.png" resp.asset.content_type = "image/png" resp.asset.size = 1024 resp.asset.asset_url.url = "https://cdn/pic.png" mock = _mock_method(client, "assets", "get_asset", resp) async with client: asset = await client.get_asset( "r1", "a1", thumbnail=ImageTransformOptions(128, 128, ImageFitMode.COVER), ) assert asset is not None assert asset.filename == "pic.png" assert asset.asset_url is not None assert asset.asset_url.url == "https://cdn/pic.png" # verify the request encoded thumbnail options req = mock.call_args.args[0] assert req.thumbnail.width == 128 assert req.thumbnail.height == 128 # --- Roles ------------------------------------------------------------ async def test_list_roles(client): resp = roles_pb2.ListRolesResponse() r1 = resp.roles.add() r1.name = "everyone" r1.display_name = "Everyone" r1.is_system = True r1.position = 0 r2 = resp.roles.add() r2.name = "admin" r2.display_name = "Admin" r2.position = 10 _mock_method(client, "roles", "list_roles", resp) async with client: roles = await client.list_roles() assert [r.name for r in roles] == ["everyone", "admin"] assert roles[0].is_system is True # --- Asset uploads --------------------------------------------------- async def test_asset_upload_end_to_end(client, tmp_path): import hashlib payload = b"hello world" * 100 file = tmp_path / "greet.txt" file.write_bytes(payload) sha = hashlib.sha256(payload).hexdigest() create_resp = asset_uploads_pb2.CreateUploadResponse() create_resp.upload.upload_id = "up1" create_resp.upload.room_id = "r1" create_resp.upload.status = "ASSET_UPLOAD_STATUS_OPEN" create_resp.upload.committed_offset = 0 create_resp.upload.size = len(payload) create_resp.upload.max_chunk_size = 4096 create_resp.upload.sha256 = sha create_mock = _mock_method(client, "asset_uploads", "create_upload", create_resp) chunk_resp = asset_uploads_pb2.UploadChunkResponse() chunk_resp.upload.upload_id = "up1" chunk_resp.upload.room_id = "r1" chunk_resp.upload.status = "ASSET_UPLOAD_STATUS_OPEN" chunk_resp.upload.committed_offset = len(payload) chunk_resp.upload.size = len(payload) chunk_resp.upload.max_chunk_size = 4096 chunk_resp.upload.sha256 = sha _mock_method(client, "asset_uploads", "upload_chunk", chunk_resp) complete_resp = asset_uploads_pb2.CompleteUploadResponse() complete_resp.upload.upload_id = "up1" complete_resp.upload.room_id = "r1" complete_resp.upload.status = "ASSET_UPLOAD_STATUS_COMPLETED" complete_resp.upload.committed_offset = len(payload) complete_resp.upload.size = len(payload) complete_resp.upload.sha256 = sha complete_resp.upload.asset_id = "a1" complete_resp.asset.id = "a1" complete_resp.asset.filename = "greet.txt" complete_resp.asset.content_type = "text/plain" complete_resp.asset.size = len(payload) _mock_method(client, "asset_uploads", "complete_upload", complete_resp) async with client: asset = await client.upload_attachment("r1", file, content_type="text/plain") assert asset.id == "a1" assert asset.filename == "greet.txt" create_req = create_mock.call_args.args[0] assert create_req.sha256 == sha assert create_req.size == len(payload) # --- External identities -------------------------------------------- # --- Admin (structural smoke tests) ---------------------------------- async def test_admin_list_room_groups(client): resp = room_layout_pb2.ListRoomGroupsResponse() g = resp.groups.add() g.id = "g1" g.name = "General" room_item = g.items.add() room_item.room.id = "r1" room_item.room.kind = "ROOM_KIND_CHANNEL" room_item.room.name = "chat" link_item = g.items.add() link_item.sidebar_link.id = "sl1" link_item.sidebar_link.label = "Docs" link_item.sidebar_link.url = "https://x" g.can_create_room = True _mock_method(client, "admin_room_layout", "list_room_groups", resp) async with client: groups = await client.admin_list_room_groups() assert len(groups) == 1 assert groups[0].name == "General" assert groups[0].rooms[0].id == "r1" assert groups[0].sidebar_links[0].label == "Docs" assert groups[0].can_create_room is True async def test_admin_create_sidebar_link(client): resp = room_layout_pb2.CreateSidebarLinkResponse() resp.sidebar_link.id = "sl1" resp.sidebar_link.label = "Docs" resp.sidebar_link.url = "https://x" _mock_method(client, "admin_room_layout", "create_sidebar_link", resp) async with client: result = await client.admin_create_sidebar_link("g1", "Docs", "https://x") assert result == {"id": "sl1", "label": "Docs", "url": "https://x"} async def test_admin_move_room_group(client): resp = room_layout_pb2.MoveRoomGroupResponse() g = resp.groups.add() g.id = "g1" g.name = "General" mock = _mock_method(client, "admin_room_layout", "move_room_group", resp) async with client: groups = await client.admin_move_room_group("g1", before_group_id="g2") assert [g.id for g in groups] == ["g1"] sent = mock.call_args.args[0] assert sent.group_id == "g1" assert sent.before_group_id == "g2" async def test_admin_move_room_group_to_end(client): resp = room_layout_pb2.MoveRoomGroupResponse() mock = _mock_method(client, "admin_room_layout", "move_room_group", resp) async with client: await client.admin_move_room_group("g1") sent = mock.call_args.args[0] assert sent.group_id == "g1" assert not sent.HasField("before_group_id") async def test_admin_move_sidebar_item(client): resp = room_layout_pb2.MoveSidebarItemResponse() resp.group.id = "g1" resp.group.name = "General" mock = _mock_method(client, "admin_room_layout", "move_sidebar_item", resp) async with client: group = await client.admin_move_sidebar_item( ("sidebar_link", "sl1"), "g1", before=("room", "r1"), ) assert group.id == "g1" sent = mock.call_args.args[0] assert sent.item.sidebar_link_id == "sl1" assert sent.before.room_id == "r1" assert sent.group_id == "g1" async def test_admin_get_system_info_livekit(client): resp = diagnostics_pb2.GetSystemInfoResponse() resp.projections_available = True resp.livekit.enabled = True resp.livekit.configured = True resp.livekit.url = "wss://livekit.example" resp.livekit.api_key = "AP1234" resp.livekit.separate_webhook_key = True resp.livekit.webhook_url = "https://chat.example/api/admin/livekit/webhook" resp.livekit.connection_state = diagnostics_pb2.ADMIN_LIVE_KIT_CONNECTION_STATE_OK resp.livekit.insecure_url = False _mock_method(client, "admin_diagnostics", "get_system_info", resp) async with client: data = await client.admin_get_system_info() lk = data["livekit"] assert lk["enabled"] is True assert lk["configured"] is True assert lk["url"] == "wss://livekit.example" assert lk["apiKey"] == "AP1234" assert lk["separateWebhookKey"] is True # enums render as their full name in Connect JSON (use_integers_for_enums=False) assert lk["connectionState"] == "ADMIN_LIVE_KIT_CONNECTION_STATE_OK" assert ( diagnostics_pb2.AdminLiveKitConnectionState.Value( "ADMIN_LIVE_KIT_CONNECTION_STATE_UNREACHABLE" ) == 3 ) assert "connectionError" not in lk # empty string omitted by MessageToDict async def test_admin_update_server_config(client): resp = admin_server_pb2.UpdateServerConfigResponse() resp.config.server_name = "MyServer" resp.config.motd = "hi" resp.public_profile.name = "MyServer" resp.public_profile.version = "0.4.2" _mock_method(client, "admin_server", "update_server_config", resp) async with client: config, profile = await client.admin_update_server_config(server_name="MyServer", motd="hi") assert config.server_name == "MyServer" assert profile.version == "0.4.2" # --- Neighbors (0.5.0-alpha.3) ----------------------------------------- async def test_list_neighbors_public(client): resp = discovery_server_pb2.ListNeighborsResponse() resp.origins.extend(["https://a.example", "https://b.example"]) _mock_method(client, "server_discovery", "list_neighbors", resp) async with client: origins = await client.list_neighbors() assert origins == ["https://a.example", "https://b.example"] async def test_admin_neighbor_crud(client): list_resp = admin_server_pb2.ListNeighborsResponse() n = list_resp.neighbors.add() n.id = "n1" n.origin = "https://a.example" n.revision = "rev1" _mock_method(client, "admin_server", "list_neighbors", list_resp) get_resp = admin_server_pb2.GetNeighborResponse() get_resp.neighbor.id = "n1" get_resp.neighbor.origin = "https://a.example" get_resp.neighbor.revision = "rev1" _mock_method(client, "admin_server", "get_neighbor", get_resp) create_resp = admin_server_pb2.CreateNeighborResponse() create_resp.neighbor.id = "n2" create_resp.neighbor.origin = "https://b.example" create_resp.neighbor.revision = "rev2" _mock_method(client, "admin_server", "create_neighbor", create_resp) update_resp = admin_server_pb2.UpdateNeighborResponse() update_resp.neighbor.id = "n2" update_resp.neighbor.origin = "https://b2.example" update_resp.neighbor.revision = "rev3" _mock_method(client, "admin_server", "update_neighbor", update_resp) del_resp = admin_server_pb2.DeleteNeighborResponse() _mock_method(client, "admin_server", "delete_neighbor", del_resp) async with client: listed = await client.admin_list_neighbors() assert [x.origin for x in listed] == ["https://a.example"] got = await client.admin_get_neighbor("n1") assert got is not None and got.revision == "rev1" created = await client.admin_create_neighbor("https://b.example") assert created.id == "n2" and created.revision == "rev2" updated = await client.admin_update_neighbor("n2", "https://b2.example", "rev2") assert updated.origin == "https://b2.example" and updated.revision == "rev3" await client.admin_delete_neighbor("n2", "rev3") async def test_update_settings_share_timezone(client): resp = account_pb2.UpdateSettingsResponse() resp.settings.timezone = "Europe/Berlin" resp.settings.share_timezone = True _mock_method(client, "account", "update_settings", resp) async with client: settings = await client.update_settings(timezone="Europe/Berlin", share_timezone=True) assert settings.timezone == "Europe/Berlin" assert settings.share_timezone is True def test_snake_case_is_acronym_aware(): """``types._snake_case`` must not chop acronym runs (same bug class as the protoc plugin's old ``StartDM`` -> ``start_d_m``).""" from chattolib.types import _snake_case assert _snake_case("messagePosted") == "message_posted" assert _snake_case("directMessageReceived") == "direct_message_received" # hypothetical acronym field: DM stays whole, boundary before the word after it assert _snake_case("startDMReceived") == "start_dm_received" assert _snake_case("getDMStats") == "get_dm_stats"