Repository: ruma/ruma
Pull request #57 (2020-06-15 20:20:03)

Fix failing deserialization when encountering an unknown field
This is an odd test failure with no difference other than one being `EventJson<StateEventStub<AnyStateEventContent>>` and the other `StateEventStub<AnyStateEventContent>` the `EventJson` test fails the other passes?

```bash
thread 'deserialize_aliases_stub_with_room_id_event_json' panicked at
'called `Result::unwrap()` on an `Err` value:
InvalidEvent { message: "expected `,` or `}` at line 1 column 162", kind: Validation }',
ruma-events/tests/state_event.rs:163:9
```

jplatte: Can you just drop the `EventJson` usage? I'll investigate what the issue there is (kinda seems like a `serde_json` bug, or maybe even sth. worse :S
jplatte: Also, since the `Validation` kind of that error was quite wrong and in fact there is no longer any use for that kind, which also makes `InvalidEvent` pretty useless, I've just removed it. Rebasing shouldn't be hard though.

Patch:
diff --git a/.gitignore b/.gitignore
index 96ef6c0b..a9d37c56 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
-/target
+target
 Cargo.lock
diff --git a/ruma-events-macros/src/content_enum.rs b/ruma-events-macros/src/content_enum.rs
index 4a207a1f..e54bb999 100644
--- a/ruma-events-macros/src/content_enum.rs
+++ b/ruma-events-macros/src/content_enum.rs
@@ -8,9 +8,12 @@ use syn::{
 };
 
 /// Create a content enum from `ContentEnumInput`.
+///
+/// This is the internals of the `event_content_enum!` macro.
 pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream> {
     let attrs = &input.attrs;
     let ident = &input.name;
+
     let event_type_str = &input.events;
 
     let variants = input.events.iter().map(to_camel_case).collect::<Vec<_>>();
@@ -54,11 +57,24 @@ pub fn expand_content_enum(input: ContentEnumInput) -> syn::Result<TokenStream>
         }
     };
 
+    let any_event_variant_impl = quote! {
+        impl #ident {
+            fn is_compatible(event_type: &str) -> bool {
+                match event_type {
+                    #( #event_type_str => true, )*
+                    _ => false,
+                }
+            }
+        }
+    };
+
     let marker_trait_impls = marker_traits(ident);
 
     Ok(quote! {
         #content_enum
 
+        #any_event_variant_impl
+
         #event_content_impl
 
         #marker_trait_impls
diff --git a/ruma-events-macros/src/event.rs b/ruma-events-macros/src/event.rs
index b58ee9ca..32532529 100644
--- a/ruma-events-macros/src/event.rs
+++ b/ruma-events-macros/src/event.rs
@@ -212,12 +212,14 @@ fn expand_deserialize_event(
             where
                 D: ::serde::de::Deserializer<'de>,
             {
-                #[derive(serde::Deserialize)]
+                #[derive(::serde::Deserialize)]
                 #[serde(field_identifier, rename_all = "snake_case")]
                 enum Field {
                     // since this is represented as an enum we have to add it so the JSON picks it up
                     Type,
-                    #( #enum_variants ),*
+                    #( #enum_variants, )*
+                    #[serde(other)]
+                    Unknown,
                 }
 
                 /// Visits the fields of an event struct to handle deserialization of
@@ -242,6 +244,7 @@ fn expand_deserialize_event(
 
                         while let Some(key) = map.next_key()? {
                             match key {
+                                Field::Unknown => continue,
                                 Field::Type => {
                                     if event_type.is_some() {
                                         return Err(::serde::de::Error::duplicate_field("type"));
diff --git a/ruma-events/Cargo.toml b/ruma-events/Cargo.toml
index 23c50476..2010fd50 100644
--- a/ruma-events/Cargo.toml
+++ b/ruma-events/Cargo.toml
@@ -27,3 +27,8 @@ maplit = "1.0.2"
 matches = "0.1.8"
 ruma-identifiers = { version = "0.16.2", path = "../ruma-identifiers", features = ["rand"] }
 trybuild = "1.0.28"
+criterion = "0.3.2"
+
+[[bench]]
+name = "event_deserialize"
+harness = false
diff --git a/ruma-events/benches/event_deserialize.rs b/ruma-events/benches/event_deserialize.rs
new file mode 100644
index 00000000..47026ade
--- /dev/null
+++ b/ruma-events/benches/event_deserialize.rs
@@ -0,0 +1,109 @@
+// `cargo bench` works, but if you use `cargo bench -- --save-baseline <name>`
+// or pass any other args to it, it fails with the error
+// `cargo bench unknown option --save-baseline`.
+// To pass args to criterion, use this form
+// `cargo bench --bench <name of the bench> -- --save-baseline <name>`.
+
+use criterion::{criterion_group, criterion_main, Criterion};
+use ruma_events::{
+    room::power_levels::PowerLevelsEventContent, AnyEvent, AnyRoomEvent, AnyStateEvent, EventJson,
+    StateEvent,
+};
+use serde_json::json;
+
+fn power_levels() -> serde_json::Value {
+    json!({
+        "content": {
+            "ban": 50,
+            "events": {
+                "m.room.avatar": 50,
+                "m.room.canonical_alias": 50,
+                "m.room.history_visibility": 100,
+                "m.room.name": 50,
+                "m.room.power_levels": 100
+            },
+            "events_default": 0,
+            "invite": 0,
+            "kick": 50,
+            "redact": 50,
+            "state_default": 50,
+            "users": {
+                "@example:localhost": 100
+            },
+            "users_default": 0
+        },
+        "event_id": "$15139375512JaHAW:localhost",
+        "origin_server_ts": 45,
+        "sender": "@example:localhost",
+        "room_id": "!room:localhost",
+        "state_key": "",
+        "type": "m.room.power_levels",
+        "unsigned": {
+            "age": 45
+        }
+    })
+}
+
+fn deserialize_any_event(c: &mut Criterion) {
+    let json_data = power_levels();
+
+    c.bench_function("deserialize to `AnyEvent`", |b| {
+        b.iter(|| {
+            let _ = serde_json::from_value::<EventJson<AnyEvent>>(json_data.clone())
+                .unwrap()
+                .deserialize()
+                .unwrap();
+        })
+    });
+}
+
+fn deserialize_any_room_event(c: &mut Criterion) {
+    let json_data = power_levels();
+
+    c.bench_function("deserialize to `AnyRoomEvent`", |b| {
+        b.iter(|| {
+            let _ = serde_json::from_value::<EventJson<AnyRoomEvent>>(json_data.clone())
+                .unwrap()
+                .deserialize()
+                .unwrap();
+        })
+    });
+}
+
+fn deserialize_any_state_event(c: &mut Criterion) {
+    let json_data = power_levels();
+
+    c.bench_function("deserialize to `AnyStateEvent`", |b| {
+        b.iter(|| {
+            let _ = serde_json::from_value::<EventJson<AnyStateEvent>>(json_data.clone())
+                .unwrap()
+                .deserialize()
+                .unwrap();
+        })
+    });
+}
+
+fn deserialize_specific_event(c: &mut Criterion) {
+    let json_data = power_levels();
+
+    c.bench_function("deserialize to `StateEvent<PowerLevelsEventContent>`", |b| {
+        b.iter(|| {
+            let _ = serde_json::from_value::<EventJson<StateEvent<PowerLevelsEventContent>>>(
+                json_data.clone(),
+            )
+            .unwrap()
+            .deserialize()
+            .unwrap();
+        })
+    });
+}
+
+criterion_group!(
+    benches,
+    deserialize_any_event,
+    deserialize_any_room_event,
+    deserialize_any_state_event,
+    deserialize_specific_event
+);
+
+criterion_main!(benches);
diff --git a/ruma-events/src/enums.rs b/ruma-events/src/enums.rs
index fb0fcabe..8f073907 100644
--- a/ruma-events/src/enums.rs
+++ b/ruma-events/src/enums.rs
@@ -1,5 +1,9 @@
 use ruma_events_macros::event_content_enum;
-use serde::{Deserialize, Serialize};
+use serde::{
+    de::{self, DeserializeOwned, Error as _},
+    Deserialize, Serialize,
+};
+use serde_json::value::RawValue as RawJsonValue;
 
 use crate::{
     event_kinds::{
@@ -116,7 +120,7 @@ pub type AnyStrippedStateEventStub = StrippedStateEventStub<AnyStateEventContent
 pub type AnyToDeviceEvent = ToDeviceEvent<AnyToDeviceEventContent>;
 
 /// Any event.
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(untagged)]
 pub enum AnyEvent {
     /// Any basic event.
@@ -134,7 +138,7 @@ pub enum AnyEvent {
 }
 
 /// Any room event.
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(untagged)]
 pub enum AnyRoomEvent {
     /// Any message event.
@@ -146,7 +150,7 @@ pub enum AnyRoomEvent {
 }
 
 /// Any room event stub (room event without a `room_id`, as returned in `/sync` responses)
-#[derive(Clone, Debug, Deserialize, Serialize)]
+#[derive(Clone, Debug, Serialize)]
 #[serde(untagged)]
 pub enum AnyRoomEventStub {
     /// Any message event stub
@@ -154,5 +158,91 @@ pub enum AnyRoomEventStub {
     /// `"m.room.redaction"` stub
     Redaction(RedactionEventStub),
     /// Any state event stub
-    StateEvent(AnyStateEventStub),
+    State(AnyStateEventStub),
+}
+
+#[derive(Deserialize)]
+struct EventDeHelper {
+    #[serde(rename = "type")]
+    ev_type: String,
+}
+
+fn from_raw_json_value<T, E>(val: &RawJsonValue) -> Result<T, E>
+where
+    T: DeserializeOwned,
+    E: de::Error,
+{
+    serde_json::from_str(val.get()).map_err(E::custom)
+}
+
+// FIXME `#[serde(untagged)]` deserialization fails for these enums which
+// is odd as we are doing basically the same thing here, investigate?
+impl<'de> de::Deserialize<'de> for AnyEvent {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        let json = <&RawJsonValue>::deserialize(deserializer)?;
+        let EventDeHelper { ev_type } = from_raw_json_value(json)?;
+
+        match ev_type.as_str() {
+            "m.room.redaction" => Ok(AnyEvent::Redaction(from_raw_json_value(json)?)),
+            "m.presence" => Ok(AnyEvent::Presence(from_raw_json_value(json)?)),
+            ev_type if AnyBasicEventContent::is_compatible(ev_type) => {
+                Ok(AnyEvent::Basic(from_raw_json_value(json)?))
+            }
+            ev_type if AnyEphemeralRoomEventContent::is_compatible(ev_type) => {
+                Ok(AnyEvent::Ephemeral(from_raw_json_value(json)?))
+            }
+            ev_type if AnyMessageEventContent::is_compatible(ev_type) => {
+                Ok(AnyEvent::Message(from_raw_json_value(json)?))
+            }
+            ev_type if AnyStateEventContent::is_compatible(ev_type) => {
+                Ok(AnyEvent::State(from_raw_json_value(json)?))
+            }
+            _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))),
+        }
+    }
+}
+
+impl<'de> de::Deserialize<'de> for AnyRoomEvent {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        let json = <&RawJsonValue>::deserialize(deserializer)?;
+        let EventDeHelper { ev_type } = from_raw_json_value(json)?;
+
+        match ev_type.as_str() {
+            "m.room.redaction" => Ok(AnyRoomEvent::Redaction(from_raw_json_value(json)?)),
+            ev_type if AnyMessageEventContent::is_compatible(ev_type) => {
+                Ok(AnyRoomEvent::Message(from_raw_json_value(json)?))
+            }
+            ev_type if AnyStateEventContent::is_compatible(ev_type) => {
+                Ok(AnyRoomEvent::State(from_raw_json_value(json)?))
+            }
+            _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))),
+        }
+    }
+}
+
+impl<'de> de::Deserialize<'de> for AnyRoomEventStub {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: de::Deserializer<'de>,
+    {
+        let json = <&RawJsonValue>::deserialize(deserializer)?;
+        let EventDeHelper { ev_type } = from_raw_json_value(json)?;
+
+        match ev_type.as_str() {
+            "m.room.redaction" => Ok(AnyRoomEventStub::Redaction(from_raw_json_value(json)?)),
+            ev_type if AnyMessageEventContent::is_compatible(ev_type) => {
+                Ok(AnyRoomEventStub::Message(from_raw_json_value(json)?))
+            }
+            ev_type if AnyStateEventContent::is_compatible(ev_type) => {
+                Ok(AnyRoomEventStub::State(from_raw_json_value(json)?))
+            }
+            _ => Err(D::Error::custom(format!("event type `{}` is not a valid event", ev_type))),
+        }
+    }
 }


Test patch:
diff --git a/ruma-events/tests/enums.rs b/ruma-events/tests/enums.rs
new file mode 100644
index 00000000..733c03ba
--- /dev/null
+++ b/ruma-events/tests/enums.rs
@@ -0,0 +1,245 @@
+use std::convert::TryFrom;
+
+use matches::assert_matches;
+use ruma_identifiers::RoomAliasId;
+use serde_json::{from_value as from_json_value, json, Value as JsonValue};
+
+use ruma_events::{
+    room::{
+        aliases::AliasesEventContent,
+        message::{MessageEventContent, TextMessageEventContent},
+        power_levels::PowerLevelsEventContent,
+    },
+    AnyEvent, AnyMessageEventContent, AnyRoomEvent, AnyRoomEventStub, AnyStateEventContent,
+    MessageEvent, MessageEventStub, StateEvent, StateEventStub,
+};
+
+fn message_event() -> JsonValue {
+    json!({
+        "content": {
+            "body": "baba",
+            "format": "org.matrix.custom.html",
+            "formatted_body": "<strong>baba</strong>",
+            "msgtype": "m.text"
+        },
+        "event_id": "$152037280074GZeOm:localhost",
+        "origin_server_ts": 1,
+        "sender": "@example:localhost",
+        "room_id": "!room:room.com",
+        "type": "m.room.message",
+        "unsigned": {
+            "age": 1
+        }
+    })
+}
+
+fn message_event_stub() -> JsonValue {
+    json!({
+        "content": {
+            "body": "baba",
+            "format": "org.matrix.custom.html",
+            "formatted_body": "<strong>baba</strong>",
+            "msgtype": "m.text"
+        },
+        "event_id": "$152037280074GZeOm:localhost",
+        "origin_server_ts": 1,
+        "sender": "@example:localhost",
+        "type": "m.room.message",
+        "unsigned": {
+            "age": 1
+        }
+    })
+}
+
+fn aliases_event() -> JsonValue {
+    json!({
+        "content": {
+            "aliases": ["#somewhere:localhost"]
+        },
+        "event_id": "$152037280074GZeOm:localhost",
+        "origin_server_ts": 1,
+        "sender": "@example:localhost",
+        "state_key": "",
+        "room_id": "!room:room.com",
+        "type": "m.room.aliases",
+        "unsigned": {
+            "age": 1
+        }
+    })
+}
+
+fn aliases_event_stub() -> JsonValue {
+    json!({
+        "content": {
+            "aliases": ["#somewhere:localhost"]
+        },
+        "event_id": "$152037280074GZeOm:localhost",
+        "origin_server_ts": 1,
+        "sender": "@example:localhost",
+        "state_key": "",
+        "type": "m.room.aliases",
+        "unsigned": {
+            "age": 1
+        }
+    })
+}
+
+#[test]
+fn power_event_stub_deserialization() {
+    let json_data = json!({
+        "content": {
+            "ban": 50,
+            "events": {
+                "m.room.avatar": 50,
+                "m.room.canonical_alias": 50,
+                "m.room.history_visibility": 100,
+                "m.room.name": 50,
+                "m.room.power_levels": 100
+            },
+            "events_default": 0,
+            "invite": 0,
+            "kick": 50,
+            "redact": 50,
+            "state_default": 50,
+            "users": {
+                "@example:localhost": 100
+            },
+            "users_default": 0
+        },
+        "event_id": "$15139375512JaHAW:localhost",
+        "origin_server_ts": 45,
+        "sender": "@example:localhost",
+        "state_key": "",
+        "type": "m.room.power_levels",
+        "unsigned": {
+            "age": 45
+        }
+    });
+
+    assert_matches!(
+        from_json_value::<AnyRoomEventStub>(json_data),
+        Ok(AnyRoomEventStub::State(
+            StateEventStub {
+                content: AnyStateEventContent::RoomPowerLevels(PowerLevelsEventContent {
+                    ban, ..
+                }),
+                ..
+            }
+        ))
+        if ban == js_int::Int::new(50).unwrap()
+    );
+}
+
+#[test]
+fn message_event_stub_deserialization() {
+    let json_data = message_event_stub();
+
+    assert_matches!(
+        from_json_value::<AnyRoomEventStub>(json_data),
+        Ok(AnyRoomEventStub::Message(
+            MessageEventStub {
+                content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent {
+                    body,
+                    formatted: Some(formatted),
+                    relates_to: None,
+                })),
+                ..
+            }
+        ))
+        if body == "baba" && formatted.body == "<strong>baba</strong>"
+    );
+}
+
+#[test]
+fn aliases_event_stub_deserialization() {
+    let json_data = aliases_event_stub();
+
+    assert_matches!(
+        from_json_value::<AnyRoomEventStub>(json_data),
+        Ok(AnyRoomEventStub::State(
+            StateEventStub {
+                content: AnyStateEventContent::RoomAliases(AliasesEventContent {
+                    aliases,
+                }),
+                ..
+            }
+        ))
+        if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ]
+    );
+}
+
+#[test]
+fn message_room_event_deserialization() {
+    let json_data = message_event();
+
+    assert_matches!(
+        from_json_value::<AnyRoomEvent>(json_data),
+        Ok(AnyRoomEvent::Message(
+            MessageEvent {
+                content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent {
+                    body,
+                    formatted: Some(formatted),
+                    relates_to: None,
+                })),
+                ..
+            }
+        ))
+        if body == "baba" && formatted.body == "<strong>baba</strong>"
+    );
+}
+
+#[test]
+fn alias_room_event_deserialization() {
+    let json_data = aliases_event();
+
+    assert_matches!(
+        from_json_value::<AnyRoomEvent>(json_data),
+        Ok(AnyRoomEvent::State(
+            StateEvent {
+                content: AnyStateEventContent::RoomAliases(AliasesEventContent {
+                    aliases,
+                }),
+                ..
+            }
+        ))
+        if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ]
+    );
+}
+
+#[test]
+fn message_event_deserialization() {
+    let json_data = message_event();
+
+    assert_matches!(
+        from_json_value::<AnyEvent>(json_data),
+        Ok(AnyEvent::Message(
+            MessageEvent {
+                content: AnyMessageEventContent::RoomMessage(MessageEventContent::Text(TextMessageEventContent {
+                    body,
+                    formatted: Some(formatted),
+                    relates_to: None,
+                })),
+                ..
+            }
+        ))
+        if body == "baba" && formatted.body == "<strong>baba</strong>"
+    );
+}
+
+#[test]
+fn alias_event_deserialization() {
+    let json_data = aliases_event();
+
+    assert_matches!(
+        from_json_value::<AnyEvent>(json_data),
+        Ok(AnyEvent::State(
+            StateEvent {
+                content: AnyStateEventContent::RoomAliases(AliasesEventContent {
+                    aliases,
+                }),
+                ..
+            }
+        ))
+        if aliases == vec![ RoomAliasId::try_from("#somewhere:localhost").unwrap() ]
+    );
+}
diff --git a/ruma-events/tests/state_event.rs b/ruma-events/tests/state_event.rs
index e19559cf..a303fad7 100644
--- a/ruma-events/tests/state_event.rs
+++ b/ruma-events/tests/state_event.rs
@@ -7,10 +7,29 @@ use js_int::UInt;
 use matches::assert_matches;
 use ruma_events::{
     room::{aliases::AliasesEventContent, avatar::AvatarEventContent, ImageInfo, ThumbnailInfo},
-    AnyStateEventContent, EventJson, StateEvent, UnsignedData,
+    AnyStateEventContent, EventJson, StateEvent, StateEventStub, UnsignedData,
 };
 use ruma_identifiers::{EventId, RoomAliasId, RoomId, UserId};
-use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
+use serde_json::{
+    from_value as from_json_value, json, to_value as to_json_value, Value as JsonValue,
+};
+
+fn aliases_event_with_prev_content() -> JsonValue {
+    json!({
+        "content": {
+            "aliases": [ "#somewhere:localhost" ]
+        },
+        "event_id": "$h29iv0s8:example.com",
+        "origin_server_ts": 1,
+        "prev_content": {
+            "aliases": [ "#inner:localhost" ]
+        },
+        "room_id": "!roomid:room.com",
+        "sender": "@carl:example.com",
+        "state_key": "",
+        "type": "m.room.aliases"
+    })
+}
 
 #[test]
 fn serialize_aliases_with_prev_content() {
@@ -21,7 +40,7 @@ fn serialize_aliases_with_prev_content() {
         event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
         origin_server_ts: UNIX_EPOCH + Duration::from_millis(1),
         prev_content: Some(AnyStateEventContent::RoomAliases(AliasesEventContent {
-            aliases: vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()],
+            aliases: vec![RoomAliasId::try_from("#inner:localhost").unwrap()],
         })),
         room_id: RoomId::try_from("!roomid:room.com").unwrap(),
         sender: UserId::try_from("@carl:example.com").unwrap(),
@@ -30,20 +49,7 @@ fn serialize_aliases_with_prev_content() {
     };
 
     let actual = to_json_value(&aliases_event).unwrap();
-    let expected = json!({
-        "content": {
-            "aliases": [ "#somewhere:localhost" ]
-        },
-        "event_id": "$h29iv0s8:example.com",
-        "origin_server_ts": 1,
-        "prev_content": {
-            "aliases": [ "#somewhere:localhost" ]
-        },
-        "room_id": "!roomid:room.com",
-        "sender": "@carl:example.com",
-        "state_key": "",
-        "type": "m.room.aliases",
-    });
+    let expected = aliases_event_with_prev_content();
 
     assert_eq!(actual, expected);
 }
@@ -97,20 +103,7 @@ fn deserialize_aliases_content() {
 
 #[test]
 fn deserialize_aliases_with_prev_content() {
-    let json_data = json!({
-        "content": {
-            "aliases": [ "#somewhere:localhost" ]
-        },
-        "event_id": "$h29iv0s8:example.com",
-        "origin_server_ts": 1,
-        "prev_content": {
-            "aliases": [ "#inner:localhost" ]
-        },
-        "room_id": "!roomid:room.com",
-        "sender": "@carl:example.com",
-        "state_key": "",
-        "type": "m.room.aliases"
-    });
+    let json_data = aliases_event_with_prev_content();
 
     assert_matches!(
         from_json_value::<EventJson<StateEvent<AnyStateEventContent>>>(json_data)
@@ -137,6 +130,31 @@ fn deserialize_aliases_with_prev_content() {
     );
 }
 
+#[test]
+fn deserialize_aliases_stub_with_room_id() {
+    let json_data = aliases_event_with_prev_content();
+
+    assert_matches!(
+        from_json_value::<StateEventStub<AnyStateEventContent>>(json_data)
+            .unwrap(),
+        StateEventStub {
+            content: AnyStateEventContent::RoomAliases(content),
+            event_id,
+            origin_server_ts,
+            prev_content: Some(AnyStateEventContent::RoomAliases(prev_content)),
+            sender,
+            state_key,
+            unsigned,
+        } if content.aliases == vec![RoomAliasId::try_from("#somewhere:localhost").unwrap()]
+            && event_id == EventId::try_from("$h29iv0s8:example.com").unwrap()
+            && origin_server_ts == UNIX_EPOCH + Duration::from_millis(1)
+            && prev_content.aliases == vec![RoomAliasId::try_from("#inner:localhost").unwrap()]
+            && sender == UserId::try_from("@carl:example.com").unwrap()
+            && state_key == ""
+            && unsigned.is_empty()
+    );
+}
+
 #[test]
 fn deserialize_avatar_without_prev_content() {
     let json_data = json!({