diff --git a/tests/test_utils/event_builders.py b/tests/test_utils/event_builders.py new file mode 100644 index 0000000000..88904c51d6 --- /dev/null +++ b/tests/test_utils/event_builders.py @@ -0,0 +1,140 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2026 Element Creations Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +# +"""Test-only helpers for building events. + +The Rust `Event` constructor strictly validates that all format-required +fields are present on the event dict. Most production code paths always +supply these, but tests routinely build minimal dicts that omit fields +like `depth`, `hashes`, `origin_server_ts`, `auth_events`, or +`prev_events`. This module provides `make_test_event`, which fills in +sensible defaults for the required fields based on the event format +version, so individual tests only need to specify the fields they +actually care about. +""" + +from typing import Any + +from synapse.api.room_versions import ( + EventFormatVersions, + RoomVersion, + RoomVersions, +) +from synapse.events import EventBase, make_event_from_dict +from synapse.federation.federation_base import event_from_pdu_json +from synapse.types import JsonDict + + +def default_event_fields(room_version: RoomVersion) -> JsonDict: + """Return the default values for every field required by `room_version`. + + Tests can call this directly when they need to merge defaults into a + builder (e.g. inside another helper) rather than constructing the + event up-front. + """ + defaults: JsonDict = { + "type": "m.test", + "sender": "@test:test", + "content": {}, + "depth": 1, + "origin_server_ts": 1, + "hashes": {"sha256": ""}, + } + + if room_version.event_format == EventFormatVersions.ROOM_V1_V2: + # V1 events store auth/prev as `[(event_id, hashes)]` pairs and + # carry an explicit `event_id` and `room_id`. + defaults["auth_events"] = [] + defaults["prev_events"] = [] + defaults["room_id"] = "!test:test" + defaults["event_id"] = "$test:test" + elif room_version.event_format in ( + EventFormatVersions.ROOM_V3, + EventFormatVersions.ROOM_V4_PLUS, + ): + # V2/V3 and V4 share the flat auth/prev list shape. V2/V3 always + # carry a `room_id`; V4 makes it optional on create events but + # required otherwise, so callers building non-create events + # supply it explicitly. + defaults["auth_events"] = [] + defaults["prev_events"] = [] + defaults["room_id"] = "!test:test" + else: + # V11 Hydra+ and VMSC4242 derive the room_id from the create + # event's ID, so we never default it here — providing one would + # break auth-event derivation for create events on these + # versions. Callers supply room_id explicitly on non-create + # events. + defaults["auth_events"] = [] + defaults["prev_events"] = [] + + if room_version.msc4242_state_dags: + defaults["prev_state_events"] = [] + + return defaults + + +def make_test_event( + event_dict: JsonDict | None = None, + room_version: RoomVersion = RoomVersions.V1, + internal_metadata_dict: JsonDict | None = None, + rejected_reason: str | None = None, + **fields: Any, +) -> EventBase: + """Build an `EventBase` with defaults for the strict-required fields. + + Pass an `event_dict` and/or `**fields` keyword arguments — both are + merged on top of the format-version defaults from + `default_event_fields`. Explicit values win over defaults, and + `**fields` wins over `event_dict` so call sites can override a + shared base dict with one-off tweaks. + + Args: + event_dict: Explicit event fields. Wins over defaults; loses to + `**fields`. + room_version: Determines which format-specific defaults apply. + internal_metadata_dict: Forwarded to `make_event_from_dict`. + rejected_reason: Forwarded to `make_event_from_dict`. + **fields: Additional event fields. Wins over `event_dict`. + + Returns: + The constructed `EventBase`. + """ + merged: JsonDict = { + **default_event_fields(room_version), + **(event_dict or {}), + **fields, + } + return make_event_from_dict( + merged, + room_version=room_version, + internal_metadata_dict=internal_metadata_dict, + rejected_reason=rejected_reason, + ) + + +def make_test_pdu_event( + pdu: JsonDict, + room_version: RoomVersion, + received_time: int | None = None, +) -> EventBase: + """Wrapper around `event_from_pdu_json` for test PDU dicts. + + Federation-side test fixtures often omit fields the strict Rust ctor + requires (e.g. `hashes`, `auth_events`, `prev_events`, `depth`) + because those tests focus on transport/auth flow rather than event + well-formedness. This helper layers in the same format-version + defaults as `make_test_event` before delegating. + """ + pdu = {**default_event_fields(room_version), **pdu} + return event_from_pdu_json(pdu, room_version, received_time=received_time)