184 lines
7.9 KiB
Python
184 lines
7.9 KiB
Python
import unittest
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from nio import AsyncClient, RoomMessagesResponse
|
|
from nio.responses import RoomEventRelationsResponse
|
|
from room import Room
|
|
|
|
|
|
class Message(SimpleNamespace):
|
|
pass
|
|
|
|
|
|
class Response(SimpleNamespace):
|
|
pass
|
|
|
|
|
|
def message(body, event_id, sender="bot"):
|
|
return Message(body=body, event_id=event_id, sender=sender, source={})
|
|
|
|
|
|
def reaction(target, sender, event_id, key="👍"):
|
|
return SimpleNamespace(sender=sender, event_id=event_id, source={
|
|
"type": "m.reaction",
|
|
"content": {"m.relates_to": {
|
|
"rel_type": "m.annotation", "key": key, "event_id": target,
|
|
}},
|
|
})
|
|
|
|
|
|
class CountVotesTests(unittest.IsolatedAsyncioTestCase):
|
|
def setUp(self):
|
|
self.room = Room.__new__(Room)
|
|
self.room.config = SimpleNamespace(room_id="test", batch_size=100)
|
|
self.room.user_id = "bot"
|
|
self.room.client = SimpleNamespace(room_messages=AsyncMock())
|
|
self.enterContext(patch("room.RoomMessageText", Message))
|
|
self.enterContext(patch("room.RoomMessagesResponse", Response))
|
|
|
|
async def count(self, *pages, live_reactions=None):
|
|
self.room.client.room_messages.side_effect = pages
|
|
events = [event for page in pages for event in getattr(page, "chunk", [])]
|
|
removed = set()
|
|
for event in events:
|
|
source = getattr(event, "source", {})
|
|
if source.get("type") == "m.room.redaction":
|
|
removed.add(source.get("redacts") or source.get("content", {}).get("redacts"))
|
|
|
|
async def relations(room_id, event_id, **kwargs):
|
|
for event in events if live_reactions is None else live_reactions:
|
|
relation = event.source.get("content", {}).get("m.relates_to", {})
|
|
if relation.get("event_id") == event_id and event.event_id not in removed:
|
|
yield event
|
|
|
|
self.room.client.room_get_event_relations = relations
|
|
return await self.room.count_votes()
|
|
|
|
async def test_votes_absent_from_timeline_are_read_from_live_relations(self):
|
|
result = await self.count(Response(chunk=[
|
|
message("Suggestion 1: Author: Title", "$one"),
|
|
message("Time to vote!", "$start"),
|
|
], end=None), live_reactions=[
|
|
reaction("$one", "alice", "$vote", key="👍🏽"),
|
|
reaction("$one", "bob", "$vote2", key="👍\ufe0f"),
|
|
])
|
|
self.assertIn("Suggestion 1: 2 votes", result)
|
|
|
|
async def test_relation_failure_is_not_reported_as_zero_votes(self):
|
|
async def broken_relations(*args, **kwargs):
|
|
raise RuntimeError("Server unavailable")
|
|
yield
|
|
|
|
self.room.client.room_messages.return_value = Response(chunk=[
|
|
message("Suggestion 1: Author: Title", "$one"),
|
|
message("Time to vote!", "$start"),
|
|
], end=None)
|
|
self.room.client.room_get_event_relations = broken_relations
|
|
result = await self.room.count_votes()
|
|
self.assertIn("Unable to read emoji votes", result)
|
|
self.assertNotIn("0 votes", result)
|
|
|
|
async def test_latest_round_ranking_and_later_votes_refresh(self):
|
|
latest = [
|
|
reaction("$two", "alice", "$v2"),
|
|
reaction("$one", "alice", "$v1"),
|
|
reaction("$one", "alice", "$duplicate"),
|
|
reaction("$one", "bot", "$starter"),
|
|
reaction("$one", "charlie", "$wrong", key="👎"),
|
|
reaction("$old", "dana", "$oldvote"),
|
|
message("Suggestion 3: Author: Zero votes", "$three"),
|
|
message("Suggestion 2: Author: Second title", "$two"),
|
|
message("Suggestion 1: Author: First title", "$one"),
|
|
message("Time to vote! 🚢", "$announcement"),
|
|
message("Suggestion 1: Author: Old title", "$old"),
|
|
]
|
|
result = await self.count(Response(chunk=latest, end="older"))
|
|
self.assertIn("Suggestion 1: 1 vote", result)
|
|
self.assertIn("Suggestion 2: 1 vote", result)
|
|
self.assertIn("Suggestion 3: 0 votes", result)
|
|
self.assertNotIn("Old title", result)
|
|
self.assertLess(result.index("Suggestion 1:"), result.index("Suggestion 2:"))
|
|
updated = await self.count(Response(chunk=[
|
|
reaction("$two", "bob", "$later"), *latest,
|
|
], end="older"))
|
|
self.assertIn("Suggestion 2: 2 votes", updated)
|
|
self.assertLess(updated.index("Suggestion 2:"), updated.index("Suggestion 1:"))
|
|
|
|
async def test_removed_reactions_and_zero_vote_results(self):
|
|
redactions = [SimpleNamespace(source={
|
|
"type": "m.room.redaction", **target,
|
|
}) for target in ({"redacts": "$removed"}, {"content": {"redacts": "$removedv11"}})]
|
|
result = await self.count(Response(chunk=[
|
|
*redactions,
|
|
reaction("$one", "alice", "$removed"),
|
|
reaction("$one", "bob", "$removedv11"),
|
|
message("Suggestion 1: Author: Title", "$one"),
|
|
message("Time to vote!", "$start"),
|
|
], end=None))
|
|
self.assertIn("Suggestion 1: 0 votes", result)
|
|
|
|
async def test_round_older_than_fifteen_pages(self):
|
|
pages = [Response(chunk=[reaction("$one", "alice", "$vote")], end="1")]
|
|
pages.extend(Response(chunk=[message("Chat", f"$chat{i}", sender="alice")], end=str(i))
|
|
for i in range(2, 18))
|
|
pages.append(Response(chunk=[
|
|
message("Suggestion 1: Author: Title", "$one"),
|
|
message("Time to vote!", "$start"),
|
|
], end="old"))
|
|
result = await self.count(*pages)
|
|
self.assertIn("Suggestion 1: 1 vote", result)
|
|
self.assertEqual(self.room.client.room_messages.await_count, 18)
|
|
|
|
async def test_history_failure_does_not_report_partial_counts(self):
|
|
result = await self.count(Response(chunk=[
|
|
message("Suggestion 1: Author: Title", "$one"),
|
|
], end="older"), object())
|
|
self.assertIn("Unable to read voting history", result)
|
|
|
|
async def test_no_round(self):
|
|
self.assertIn("No suggestion posts found", await self.count(Response(chunk=[], end=None)))
|
|
|
|
|
|
class MatrixSDKVotesTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_real_sdk_parses_and_paginates_live_reactions(self):
|
|
room = Room.__new__(Room)
|
|
room.config = SimpleNamespace(room_id="!room:matrix.test", batch_size=2)
|
|
room.user_id = "@bot:matrix.test"
|
|
room.client = AsyncClient("https://matrix.test", room.user_id)
|
|
room.client.access_token = "test-token"
|
|
self.addAsyncCleanup(room.client.close)
|
|
|
|
def event(event_id, sender, event_type, content):
|
|
return {"event_id": event_id, "sender": sender, "type": event_type,
|
|
"origin_server_ts": 100000, "content": content,
|
|
"room_id": room.config.room_id}
|
|
|
|
history = RoomMessagesResponse.from_dict({
|
|
"start": "now", "chunk": [
|
|
event("$one", room.user_id, "m.room.message", {
|
|
"msgtype": "m.text", "body": "Suggestion 1: Author: Title"}),
|
|
event("$start", room.user_id, "m.room.message", {
|
|
"msgtype": "m.text", "body": "Time to vote!"}),
|
|
],
|
|
}, room.config.room_id)
|
|
|
|
def votes(sender, event_id, next_batch=None):
|
|
return RoomEventRelationsResponse.from_dict({
|
|
"chunk": [event(event_id, sender, "m.reaction", {"m.relates_to": {
|
|
"rel_type": "m.annotation", "key": "👍", "event_id": "$one",
|
|
}})], **({"next_batch": next_batch} if next_batch else {}),
|
|
}, room.config.room_id, "$one")
|
|
|
|
room.client._send = AsyncMock(side_effect=[
|
|
history, votes("@alice:matrix.test", "$v1", "page2"),
|
|
votes("@bob:matrix.test", "$v2"),
|
|
])
|
|
result = await room.count_votes()
|
|
self.assertIn("Suggestion 1: 2 votes", result)
|
|
self.assertIn("from=page2", room.client._send.await_args_list[-1].args[2])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|