test_client.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. """Tests for ChattoClient using respx to mock HTTP."""
  2. import pytest
  3. import respx
  4. import httpx
  5. from chattolib.client import ChattoClient
  6. from chattolib.exceptions import ChattoGraphQLError, ChattoAuthError
  7. from chattolib.types import PresenceStatus
  8. @pytest.fixture
  9. def mock_api():
  10. with respx.mock(base_url="https://chat.chatto.run") as api:
  11. yield api
  12. @pytest.fixture
  13. def client():
  14. return ChattoClient(token="test-token")
  15. def _gql_response(data: dict) -> httpx.Response:
  16. return httpx.Response(200, json={"data": data})
  17. async def test_me(mock_api, client):
  18. mock_api.post("/api/graphql").mock(
  19. return_value=_gql_response(
  20. {
  21. "me": {
  22. "id": "u1",
  23. "login": "alice",
  24. "displayName": "Alice",
  25. "createdAt": "2025-01-01T00:00:00Z",
  26. "avatarUrl": None,
  27. "presenceStatus": "ONLINE",
  28. }
  29. }
  30. )
  31. )
  32. async with client:
  33. user = await client.me()
  34. assert user.id == "u1"
  35. assert user.login == "alice"
  36. assert user.display_name == "Alice"
  37. async def test_rooms(mock_api, client):
  38. mock_api.post("/api/graphql").mock(
  39. return_value=_gql_response(
  40. {
  41. "instance": {
  42. "rooms": [
  43. {
  44. "id": "r1",
  45. "type": "CHANNEL",
  46. "name": "general",
  47. "description": None,
  48. "archived": False,
  49. "autoJoin": True,
  50. "hasUnread": False,
  51. "hasMention": False,
  52. }
  53. ]
  54. }
  55. }
  56. )
  57. )
  58. async with client:
  59. rooms = await client.rooms()
  60. assert len(rooms) == 1
  61. assert rooms[0].name == "general"
  62. assert rooms[0].type.value == "CHANNEL"
  63. async def test_graphql_error(mock_api, client):
  64. mock_api.post("/api/graphql").mock(
  65. return_value=httpx.Response(
  66. 200,
  67. json={"errors": [{"message": "Not authorized"}]},
  68. )
  69. )
  70. async with client:
  71. with pytest.raises(ChattoGraphQLError, match="Not authorized"):
  72. await client.me()
  73. async def test_auth_error(mock_api, client):
  74. mock_api.post("/api/graphql").mock(return_value=httpx.Response(401))
  75. async with client:
  76. with pytest.raises(ChattoAuthError):
  77. await client.me()
  78. async def test_post_message(mock_api, client):
  79. mock_api.post("/api/graphql").mock(
  80. return_value=_gql_response(
  81. {"postMessage": {"id": "e1", "createdAt": "2025-01-01T00:00:00Z"}}
  82. )
  83. )
  84. async with client:
  85. result = await client.post_message("r1", "Hello!")
  86. assert result["id"] == "e1"
  87. async def test_room_events(mock_api, client):
  88. mock_api.post("/api/graphql").mock(
  89. return_value=_gql_response(
  90. {
  91. "roomEvents": {
  92. "events": [
  93. {
  94. "id": "e1",
  95. "createdAt": "2025-01-01T00:00:00Z",
  96. "actorId": "u1",
  97. "actor": {
  98. "id": "u1",
  99. "login": "alice",
  100. "displayName": "Alice",
  101. "avatarUrl": None,
  102. },
  103. "event": {
  104. "roomId": "r1",
  105. "body": "Hello",
  106. "attachments": [],
  107. "reactions": [],
  108. "inReplyTo": None,
  109. "inThread": None,
  110. "replyCount": 0,
  111. "lastReplyAt": None,
  112. "linkPreview": None,
  113. },
  114. }
  115. ],
  116. "hasOlder": False,
  117. "hasNewer": False,
  118. "startCursor": None,
  119. "endCursor": None,
  120. }
  121. }
  122. )
  123. )
  124. async with client:
  125. page = await client.room_events("r1", limit=10)
  126. assert len(page.events) == 1
  127. assert page.events[0].body == "Hello"
  128. assert page.events[0].actor.login == "alice"
  129. async def test_login():
  130. with respx.mock(base_url="https://chat.chatto.run") as api:
  131. api.post("/auth/login").mock(
  132. return_value=httpx.Response(
  133. 200,
  134. json={"success": True, "token": "cht_abc123", "user": {"id": "u1", "login": "alice"}},
  135. headers={"set-cookie": "chatto_session=xyz; Path=/; HttpOnly"},
  136. )
  137. )
  138. client = await ChattoClient.login("alice", "password123")
  139. assert client._token == "cht_abc123"
  140. assert client._session_cookie == "xyz"
  141. await client.close()
  142. async def test_login_invalid():
  143. with respx.mock(base_url="https://chat.chatto.run") as api:
  144. api.post("/auth/login").mock(return_value=httpx.Response(401, json={"error": "Invalid credentials"}))
  145. with pytest.raises(ChattoAuthError, match="Invalid credentials"):
  146. await ChattoClient.login("bad", "creds")
  147. async def test_edit_message(mock_api, client):
  148. mock_api.post("/api/graphql").mock(
  149. return_value=_gql_response({"editMessage": {"id": "e1"}})
  150. )
  151. async with client:
  152. result = await client.edit_message("r1", "e1", "edited body")
  153. assert result["id"] == "e1"
  154. async def test_delete_message(mock_api, client):
  155. mock_api.post("/api/graphql").mock(
  156. return_value=_gql_response({"deleteMessage": True})
  157. )
  158. async with client:
  159. result = await client.delete_message("r1", "e1")
  160. assert result is True
  161. async def test_add_reaction(mock_api, client):
  162. mock_api.post("/api/graphql").mock(
  163. return_value=_gql_response({"addReaction": True})
  164. )
  165. async with client:
  166. result = await client.add_reaction("r1", "e1", "thumbsup")
  167. assert result is True
  168. async def test_remove_reaction(mock_api, client):
  169. mock_api.post("/api/graphql").mock(
  170. return_value=_gql_response({"removeReaction": True})
  171. )
  172. async with client:
  173. result = await client.remove_reaction("r1", "e1", "thumbsup")
  174. assert result is True
  175. async def test_create_room(mock_api, client):
  176. mock_api.post("/api/graphql").mock(
  177. return_value=_gql_response(
  178. {"createRoom": {"id": "r1", "type": "CHANNEL", "name": "general", "description": None}}
  179. )
  180. )
  181. async with client:
  182. room = await client.create_room("general")
  183. assert room.id == "r1"
  184. assert room.name == "general"
  185. async def test_update_my_profile(mock_api, client):
  186. mock_api.post("/api/graphql").mock(
  187. return_value=_gql_response(
  188. {
  189. "updateMyProfile": {
  190. "id": "u1",
  191. "login": "newname",
  192. "displayName": "New Name",
  193. }
  194. }
  195. )
  196. )
  197. async with client:
  198. user = await client.update_my_profile(login="newname", display_name="New Name")
  199. assert user.login == "newname"
  200. assert user.display_name == "New Name"
  201. async def test_update_presence(mock_api, client):
  202. mock_api.post("/api/graphql").mock(
  203. return_value=_gql_response({"updateMyPresence": True})
  204. )
  205. async with client:
  206. result = await client.update_presence(PresenceStatus.ONLINE)
  207. assert result is True
  208. async def test_upload_my_avatar(mock_api, client, tmp_path):
  209. mock_api.post("/api/graphql").mock(
  210. return_value=_gql_response(
  211. {"uploadMyAvatar": {"id": "u1", "avatarUrl": "https://example.com/avatar.jpg"}}
  212. )
  213. )
  214. avatar = tmp_path / "avatar.jpg"
  215. avatar.write_bytes(b"\xff\xd8\xff\xe0fake-jpeg")
  216. async with client:
  217. result = await client.upload_my_avatar(str(avatar))
  218. assert result["avatarUrl"] == "https://example.com/avatar.jpg"
  219. async def test_user(mock_api, client):
  220. mock_api.post("/api/graphql").mock(
  221. return_value=_gql_response(
  222. {
  223. "user": {
  224. "id": "u1",
  225. "login": "alice",
  226. "displayName": "Alice",
  227. "createdAt": "2025-01-01T00:00:00Z",
  228. "avatarUrl": None,
  229. "presenceStatus": "ONLINE",
  230. }
  231. }
  232. )
  233. )
  234. async with client:
  235. user = await client.user("u1")
  236. assert user.id == "u1"
  237. assert user.login == "alice"
  238. async def test_user_by_login(mock_api, client):
  239. mock_api.post("/api/graphql").mock(
  240. return_value=_gql_response(
  241. {
  242. "userByLogin": {
  243. "id": "u1",
  244. "login": "alice",
  245. "displayName": "Alice",
  246. "createdAt": None,
  247. "avatarUrl": None,
  248. "presenceStatus": None,
  249. }
  250. }
  251. )
  252. )
  253. async with client:
  254. user = await client.user_by_login("alice")
  255. assert user.login == "alice"
  256. async def test_start_dm(mock_api, client):
  257. mock_api.post("/api/graphql").mock(
  258. return_value=_gql_response({"startDM": {"id": "r1", "name": "DM"}})
  259. )
  260. async with client:
  261. result = await client.start_dm(["u1", "u2"])
  262. assert result["id"] == "r1"
  263. async def test_dismiss_notification(mock_api, client):
  264. mock_api.post("/api/graphql").mock(
  265. return_value=_gql_response({"dismissNotification": True})
  266. )
  267. async with client:
  268. result = await client.dismiss_notification("n1")
  269. assert result is True
  270. async def test_dismiss_all_notifications(mock_api, client):
  271. mock_api.post("/api/graphql").mock(
  272. return_value=_gql_response({"dismissAllNotifications": True})
  273. )
  274. async with client:
  275. result = await client.dismiss_all_notifications()
  276. assert result is True