test_client.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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_spaces(mock_api, client):
  38. mock_api.post("/api/graphql").mock(
  39. return_value=_gql_response(
  40. {
  41. "spaces": [
  42. {
  43. "id": "s1",
  44. "name": "General",
  45. "description": None,
  46. "memberCount": 5,
  47. "roomCount": 3,
  48. "viewerIsMember": True,
  49. }
  50. ]
  51. }
  52. )
  53. )
  54. async with client:
  55. spaces = await client.spaces()
  56. assert len(spaces) == 1
  57. assert spaces[0].name == "General"
  58. assert spaces[0].viewer_is_member is True
  59. async def test_graphql_error(mock_api, client):
  60. mock_api.post("/api/graphql").mock(
  61. return_value=httpx.Response(
  62. 200,
  63. json={"errors": [{"message": "Not authorized"}]},
  64. )
  65. )
  66. async with client:
  67. with pytest.raises(ChattoGraphQLError, match="Not authorized"):
  68. await client.me()
  69. async def test_auth_error(mock_api, client):
  70. mock_api.post("/api/graphql").mock(return_value=httpx.Response(401))
  71. async with client:
  72. with pytest.raises(ChattoAuthError):
  73. await client.me()
  74. async def test_post_message(mock_api, client):
  75. mock_api.post("/api/graphql").mock(
  76. return_value=_gql_response(
  77. {"postMessage": {"id": "e1", "createdAt": "2025-01-01T00:00:00Z"}}
  78. )
  79. )
  80. async with client:
  81. result = await client.post_message("s1", "r1", "Hello!")
  82. assert result["id"] == "e1"
  83. async def test_room_events(mock_api, client):
  84. mock_api.post("/api/graphql").mock(
  85. return_value=_gql_response(
  86. {
  87. "roomEvents": {
  88. "events": [
  89. {
  90. "id": "e1",
  91. "createdAt": "2025-01-01T00:00:00Z",
  92. "actorId": "u1",
  93. "actor": {
  94. "id": "u1",
  95. "login": "alice",
  96. "displayName": "Alice",
  97. "avatarUrl": None,
  98. },
  99. "event": {
  100. "spaceId": "s1",
  101. "roomId": "r1",
  102. "body": "Hello",
  103. "attachments": [],
  104. "reactions": [],
  105. "inReplyTo": None,
  106. "inThread": None,
  107. "replyCount": 0,
  108. "lastReplyAt": None,
  109. "linkPreview": None,
  110. },
  111. }
  112. ],
  113. "hasOlder": False,
  114. "hasNewer": False,
  115. }
  116. }
  117. )
  118. )
  119. async with client:
  120. page = await client.room_events("s1", "r1", limit=10)
  121. assert len(page.events) == 1
  122. assert page.events[0].body == "Hello"
  123. assert page.events[0].actor.login == "alice"
  124. async def test_login():
  125. with respx.mock(base_url="https://chat.chatto.run") as api:
  126. api.post("/auth/login").mock(
  127. return_value=httpx.Response(
  128. 200,
  129. json={"success": True, "token": "cht_abc123", "user": {"id": "u1", "login": "alice"}},
  130. headers={"set-cookie": "chatto_session=xyz; Path=/; HttpOnly"},
  131. )
  132. )
  133. client = await ChattoClient.login("alice", "password123")
  134. assert client._token == "cht_abc123"
  135. assert client._session_cookie == "xyz"
  136. await client.close()
  137. async def test_login_invalid():
  138. with respx.mock(base_url="https://chat.chatto.run") as api:
  139. api.post("/auth/login").mock(return_value=httpx.Response(401, json={"error": "Invalid credentials"}))
  140. with pytest.raises(ChattoAuthError, match="Invalid credentials"):
  141. await ChattoClient.login("bad", "creds")
  142. async def test_create_user(mock_api, client):
  143. mock_api.post("/api/graphql").mock(
  144. return_value=_gql_response(
  145. {
  146. "createUser": {
  147. "id": "u2",
  148. "login": "newbot",
  149. "displayName": "New Bot",
  150. "createdAt": "2025-01-01T00:00:00Z",
  151. "avatarUrl": None,
  152. "presenceStatus": None,
  153. }
  154. }
  155. )
  156. )
  157. async with client:
  158. user = await client.create_user("newbot", "New Bot", "pass123")
  159. assert user.id == "u2"
  160. assert user.login == "newbot"
  161. assert user.display_name == "New Bot"
  162. async def test_edit_message(mock_api, client):
  163. mock_api.post("/api/graphql").mock(
  164. return_value=_gql_response({"editMessage": {"id": "e1"}})
  165. )
  166. async with client:
  167. result = await client.edit_message("s1", "r1", "e1", "edited body")
  168. assert result["id"] == "e1"
  169. async def test_delete_message(mock_api, client):
  170. mock_api.post("/api/graphql").mock(
  171. return_value=_gql_response({"deleteMessage": True})
  172. )
  173. async with client:
  174. result = await client.delete_message("s1", "r1", "e1")
  175. assert result is True
  176. async def test_add_reaction(mock_api, client):
  177. mock_api.post("/api/graphql").mock(
  178. return_value=_gql_response({"addReaction": True})
  179. )
  180. async with client:
  181. result = await client.add_reaction("s1", "r1", "e1", "👍")
  182. assert result is True
  183. async def test_remove_reaction(mock_api, client):
  184. mock_api.post("/api/graphql").mock(
  185. return_value=_gql_response({"removeReaction": True})
  186. )
  187. async with client:
  188. result = await client.remove_reaction("s1", "r1", "e1", "👍")
  189. assert result is True
  190. async def test_create_space(mock_api, client):
  191. mock_api.post("/api/graphql").mock(
  192. return_value=_gql_response(
  193. {"createSpace": {"id": "s1", "name": "Test Space", "description": "A test"}}
  194. )
  195. )
  196. async with client:
  197. space = await client.create_space("Test Space", description="A test")
  198. assert space.id == "s1"
  199. assert space.name == "Test Space"
  200. async def test_create_room(mock_api, client):
  201. mock_api.post("/api/graphql").mock(
  202. return_value=_gql_response(
  203. {"createRoom": {"id": "r1", "spaceId": "s1", "name": "general", "description": None}}
  204. )
  205. )
  206. async with client:
  207. room = await client.create_room("s1", "general")
  208. assert room.id == "r1"
  209. assert room.name == "general"
  210. async def test_update_my_profile(mock_api, client):
  211. mock_api.post("/api/graphql").mock(
  212. return_value=_gql_response(
  213. {
  214. "updateMyProfile": {
  215. "id": "u1",
  216. "login": "newname",
  217. "displayName": "New Name",
  218. }
  219. }
  220. )
  221. )
  222. async with client:
  223. user = await client.update_my_profile(login="newname", display_name="New Name")
  224. assert user.login == "newname"
  225. assert user.display_name == "New Name"
  226. async def test_update_presence(mock_api, client):
  227. mock_api.post("/api/graphql").mock(
  228. return_value=_gql_response({"updateMyPresence": True})
  229. )
  230. async with client:
  231. result = await client.update_presence(PresenceStatus.ONLINE)
  232. assert result is True
  233. async def test_upload_my_avatar(mock_api, client, tmp_path):
  234. mock_api.post("/api/graphql").mock(
  235. return_value=_gql_response(
  236. {"uploadMyAvatar": {"id": "u1", "avatarUrl": "https://example.com/avatar.jpg"}}
  237. )
  238. )
  239. avatar = tmp_path / "avatar.jpg"
  240. avatar.write_bytes(b"\xff\xd8\xff\xe0fake-jpeg")
  241. async with client:
  242. result = await client.upload_my_avatar(str(avatar))
  243. assert result["avatarUrl"] == "https://example.com/avatar.jpg"
  244. async def test_user(mock_api, client):
  245. mock_api.post("/api/graphql").mock(
  246. return_value=_gql_response(
  247. {
  248. "user": {
  249. "id": "u1",
  250. "login": "alice",
  251. "displayName": "Alice",
  252. "createdAt": "2025-01-01T00:00:00Z",
  253. "avatarUrl": None,
  254. "presenceStatus": "ONLINE",
  255. }
  256. }
  257. )
  258. )
  259. async with client:
  260. user = await client.user("u1")
  261. assert user.id == "u1"
  262. assert user.login == "alice"
  263. async def test_user_by_login(mock_api, client):
  264. mock_api.post("/api/graphql").mock(
  265. return_value=_gql_response(
  266. {
  267. "userByLogin": {
  268. "id": "u1",
  269. "login": "alice",
  270. "displayName": "Alice",
  271. "createdAt": None,
  272. "avatarUrl": None,
  273. "presenceStatus": None,
  274. }
  275. }
  276. )
  277. )
  278. async with client:
  279. user = await client.user_by_login("alice")
  280. assert user.login == "alice"
  281. async def test_start_dm(mock_api, client):
  282. mock_api.post("/api/graphql").mock(
  283. return_value=_gql_response({"startDM": {"id": "r1", "name": "DM"}})
  284. )
  285. async with client:
  286. result = await client.start_dm(["u1", "u2"])
  287. assert result["id"] == "r1"
  288. async def test_dismiss_notification(mock_api, client):
  289. mock_api.post("/api/graphql").mock(
  290. return_value=_gql_response({"dismissNotification": True})
  291. )
  292. async with client:
  293. result = await client.dismiss_notification("n1")
  294. assert result is True
  295. async def test_dismiss_all_notifications(mock_api, client):
  296. mock_api.post("/api/graphql").mock(
  297. return_value=_gql_response({"dismissAllNotifications": True})
  298. )
  299. async with client:
  300. result = await client.dismiss_all_notifications()
  301. assert result is True