Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c2d1039
MSC4140: impose limit of scheduled delayed events
AndrewFerr Mar 9, 2026
c70c428
MSC4140: update error codes to match latest MSC
AndrewFerr Mar 9, 2026
3b51b48
Add changelogs
AndrewFerr Mar 9, 2026
ac2bed9
Assert against variable message, not literal
AndrewFerr Mar 12, 2026
e02f554
Put MSC4140 config near other one & allow stable
AndrewFerr Mar 12, 2026
2e01d7f
In test, assign default error message to variable
AndrewFerr Mar 13, 2026
79a0218
Explain reason for location of experimental config
AndrewFerr Mar 13, 2026
a614ebb
Leave new config as experimental only
AndrewFerr Mar 13, 2026
448bf17
Set Retry-After on delayed event limit error
AndrewFerr Mar 16, 2026
43e14e9
Enforce max delayed event config to be positive
AndrewFerr Mar 16, 2026
f69ddc1
Update copyright dates
AndrewFerr Mar 16, 2026
437a034
Revert "Update copyright dates"
AndrewFerr Jun 2, 2026
5dfc8b8
Merge with 'develop'
AndrewFerr Jun 5, 2026
1b19c13
Use capabilities to convey delayed event limits
AndrewFerr Jun 7, 2026
267a7e5
Allow delayed event limit to be set to 0
AndrewFerr Jun 7, 2026
df09f8d
Rename variable to better indicate it as a time
AndrewFerr Jun 7, 2026
3804db0
Elaborate comment on make_request time step
AndrewFerr Jun 7, 2026
184b32d
Remove unnecessary wait in test
AndrewFerr Jun 7, 2026
c19df5d
Clarify config limits in errors & test comments
AndrewFerr Jun 8, 2026
377cac4
Lint
AndrewFerr Jun 8, 2026
0b0aab1
Document special-case error for limit <= 0
AndrewFerr Jun 12, 2026
d62f440
Cover case of limit << num existing delayed events
AndrewFerr Jun 12, 2026
4a9464c
Clarify test on ratelimit override
AndrewFerr Jun 12, 2026
79a6f31
Simplify delayed event limit test
AndrewFerr Jun 12, 2026
34c5ce9
Use consistent keyvalues when applying limit
AndrewFerr Jun 12, 2026
2eea7f5
Use "err" instead of "e"
AndrewFerr Jun 12, 2026
87ce5f3
Fixups
AndrewFerr Jun 12, 2026
e1a0ba4
Test limit << num existing delayed events
AndrewFerr Jun 12, 2026
1a2895b
Add alias to sub-SELECT, needed for PostgreSQL <16
AndrewFerr Jun 12, 2026
7bc51d7
Apply suggestions from code review
AndrewFerr Jun 17, 2026
56439ef
Improve test comments
AndrewFerr Jun 17, 2026
d2e84b7
Fix typo in test docstring
AndrewFerr Jun 17, 2026
af16899
Move all delayed event limit processing in handler
AndrewFerr Jun 17, 2026
24a543a
Update imports
AndrewFerr Jun 18, 2026
b6e61ef
Run entire user limit test with ratelimit disabled
AndrewFerr Jun 18, 2026
2278272
Add test comment to explain purpose of Retry-After
AndrewFerr Jun 18, 2026
d805c4d
Test for Retry-After having only a single value
AndrewFerr Jun 18, 2026
5390153
Use TestCase assert for a non-narrowing assertion
AndrewFerr Jun 18, 2026
a3d0def
Rename capability field to `max_delay_ms`
AndrewFerr Jun 26, 2026
45b6fc9
In test, lift ratelimit before making any request
AndrewFerr Jun 26, 2026
da30132
Test that is_processed events count against limit
AndrewFerr Jun 26, 2026
bcd5b61
In test, rename time values to proper unit
AndrewFerr Jun 26, 2026
80b47aa
Enforce & document positive limit in storage fn
AndrewFerr Jun 26, 2026
06a6a9e
Restore comment on Retry-After lookup query
AndrewFerr Jun 26, 2026
ba17cda
In tests, use closures instead of passing `*args`
AndrewFerr Jun 26, 2026
17d1bfe
Tweak user limit tweak test
AndrewFerr Jun 26, 2026
2f0a302
Use M_FORBIDDEN/403 for disallowed/exceeded delay
AndrewFerr Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/19539.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[MSC4140: Cancellable delayed events](https://github.com/matrix-org/matrix-spec-proposals/pull/4140): Update error responses to match their format in the current draft of the MSC.
1 change: 1 addition & 0 deletions changelog.d/19539.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[MSC4140: Cancellable delayed events](https://github.com/matrix-org/matrix-spec-proposals/pull/4140): Limit how many delayed events a user may have scheduled at once.
19 changes: 18 additions & 1 deletion synapse/config/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,10 +914,27 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
max_event_delay_duration
)
if self.max_event_delay_ms <= 0:
raise ConfigError("max_event_delay_duration must be a positive value")
raise ConfigError(
"'max_event_delay_duration' must be a positive value if set",
("max_event_delay_duration",),
)
Comment on lines +917 to +920

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain more why and what do do.

Enforce max delayed event config to be positive

If delayed events are to be disabled, it should be done by leaving the
max_event_delay_duration config unspecified, not by setting the delayed
event limit to 0.

-- 43e14e9

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

267a7e5 rewords the error message to be a bit more descriptive, and to follow the same format used by similar errors in this module.

@MadLittleMods MadLittleMods Jun 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the original intentions, I was more looking for something like this:

Suggested change
raise ConfigError(
"Expected a positive value", ("max_event_delay_duration",)
)
raise ConfigError(
"Expected a non-zero, positive value for the delay duration. To disable delayed events, leave `max_event_delay_duration` unspecified.", ("max_event_delay_duration",)
)

It seems like ideally, we would have had this kind of structure for delayed event config:

delayed_events:
  enabled: true
  max_event_delay_duration: 24h
  max_delayed_events_per_user: 100

If we're going with 0 as a valid value to disable delayed events, I guess the updated error message works ⏩

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That new config does look better, though I'd prefer using a dedicated PR to change it, given that max_event_delay_duration has been around for a while now & moving it would be a breaking change.

I'd also prefer to keep config suggestions in the documentation instead of error messages, to reduce churn on code changes while the MSC is still unstable.

else:
self.max_event_delay_ms = None

# The maximum number of delayed events a user may have scheduled at a time.
# (Defined here despite being experimental to be near the other MSC4140 config)
self.max_delayed_events_per_user: int = config.get(
"experimental_features", {}
).get("msc4140_max_delayed_events_per_user", 100)
if (
not isinstance(self.max_delayed_events_per_user, int)
or self.max_delayed_events_per_user < 0
):
raise ConfigError(
"'msc4140_max_delayed_events_per_user' must be a non-negative integer",
("experimental", "msc4140_max_delayed_events_per_user"),
)

def has_tls_listener(self) -> bool:
return any(listener.is_tls() for listener in self.listeners)

Expand Down
26 changes: 24 additions & 2 deletions synapse/handlers/delayed_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
#

import logging
from http import HTTPStatus
from typing import TYPE_CHECKING, Optional

from twisted.internet.interfaces import IDelayedCall

from synapse.api.constants import EventTypes, StickyEvent, StickyEventField
from synapse.api.errors import ShadowBanError, SynapseError
from synapse.api.errors import Codes, ShadowBanError, SynapseError
from synapse.api.ratelimiting import Ratelimiter
from synapse.config.workers import MAIN_PROCESS_INSTANCE_NAME
from synapse.http.site import SynapseRequest
Expand Down Expand Up @@ -353,13 +354,33 @@ async def add(
Returns: The ID of the added delayed event.

Raises:
SynapseError: if the delayed event fails validation checks.
SynapseError: if the delayed event fails validation checks, or
if the requested delay is longer than allowed, or
if sending delayed events has been disallowed entirely.
"""
# Use standard request limiter for scheduling new delayed events.
# TODO: Instead apply ratelimiting based on the scheduled send time.
# See https://github.com/element-hq/synapse/issues/18021
await self._request_ratelimiter.ratelimit(requester)

max_delay = self._config.server.max_event_delay_ms
if (
max_delay is None
or max_delay <= 0
or (limit := self._config.server.max_delayed_events_per_user) <= 0
):
raise SynapseError(
HTTPStatus.FORBIDDEN,
"Sending delayed events has been disallowed",
Codes.FORBIDDEN,
)
if delay > max_delay:
raise SynapseError(
HTTPStatus.FORBIDDEN,
"The requested delay exceeds the allowed maximum",
Codes.FORBIDDEN,
)

self._event_creation_handler.validator.validate_builder(
self._event_creation_handler.event_builder_factory.for_room_version(
await self._store.get_room_version(room_id),
Expand All @@ -386,6 +407,7 @@ async def add(
content=content,
delay=delay,
sticky_duration_ms=sticky_duration_ms,
limit=limit,
)

if self._repl_client is not None:
Expand Down
5 changes: 5 additions & 0 deletions synapse/rest/client/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
"capabilities"
]["m.profile_fields"]

response["capabilities"]["org.matrix.msc4140.delayed_events"] = {
"max_delay_ms": self.config.server.max_event_delay_ms or 0,
"max_scheduled": self.config.server.max_delayed_events_per_user,
}

if self.config.experimental.msc4267_enabled:
response["capabilities"]["org.matrix.msc4267.forget_forced_upon_leave"] = {
"enabled": self.config.room.forget_on_leave,
Expand Down
40 changes: 5 additions & 35 deletions synapse/rest/client/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ def __init__(self, hs: "HomeServer"):
self.auth = hs.get_auth()
self.clock = hs.get_clock()
self._event_serializer = hs.get_event_client_serializer()
self._max_event_delay_ms = hs.config.server.max_event_delay_ms
self._spam_checker_module_callbacks = hs.get_module_api_callbacks().spam_checker
self._msc4354_enabled = hs.config.experimental.msc4354_enabled

Expand Down Expand Up @@ -343,7 +342,7 @@ async def on_PUT(
if self._msc4354_enabled:
sticky_duration_ms = parse_integer(request, StickyEvent.QUERY_PARAM_NAME)

delay = _parse_request_delay(request, self._max_event_delay_ms)
delay = _parse_request_delay(request)
if delay is not None:
delay_id = await self.delayed_events_handler.add(
requester,
Expand Down Expand Up @@ -416,7 +415,6 @@ def __init__(self, hs: "HomeServer"):
self.event_creation_handler = hs.get_event_creation_handler()
self.delayed_events_handler = hs.get_delayed_events_handler()
self.auth = hs.get_auth()
self._max_event_delay_ms = hs.config.server.max_event_delay_ms
self._msc4354_enabled = hs.config.experimental.msc4354_enabled

def register(self, http_server: HttpServer) -> None:
Expand All @@ -442,7 +440,7 @@ async def _do(
if self._msc4354_enabled:
sticky_duration_ms = parse_integer(request, StickyEvent.QUERY_PARAM_NAME)

delay = _parse_request_delay(request, self._max_event_delay_ms)
delay = _parse_request_delay(request)
if delay is not None:
delay_id = await self.delayed_events_handler.add(
requester,
Expand Down Expand Up @@ -515,47 +513,19 @@ async def on_PUT(
)


def _parse_request_delay(
request: SynapseRequest,
max_delay: int | None,
) -> int | None:
def _parse_request_delay(request: SynapseRequest) -> int | None:
"""Parses from the request string the delay parameter for
delayed event requests, and checks it for correctness.

Args:
request: the twisted HTTP request.
max_delay: the maximum allowed value of the delay parameter,
or None if no delay parameter is allowed.
Returns:
The value of the requested delay, or None if it was absent.

Raises:
SynapseError: if the delay parameter is present and forbidden,
or if it exceeds the maximum allowed value.
SynapseError: if the delay parameter is present and invalid.
"""
delay = parse_integer(request, "org.matrix.msc4140.delay")
if delay is None:
return None
if max_delay is None:
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"Delayed events are not supported on this server",
Codes.UNKNOWN,
{
"org.matrix.msc4140.errcode": "M_MAX_DELAY_UNSUPPORTED",
},
)
if delay > max_delay:
raise SynapseError(
HTTPStatus.BAD_REQUEST,
"The requested delay exceeds the allowed maximum.",
Codes.UNKNOWN,
{
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
"org.matrix.msc4140.max_delay": max_delay,
},
)
return delay
return parse_integer(request, "org.matrix.msc4140.delay")


# TODO: Needs unit testing for room ID + alias joins
Expand Down
63 changes: 62 additions & 1 deletion synapse/storage/databases/main/delayed_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import attr

from synapse.api.errors import NotFoundError
from synapse.api.errors import LimitExceededError, NotFoundError
from synapse.storage._base import SQLBaseStore, db_to_json
from synapse.storage.database import (
DatabasePool,
Expand Down Expand Up @@ -124,18 +124,79 @@ async def add_delayed_event(
content: JsonDict,
delay: int,
sticky_duration_ms: int | None,
limit: int,
) -> tuple[DelayID, Timestamp]:
"""
Inserts a new delayed event in the DB.

Args:
user_localpart: The localpart of the requester of the delayed event, who will be its owner.
device_id: The device ID of the requester.
creation_ts: The timestamp of when the request to add the delayed event was made.
room_id: The ID of the room where the event should be sent to.
event_type: The type of event to be sent.
state_key: The state key of the event to be sent, or None if it is not a state event.
origin_server_ts: The custom timestamp to send the event with.
If None, the timestamp will be the actual time when the event is sent.
content: The content of the event to be sent.
delay: How long (in milliseconds) to wait before automatically sending the event.
sticky_duration_ms: If an MSC4354 sticky event: the sticky duration (in milliseconds).
The event will be attempted to be reliably delivered to clients and remote servers
during its sticky period.
limit: The maximum number of delayed events the DB may store for the given requester.
Must be greater than 0.
Returns: The generated ID assigned to the added delayed event,
and the send time of the next delayed event to be sent,
which is either the event just added or one added earlier.

Raises:
LimitExceededError: if the DB has reached the limit of
how many delayed events it may store for the given requester.
ValueError: if the limit is not greater than 0.
"""
if limit <= 0:
raise ValueError("limit must be greater than 0")

delay_id = _generate_delay_id()
send_ts = Timestamp(creation_ts + delay)

def add_delayed_event_txn(txn: LoggingTransaction) -> Timestamp:
num_existing: int = self.db_pool.simple_select_one_onecol_txn(
txn,
table="delayed_events",
keyvalues={"user_localpart": user_localpart},
retcol="COUNT(*)",
)
Comment on lines +164 to +169

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double-checked there is a database index for this ✅ : "delayed_events_pkey" PRIMARY KEY, btree (user_localpart, delay_id)

$ psql --username=postgres synapse
psql (18.3)
Type "help" for help.

synapse=# \d+ delayed_events
                                            Table "public.delayed_events"
       Column       |  Type   | Collation | Nullable | Default | Storage  | Compression | Stats target | Description
--------------------+---------+-----------+----------+---------+----------+-------------+--------------+-------------
 delay_id           | text    |           | not null |         | extended |             |              |
 user_localpart     | text    |           | not null |         | extended |             |              |
 device_id          | text    |           |          |         | extended |             |              |
 delay              | bigint  |           | not null |         | plain    |             |              |
 send_ts            | bigint  |           | not null |         | plain    |             |              |
 room_id            | text    |           | not null |         | extended |             |              |
 event_type         | text    |           | not null |         | extended |             |              |
 state_key          | text    |           |          |         | extended |             |              |
 origin_server_ts   | bigint  |           |          |         | plain    |             |              |
 content            | text    |           | not null |         | extended |             |              |
 is_processed       | boolean |           | not null | false   | plain    |             |              |
 sticky_duration_ms | bigint  |           |          |         | plain    |             |              |
Indexes:
    "delayed_events_pkey" PRIMARY KEY, btree (user_localpart, delay_id)
    "delayed_events_idx" UNIQUE, btree (delay_id)
    "delayed_events_is_processed" btree (is_processed)
    "delayed_events_room_state_event_idx" btree (room_id, event_type, state_key) WHERE state_key IS NOT NULL
    "delayed_events_send_ts" btree (send_ts)
Not-null constraints:
    "delayed_events_delay_id_not_null" NOT NULL "delay_id"
    "delayed_events_user_localpart_not_null" NOT NULL "user_localpart"
    "delayed_events_delay_not_null" NOT NULL "delay"
    "delayed_events_send_ts_not_null" NOT NULL "send_ts"
    "delayed_events_room_id_not_null" NOT NULL "room_id"
    "delayed_events_event_type_not_null" NOT NULL "event_type"
    "delayed_events_content_not_null" NOT NULL "content"
    "delayed_events_is_processed_not_null" NOT NULL "is_processed"

@AndrewFerr AndrewFerr Jun 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that d62f440 / 34c5ce9 updates the query used here, but it should still use the index on send_ts.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing the new queries with EXPLAIN shows that they indeed use indexes.

Comment on lines +164 to +169

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this query be looking for is_processed: False only?

In this vein, there should probably be a test to make sure that if you sent N number delayed events (more than the configured limit) which are all in the past, you can still send more delayed events.

@AndrewFerr AndrewFerr Jun 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation done by d62f440 / 34c5ce9.

Will think about how to write the recommended test.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test added by e1a0ba4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This query wasn't updated to take into account is_processed: False yet

I don't think that test covers this case especially given it didn't catch this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This query wasn't updated to take into account is_processed: False yet

The change I did make is to have both queries here ignore is_processed, so that events in the midst of being sent will count towards the limit. The reason for doing so is to avoid the (hopefully very rare) case of there being a buildup of such mid-processed events.

As a reminder, is_processed means the condition for sending a delayed event has been reached (i.e. the delayed event has either reached its scheduled send time, or was requested to be sent on-demand), but the event has not yet landed in its target room. The DB tracks this to allow recovering delayed events that would otherwise be lost if the homeserver were to crash/restart between the time of a delayed event wanting to be sent & it actually getting sent.

da30132 adds a test to confirm that the limit properly counts is_processed events. It does so by simulating the server taking a long time to process delayed events, and checking that more events cannot be scheduled until the existing ones that exceed the limit have finished being processed.

test to make sure that if you sent N number delayed events (more than the configured limit) which are all in the past, you can still send more delayed events.

I believe this is already done, because the two tests that set a limit of N>0 both check that an N+1th delayed event can be scheduled after having waited for as long as Retry-After said to. But perhaps I misunderstood your request.

With that said, what I had thought you meant was to test lowering the limit after already having scheduled enough events to exceed the new limit. That's what e1a0ba4 was for.

if num_existing >= limit:
# Cover case of num_existing > limit, which can happen by reducing
# the configured limit after delayed events have already been added
# (or by calling this method with a limit lower than the configured one)
#
# FIXME: Remove "AS subquery" after dropping support for PostgreSQL <16
txn.execute(
"""
SELECT MAX(send_ts) FROM (
SELECT * FROM delayed_events
WHERE user_localpart = ?
ORDER BY send_ts ASC
LIMIT ?
) AS subquery
Comment on lines +178 to +183

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deserves a proper comment about what it's trying to do.

Explanation for myself: First find N (closest to being sent) existing delayed events over the configured limit. Then take the max send_ts of those which marks when they will be sent and make room for another delayed event.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There used to be a comment here, added by d62f440. I've added it back with 06a6a9e.

Comment on lines +178 to +183

@MadLittleMods MadLittleMods Jun 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I'd also be fine with the simplification where we just grab the max send_ts and ignore the edge case of the scheduled delayed events being more than the configured limit. Just call it out in the comments.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should admittedly be a rare edge case, but I still feel uneasy leaving it unhandled, especially since the extra cost of the more complex query is offset by it not being on a hot path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote "max send_ts" in my previous suggestion but I think I meant "min send_ts".

This should admittedly be a rare edge case, but I still feel uneasy leaving it unhandled

With my simplified suggestion, we're still handling the edge case. The edge case happens when the homeserver admin reduces max_delayed_events_per_user and you've already scheduled more delayed events; we'd just tell the user to wait until the next delayed event gets sent (min send_ts) to try again.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The downside of the simplified approach is that it makes the returned Retry-After duration not long enough of a wait if there are many more delayed events scheduled than the limit allows. (Note that the case of Retry-After being long enough in that situation is what's covered by test_delayed_event_custom_user_limit_exceeded.)

Granted, that situation is possible only when a homeserver admin restarts the server with a lower limit while users have enough scheduled events to exceed the new limit. Although I'd expect that situation be rare, there is nothing stopping it from happening, so it may as well be addressed.

""",
(
user_localpart,
num_existing - limit + 1,
),
)
row = txn.fetchone()
assert row
retry_after_ms = int(row[0]) - creation_ts
err = LimitExceededError(
limiter_name="add_delayed_event",
retry_after_ms=retry_after_ms if retry_after_ms > 0 else None,
)
err.msg = "The maximum number of delayed events has been reached."
raise err

self.db_pool.simple_insert_txn(
txn,
table="delayed_events",
Expand Down
52 changes: 52 additions & 0 deletions tests/config/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@
#
#


from typing import Any

import yaml

from synapse.config._base import ConfigError, RootConfig
from synapse.config.homeserver import HomeServerConfig
from synapse.config.server import ServerConfig, generate_ip_set, is_threepid_reserved
from synapse.types import JsonDict

from tests import unittest

Expand Down Expand Up @@ -189,6 +193,54 @@ def test_listeners_set_correctly_open_private_ports_true(self) -> None:

self.assertEqual(conf["listeners"], expected_listeners)

def test_max_delayed_events_enforces_positive(self) -> None:
Comment thread
AndrewFerr marked this conversation as resolved.
"""
Test that the configured maximum allowed delay must be a positive value if set,
as per documentation
"""

def generate_config(value: int) -> JsonDict:
return {"max_event_delay_duration": value}

_read_config(generate_config(1))

with self.assertRaises(ConfigError):
_read_config(generate_config(0))

with self.assertRaises(ConfigError):
_read_config(generate_config(-1))

def test_max_delayed_events_per_user_enforces_non_negative_int(self) -> None:
"""
Test that the configured maximum number of delayed events must be a non-negative value if set,
as a negative limit can never be satisfied
"""

def generate_config(value: Any) -> JsonDict:
return {
"experimental_features": {"msc4140_max_delayed_events_per_user": value}
}

for allowed_value in (0, 1):
_read_config(generate_config(allowed_value))

for disallowed_value in (-1, 0.5):
with self.assertRaises(ConfigError):
_read_config(generate_config(disallowed_value))


def _read_config(config_values: JsonDict) -> None:
ServerConfig(RootConfig()).read_config(
yaml.safe_load(
HomeServerConfig().generate_config(
config_dir_path="CONFDIR",
data_dir_path="/data_dir_path",
server_name="che.org",
)
)
| config_values
)


class GenerateIpSetTestCase(unittest.TestCase):
def test_empty(self) -> None:
Expand Down
36 changes: 36 additions & 0 deletions tests/rest/client/test_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,42 @@ def test_get_set_avatar_url_capabilities_avatar_url_disabled_msc4133(self) -> No
["avatar_url"],
)

def test_get_delayed_events_capabilities_default_config_msc4140(self) -> None:
access_token = self.login(self.localpart, self.password)

channel = self.make_request("GET", self.url, access_token=access_token)
capabilities = channel.json_body["capabilities"]

self.assertEqual(channel.code, HTTPStatus.OK)
self.assertEqual(
capabilities["org.matrix.msc4140.delayed_events"]["max_delay_ms"], 0
)
self.assertEqual(
capabilities["org.matrix.msc4140.delayed_events"]["max_scheduled"], 100
)

@override_config(
{
"max_event_delay_duration": "24h",
"experimental_features": {
"msc4140_max_delayed_events_per_user": 50,
},
}
)
def test_get_delayed_events_capabilities_custom_config_msc4140(self) -> None:
access_token = self.login(self.localpart, self.password)

channel = self.make_request("GET", self.url, access_token=access_token)
capabilities = channel.json_body["capabilities"]

self.assertEqual(channel.code, HTTPStatus.OK)
self.assertEqual(
capabilities["org.matrix.msc4140.delayed_events"]["max_delay_ms"], 86400000
)
self.assertEqual(
capabilities["org.matrix.msc4140.delayed_events"]["max_scheduled"], 50
)

@override_config({"enable_3pid_changes": False})
def test_get_change_3pid_capabilities_3pid_disabled(self) -> None:
"""Test if change 3pid is disabled that the server responds it."""
Expand Down
Loading
Loading