diff --git a/tests/handlers/test_receipts.py b/tests/handlers/test_receipts.py
new file mode 100644
index 0000000000..93a9a084b2
--- /dev/null
+++ b/tests/handlers/test_receipts.py
@@ -0,0 +1,294 @@
+# Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from typing import List
+
+from synapse.api.constants import ReadReceiptEventFields
+from synapse.types import JsonDict
+
+from tests import unittest
+
+
+class ReceiptsTestCase(unittest.HomeserverTestCase):
+ def prepare(self, reactor, clock, hs):
+ self.event_source = hs.get_event_sources().sources["receipt"]
+
+ # In the first param of _test_filters_hidden we use "hidden" instead of
+ # ReadReceiptEventFields.MSC2285_HIDDEN. We do this because we're mocking
+ # the data from the database which doesn't use the prefix
+
+ def test_filters_out_hidden_receipt(self):
+ self._test_filters_hidden(
+ [
+ {
+ "content": {
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@rikj:jki.re": {
+ "ts": 1436451550453,
+ "hidden": True,
+ }
+ }
+ }
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ [],
+ )
+
+ def test_does_not_filter_out_our_hidden_receipt(self):
+ self._test_filters_hidden(
+ [
+ {
+ "content": {
+ "$1435641916hfgh4394fHBLK:matrix.org": {
+ "m.read": {
+ "@me:server.org": {
+ "ts": 1436451550453,
+ "hidden": True,
+ },
+ }
+ }
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ [
+ {
+ "content": {
+ "$1435641916hfgh4394fHBLK:matrix.org": {
+ "m.read": {
+ "@me:server.org": {
+ "ts": 1436451550453,
+ ReadReceiptEventFields.MSC2285_HIDDEN: True,
+ },
+ }
+ }
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ )
+
+ def test_filters_out_hidden_receipt_and_ignores_rest(self):
+ self._test_filters_hidden(
+ [
+ {
+ "content": {
+ "$1dgdgrd5641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@rikj:jki.re": {
+ "ts": 1436451550453,
+ "hidden": True,
+ },
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ },
+ }
+ }
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ [
+ {
+ "content": {
+ "$1dgdgrd5641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ }
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ )
+
+ def test_filters_out_event_with_only_hidden_receipts_and_ignores_the_rest(self):
+ self._test_filters_hidden(
+ [
+ {
+ "content": {
+ "$14356419edgd14394fHBLK:matrix.org": {
+ "m.read": {
+ "@rikj:jki.re": {
+ "ts": 1436451550453,
+ "hidden": True,
+ },
+ }
+ },
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ },
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ [
+ {
+ "content": {
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ }
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ )
+
+ def test_handles_missing_content_of_m_read(self):
+ self._test_filters_hidden(
+ [
+ {
+ "content": {
+ "$14356419ggffg114394fHBLK:matrix.org": {"m.read": {}},
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ },
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ [
+ {
+ "content": {
+ "$14356419ggffg114394fHBLK:matrix.org": {"m.read": {}},
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ },
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ )
+
+ def test_handles_empty_event(self):
+ self._test_filters_hidden(
+ [
+ {
+ "content": {
+ "$143564gdfg6114394fHBLK:matrix.org": {},
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ },
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ [
+ {
+ "content": {
+ "$143564gdfg6114394fHBLK:matrix.org": {},
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ },
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ )
+
+ def test_filters_out_receipt_event_with_only_hidden_receipt_and_ignores_rest(self):
+ self._test_filters_hidden(
+ [
+ {
+ "content": {
+ "$14356419edgd14394fHBLK:matrix.org": {
+ "m.read": {
+ "@rikj:jki.re": {
+ "ts": 1436451550453,
+ "hidden": True,
+ },
+ }
+ },
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ },
+ {
+ "content": {
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ },
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ },
+ ],
+ [
+ {
+ "content": {
+ "$1435641916114394fHBLK:matrix.org": {
+ "m.read": {
+ "@user:jki.re": {
+ "ts": 1436451550453,
+ }
+ }
+ }
+ },
+ "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+ "type": "m.receipt",
+ }
+ ],
+ )
+
+ def _test_filters_hidden(
+ self, events: List[JsonDict], expected_output: List[JsonDict]
+ ):
+ """Tests that the _filter_out_hidden returns the expected output"""
+ filtered_events = self.event_source.filter_out_hidden(events, "@me:server.org")
+ self.assertEquals(filtered_events, expected_output)
diff --git a/tests/rest/client/v2_alpha/test_sync.py b/tests/rest/client/v2_alpha/test_sync.py
index cdca3a3e23..f6ae9ae181 100644
--- a/tests/rest/client/v2_alpha/test_sync.py
+++ b/tests/rest/client/v2_alpha/test_sync.py
@@ -15,9 +15,14 @@
import json
import synapse.rest.admin
-from synapse.api.constants import EventContentFields, EventTypes, RelationTypes
+from synapse.api.constants import (
+ EventContentFields,
+ EventTypes,
+ ReadReceiptEventFields,
+ RelationTypes,
+)
from synapse.rest.client.v1 import login, room
-from synapse.rest.client.v2_alpha import knock, read_marker, sync
+from synapse.rest.client.v2_alpha import knock, read_marker, receipts, sync
from tests import unittest
from tests.federation.transport.test_knocking import (
@@ -368,6 +373,76 @@ class SyncKnockTestCase(
)
+class ReadReceiptsTestCase(unittest.HomeserverTestCase):
+ servlets = [
+ synapse.rest.admin.register_servlets,
+ login.register_servlets,
+ receipts.register_servlets,
+ room.register_servlets,
+ sync.register_servlets,
+ ]
+
+ def prepare(self, reactor, clock, hs):
+ self.url = "/sync?since=%s"
+ self.next_batch = "s0"
+
+ # Register the first user
+ self.user_id = self.register_user("kermit", "monkey")
+ self.tok = self.login("kermit", "monkey")
+
+ # Create the room
+ self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
+
+ # Register the second user
+ self.user2 = self.register_user("kermit2", "monkey")
+ self.tok2 = self.login("kermit2", "monkey")
+
+ # Join the second user
+ self.helper.join(room=self.room_id, user=self.user2, tok=self.tok2)
+
+ @override_config({"experimental_features": {"msc2285_enabled": True}})
+ def test_hidden_read_receipts(self):
+ # Send a message as the first user
+ res = self.helper.send(self.room_id, body="hello", tok=self.tok)
+
+ # Send a read receipt to tell the server the first user's message was read
+ body = json.dumps({ReadReceiptEventFields.MSC2285_HIDDEN: True}).encode("utf8")
+ channel = self.make_request(
+ "POST",
+ "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]),
+ body,
+ access_token=self.tok2,
+ )
+ self.assertEqual(channel.code, 200)
+
+ # Test that the first user can't see the other user's hidden read receipt
+ self.assertEqual(self._get_read_receipt(), None)
+
+ def _get_read_receipt(self):
+ """Syncs and returns the read receipt."""
+
+ # Checks if event is a read receipt
+ def is_read_receipt(event):
+ return event["type"] == "m.receipt"
+
+ # Sync
+ channel = self.make_request(
+ "GET",
+ self.url % self.next_batch,
+ access_token=self.tok,
+ )
+ self.assertEqual(channel.code, 200)
+
+ # Store the next batch for the next request.
+ self.next_batch = channel.json_body["next_batch"]
+
+ # Return the read receipt
+ ephemeral_events = channel.json_body["rooms"]["join"][self.room_id][
+ "ephemeral"
+ ]["events"]
+ return next(filter(is_read_receipt, ephemeral_events), None)
+
+
class UnreadMessagesTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
@@ -375,6 +450,7 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase):
read_marker.register_servlets,
room.register_servlets,
sync.register_servlets,
+ receipts.register_servlets,
]
def prepare(self, reactor, clock, hs):
@@ -448,6 +524,23 @@ class UnreadMessagesTestCase(unittest.HomeserverTestCase):
# Check that the unread counter is back to 0.
self._check_unread_count(0)
+ # Check that hidden read receipts don't break unread counts
+ res = self.helper.send(self.room_id, "hello", tok=self.tok2)
+ self._check_unread_count(1)
+
+ # Send a read receipt to tell the server we've read the latest event.
+ body = json.dumps({ReadReceiptEventFields.MSC2285_HIDDEN: True}).encode("utf8")
+ channel = self.make_request(
+ "POST",
+ "/rooms/%s/receipt/m.read/%s" % (self.room_id, res["event_id"]),
+ body,
+ access_token=self.tok,
+ )
+ self.assertEqual(channel.code, 200, channel.json_body)
+
+ # Check that the unread counter is back to 0.
+ self._check_unread_count(0)
+
# Check that room name changes increase the unread counter.
self.helper.send_state(
self.room_id,
|