1
0
Fork 0
ruma/crates/ruma-events/tests/it/room_message.rs
Kévin Commaille 31bd360db8 events: Prevent hidden _Custom variant types to be deserialized
To prevent users from constructing them.

We also change some methods to access the custom data to work with all
variants, otherwise they might break when new variants are added.
2026-04-25 15:26:46 +02:00

1251 lines
40 KiB
Rust

use std::borrow::Cow;
use assert_matches2::assert_matches;
use ruma_common::{
OwnedDeviceId,
canonical_json::assert_to_canonical_json_eq,
owned_device_id, owned_mxc_uri, owned_user_id,
serde::{Base64, Raw},
user_id,
};
#[cfg(feature = "unstable-msc4274")]
use ruma_events::room::message::{GalleryItemType, GalleryMessageEventContent};
use ruma_events::{
Mentions,
key::verification::VerificationMethod,
room::{
EncryptedFile, EncryptedFileHash, EncryptedFileInfo, MediaSource, V2EncryptedFileInfo,
message::{
AddMentions, AudioMessageEventContent, EmoteMessageEventContent,
FileMessageEventContent, FormattedBody, ForwardThread, ImageMessageEventContent,
KeyVerificationRequestEventContent, MessageType, OriginalRoomMessageEvent,
OriginalSyncRoomMessageEvent, Relation, RoomMessageEventContent,
TextMessageEventContent, VideoMessageEventContent,
},
},
};
use serde_json::{Value as JsonValue, from_value as from_json_value, json};
#[test]
fn custom_msgtype_serialization_roundtrip() {
let json = json!({
"msgtype": "my_custom_msgtype",
"body": "my custom message",
"custom_field": "baba",
"another_one": "abab",
});
let msg: MessageType = from_json_value(json.clone()).unwrap();
assert_eq!(msg.msgtype(), "my_custom_msgtype");
assert_eq!(msg.body(), "my custom message");
assert_eq!(
JsonValue::Object(msg.data().into_owned()),
json!({
"custom_field": "baba",
"another_one": "abab",
})
);
assert_to_canonical_json_eq!(msg, json);
}
#[test]
fn text_msgtype_formatted_body_serialization() {
let message_event_content =
RoomMessageEventContent::text_html("Hello, World!", "Hello, <em>World</em>!");
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "Hello, World!",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": "Hello, <em>World</em>!",
})
);
}
#[test]
fn text_msgtype_plain_text_serialization() {
let message_event_content =
RoomMessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply");
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "> <@test:example.com> test\n\ntest reply",
"msgtype": "m.text"
})
);
}
#[test]
#[cfg(feature = "markdown")]
fn text_msgtype_markdown_serialization() {
use ruma_events::room::message::TextMessageEventContent;
let text = "Testing **bold** and _italic_!";
let formatted_message =
RoomMessageEventContent::new(MessageType::Text(TextMessageEventContent::markdown(text)));
assert_to_canonical_json_eq!(
formatted_message,
json!({
"body": text,
"formatted_body": "Testing <strong>bold</strong> and <em>italic</em>!",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
})
);
let text = "Testing a simple phrase…";
let plain_message_simple =
RoomMessageEventContent::new(MessageType::Text(TextMessageEventContent::markdown(text)));
assert_to_canonical_json_eq!(
plain_message_simple,
json!({
"body": text,
"msgtype": "m.text"
})
);
let text = "Testing\n\nSeveral\n\nParagraphs.";
let plain_message_paragraphs =
RoomMessageEventContent::new(MessageType::Text(TextMessageEventContent::markdown(text)));
assert_to_canonical_json_eq!(
plain_message_paragraphs,
json!({
"body": text,
"formatted_body": "<p>Testing</p>\n<p>Several</p>\n<p>Paragraphs.</p>\n",
"format": "org.matrix.custom.html",
"msgtype": "m.text"
})
);
let text = r#"Testing
A paragraph
with
a soft line break
* item 1
* item 2
item 2 (cont'd)
* item 3
```
line 1
line 2
```"#;
let plain_message_paragraphs =
RoomMessageEventContent::new(MessageType::Text(TextMessageEventContent::markdown(text)));
assert_to_canonical_json_eq!(
plain_message_paragraphs,
json!({
"body": text,
"formatted_body": r#"<p>Testing</p>
<p>A paragraph<br />
with<br />
a soft line break</p>
<ul>
<li>item 1</li>
<li>item 2<br />
item 2 (cont'd)</li>
<li>item 3</li>
</ul>
<pre><code>line 1
line 2
</code></pre>
"#,
"format": "org.matrix.custom.html",
"msgtype": "m.text"
})
);
}
#[test]
#[cfg(feature = "markdown")]
fn markdown_detection() {
use ruma_events::room::message::FormattedBody;
// No markdown
let formatted_body = FormattedBody::markdown("A simple message.");
assert_matches!(formatted_body, None);
// Multiple paragraphs trigger markdown
let formatted_body =
FormattedBody::markdown("A message\nwith\n\nmultiple\n\nparagraphs").unwrap();
assert_eq!(
formatted_body.body,
"<p>A message<br />\nwith</p>\n<p>multiple</p>\n<p>paragraphs</p>\n"
);
// HTML reserved symbols do not trigger markdown.
let formatted_body = FormattedBody::markdown("A message with & HTML < entities");
assert_matches!(formatted_body, None);
// HTML triggers markdown.
let formatted_body = FormattedBody::markdown("<span>An HTML message</span>").unwrap();
assert_eq!(formatted_body.body, "<span>An HTML message</span>");
}
#[test]
#[cfg(feature = "markdown")]
fn markdown_options() {
use ruma_events::room::message::FormattedBody;
// Tables
let formatted_body = FormattedBody::markdown(
"|head1|head2|\n\
|---|---|\n\
|body1|body2|\
",
);
assert_eq!(
formatted_body.unwrap().body,
"<table>\
<thead><tr><th>head1</th><th>head2</th></tr></thead>\
<tbody>\n<tr><td>body1</td><td>body2</td></tr>\n</tbody>\
</table>\n"
);
// Strikethrough
let formatted_body = FormattedBody::markdown("A message with a ~~strike~~");
assert_eq!(formatted_body.unwrap().body, "A message with a <del>strike</del>");
}
#[test]
fn verification_request_msgtype_deserialization() {
let user_id = user_id!("@example2:localhost");
let device_id: OwnedDeviceId = "XOWLHHFSWM".into();
let json_data = json!({
"body": "@example:localhost is requesting to verify your key, ...",
"msgtype": "m.key.verification.request",
"to": user_id,
"from_device": device_id,
"methods": [
"m.sas.v1",
"m.qr_code.show.v1",
"m.reciprocate.v1"
]
});
let content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(content.msgtype, MessageType::VerificationRequest(verification));
assert_eq!(verification.body, "@example:localhost is requesting to verify your key, ...");
assert_eq!(verification.to, user_id);
assert_eq!(verification.from_device, device_id);
assert_eq!(verification.methods.len(), 3);
assert!(verification.methods.contains(&VerificationMethod::SasV1));
assert_matches!(content.relates_to, None);
}
#[test]
fn verification_request_msgtype_serialization() {
let user_id = owned_user_id!("@example2:localhost");
let device_id = owned_device_id!("XOWLHHFSWM");
let body = "@example:localhost is requesting to verify your key, ...".to_owned();
let methods =
vec![VerificationMethod::SasV1, "m.qr_code.show.v1".into(), "m.reciprocate.v1".into()];
let content = MessageType::VerificationRequest(KeyVerificationRequestEventContent::new(
body.clone(),
methods.clone(),
device_id.clone(),
user_id.clone(),
));
assert_to_canonical_json_eq!(
content,
json!({
"body": body,
"msgtype": "m.key.verification.request",
"to": user_id,
"from_device": device_id,
"methods": methods,
}),
);
}
#[test]
fn content_deserialization_failure() {
let json_data = json!({
"body": "test",
"msgtype": "m.location",
"url": "http://example.com/audio.mp3"
});
assert_matches!(from_json_value::<RoomMessageEventContent>(json_data), Err(_));
}
#[test]
fn reply_thread_fallback() {
let thread_root = from_json_value::<OriginalRoomMessageEvent>(json!({
"content": {
"msgtype": "m.text",
"body": "Thread root",
},
"event_id": "$thread_root",
"origin_server_ts": 10_000,
"room_id": "!testroomid:example.org",
"sender": "@user:example.org",
"type": "m.room.message",
}))
.unwrap();
let threaded_message = from_json_value::<OriginalRoomMessageEvent>(json!({
"content": {
"msgtype": "m.text",
"body": "Threaded message",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$thread_root",
"is_falling_back": true,
"m.in_reply_to": {
"event_id": "$thread_root",
},
}
},
"event_id": "$threaded_message",
"origin_server_ts": 10_000,
"room_id": "!testroomid:example.org",
"sender": "@user:example.org",
"type": "m.room.message",
}))
.unwrap();
let reply_as_thread_fallback = RoomMessageEventContent::text_plain(
"Reply from a thread-incapable client",
)
.make_reply_to(&threaded_message, ForwardThread::Yes, AddMentions::No);
let relation = reply_as_thread_fallback.relates_to.unwrap();
assert_matches!(relation, Relation::Thread(thread_info));
assert_eq!(
thread_info.in_reply_to.map(|in_reply_to| in_reply_to.event_id),
Some(threaded_message.event_id)
);
assert_eq!(thread_info.event_id, thread_root.event_id);
assert!(thread_info.is_falling_back);
}
#[test]
fn reply_thread_serialization_roundtrip() {
let thread_root = from_json_value::<OriginalRoomMessageEvent>(json!({
"content": {
"msgtype": "m.text",
"body": "Thread root",
},
"event_id": "$thread_root",
"origin_server_ts": 10_000,
"room_id": "!testroomid:example.org",
"sender": "@user:example.org",
"type": "m.room.message",
}))
.unwrap();
let threaded_message = from_json_value::<OriginalRoomMessageEvent>(json!({
"content": {
"msgtype": "m.text",
"body": "Threaded message",
"m.relates_to": {
"rel_type": "m.thread",
"event_id": "$thread_root",
"is_falling_back": true,
"m.in_reply_to": {
"event_id": "$thread_root",
},
}
},
"event_id": "$threaded_message",
"origin_server_ts": 10_000,
"room_id": "!testroomid:example.org",
"sender": "@user:example.org",
"type": "m.room.message",
}))
.unwrap();
let reply_as_thread_fallback = RoomMessageEventContent::text_plain(
"Reply from a thread client",
)
.make_reply_to(&threaded_message, ForwardThread::Yes, AddMentions::No);
let as_raw = Raw::new(&reply_as_thread_fallback).unwrap();
let reply_as_thread_fallback = as_raw.deserialize().unwrap();
let relation = reply_as_thread_fallback.relates_to.unwrap();
assert_matches!(relation, Relation::Thread(thread_info));
assert_eq!(
thread_info.in_reply_to.map(|in_reply_to| in_reply_to.event_id),
Some(threaded_message.event_id)
);
assert_eq!(thread_info.event_id, thread_root.event_id);
assert!(thread_info.is_falling_back);
}
#[test]
fn reply_add_mentions() {
let user = owned_user_id!("@user:example.org");
let friend = owned_user_id!("@friend:example.org");
let other_friend = owned_user_id!("@other_friend:example.org");
let first_message = from_json_value::<OriginalRoomMessageEvent>(json!({
"content": {
"msgtype": "m.text",
"body": "My friend!",
"m.mentions": {
"user_ids": [friend],
},
},
"event_id": "$143273582443PhrSn",
"origin_server_ts": 10_000,
"room_id": "!testroomid:example.org",
"sender": user,
"type": "m.room.message",
}))
.unwrap();
let mut second_message = RoomMessageEventContent::text_plain("User! Other friend!")
.make_reply_to(&first_message, ForwardThread::Yes, AddMentions::Yes);
let mentions = second_message.mentions.clone().unwrap();
assert_eq!(mentions.user_ids, [user.clone()].into());
assert!(!mentions.room);
second_message =
second_message.add_mentions(Mentions::with_user_ids([user.clone(), other_friend.clone()]));
let mentions = second_message.mentions.clone().unwrap();
assert_eq!(mentions.user_ids, [other_friend.clone(), user.clone()].into());
assert!(!mentions.room);
second_message = second_message.add_mentions(Mentions::with_room_mention());
let mentions = second_message.mentions.unwrap();
assert_eq!(mentions.user_ids, [other_friend, user].into());
assert!(mentions.room);
}
#[test]
fn make_replacement() {
let content = RoomMessageEventContent::text_html(
"This is _an edited_ message.",
"This is <em>an edited</em> message.",
);
let original_message_json = json!({
"content": {
"body": "Hello, World!",
"msgtype": "m.text",
},
"event_id": "$143273582443PhrSn",
"origin_server_ts": 134_829_848,
"room_id": "!roomid:notareal.hs",
"sender": "@user:notareal.hs",
"type": "m.room.message",
});
let original_message: OriginalSyncRoomMessageEvent =
from_json_value(original_message_json).unwrap();
let content = content.make_replacement(&original_message);
assert_matches!(
content.msgtype,
MessageType::Text(TextMessageEventContent { body, formatted, .. })
);
assert_eq!(body, "* This is _an edited_ message.");
let formatted = formatted.unwrap();
assert_eq!(formatted.body, "* This is <em>an edited</em> message.");
assert_matches!(content.mentions, None);
}
#[test]
fn audio_msgtype_serialization() {
let message_event_content =
RoomMessageEventContent::new(MessageType::Audio(AudioMessageEventContent::plain(
"Upload: my_song.mp3".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/file"),
)));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "Upload: my_song.mp3",
"url": "mxc://notareal.hs/file",
"msgtype": "m.audio",
})
);
}
#[test]
fn audio_msgtype_deserialization() {
let json_data = json!({
"body": "Upload: my_song.mp3",
"url": "mxc://notareal.hs/file",
"msgtype": "m.audio",
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Audio(content));
assert_eq!(content.body, "Upload: my_song.mp3");
assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
assert!(content.caption().is_none());
}
#[test]
fn file_msgtype_plain_content_serialization() {
let message_event_content =
RoomMessageEventContent::new(MessageType::File(FileMessageEventContent::plain(
"Upload: my_file.txt".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/file"),
)));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "Upload: my_file.txt",
"url": "mxc://notareal.hs/file",
"msgtype": "m.file",
})
);
}
#[test]
fn file_msgtype_encrypted_content_serialization() {
let message_event_content =
RoomMessageEventContent::new(MessageType::File(FileMessageEventContent::encrypted(
"Upload: my_file.txt".to_owned(),
EncryptedFile::new(
owned_mxc_uri!("mxc://notareal.hs/file"),
V2EncryptedFileInfo::new(
Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(),
Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(),
)
.into(),
std::iter::once(EncryptedFileHash::Sha256(
Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(),
))
.collect(),
),
)));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "Upload: my_file.txt",
"file": {
"url": "mxc://notareal.hs/file",
"key": {
"kty": "oct",
"key_ops": ["decrypt", "encrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
"msgtype": "m.file",
})
);
}
#[test]
fn file_msgtype_plain_content_deserialization() {
let json_data = json!({
"body": "Upload: my_file.txt",
"url": "mxc://notareal.hs/file",
"msgtype": "m.file",
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::File(content));
assert_eq!(content.body, "Upload: my_file.txt");
assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
assert!(content.caption().is_none());
}
#[test]
fn file_msgtype_encrypted_content_deserialization() {
let json_data = json!({
"body": "Upload: my_file.txt",
"file": {
"url": "mxc://notareal.hs/file",
"key": {
"kty": "oct",
"key_ops": ["decrypt", "encrypt"],
"alg": "A256CTR",
"k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"ext": true
},
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "v2",
},
"msgtype": "m.file",
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::File(content));
assert_eq!(content.body, "Upload: my_file.txt");
assert_matches!(content.source, MediaSource::Encrypted(encrypted_file));
assert_eq!(encrypted_file.url, "mxc://notareal.hs/file");
assert_matches!(encrypted_file.info, EncryptedFileInfo::V2(_));
}
#[test]
fn file_msgtype_custom_encrypted_content_deserialization() {
let json_data = json!({
"body": "Upload: my_file.txt",
"file": {
"url": "mxc://notareal.hs/file",
"key": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A",
"iv": "S22dq3NAX8wAAAAAAAAAAA",
"hashes": {
"sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q"
},
"v": "local.custom.version",
},
"msgtype": "m.file",
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::File(content));
assert_eq!(content.body, "Upload: my_file.txt");
assert_matches!(content.source, MediaSource::Encrypted(encrypted_file));
assert_eq!(encrypted_file.url, "mxc://notareal.hs/file");
assert_eq!(encrypted_file.info.version(), "local.custom.version");
let encryption_data = &*encrypted_file.info.data();
assert_eq!(
encryption_data.get("key").unwrap().as_str(),
Some("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A")
);
}
#[test]
#[cfg(feature = "unstable-msc4274")]
fn gallery_msgtype_serialization_with_image() {
let message_event_content =
RoomMessageEventContent::new(MessageType::Gallery(GalleryMessageEventContent::new(
"My photos from [FOSDEM 2025](https://fosdem.org/2025/)".to_owned(),
Some(FormattedBody::html(
"My photos from <a href=\"https://fosdem.org/2025/\">FOSDEM 2025</a>",
)),
vec![GalleryItemType::Image(ImageMessageEventContent::plain(
"my_image.jpg".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/file"),
))],
)));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "My photos from [FOSDEM 2025](https://fosdem.org/2025/)",
"formatted_body": "My photos from <a href=\"https://fosdem.org/2025/\">FOSDEM 2025</a>",
"format": "org.matrix.custom.html",
"itemtypes": [{
"body": "my_image.jpg",
"url": "mxc://notareal.hs/file",
"itemtype": "m.image",
}],
"msgtype": "dm.filament.gallery",
})
);
}
#[test]
#[cfg(feature = "unstable-msc4274")]
fn gallery_msgtype_deserialization_with_image() {
let json_data = json!({
"body": "My photos from [FOSDEM 2025](https://fosdem.org/2025/)",
"formatted_body": "My photos from <a href=\"https://fosdem.org/2025/\">FOSDEM 2025</a>",
"format": "org.matrix.custom.html",
"itemtypes": [{
"body": "my_image.jpg",
"url": "mxc://notareal.hs/file",
"itemtype": "m.image",
}],
"msgtype": "dm.filament.gallery",
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Gallery(content));
assert_eq!(content.body, "My photos from [FOSDEM 2025](https://fosdem.org/2025/)");
assert_eq!(
content.formatted.unwrap().body,
"My photos from <a href=\"https://fosdem.org/2025/\">FOSDEM 2025</a>"
);
assert_matches!(&content.itemtypes.len(), 1);
assert_matches!(&content.itemtypes.first().unwrap(), GalleryItemType::Image(content));
assert_eq!(content.body, "my_image.jpg");
assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
assert!(content.caption().is_none());
}
#[test]
#[cfg(feature = "unstable-msc4274")]
fn gallery_msgtype_custom_itemtype_serialization_roundtrip() {
use assert_matches2::assert_let;
let json = json!({
"body": "My photos from [FOSDEM 2025](https://fosdem.org/2025/)",
"formatted_body": "My photos from <a href=\"https://fosdem.org/2025/\">FOSDEM 2025</a>",
"format": "org.matrix.custom.html",
"itemtypes": [{
"body": "my message body",
"custom_field": "baba",
"another_one": "abab",
"itemtype": "my_custom_itemtype",
}],
"msgtype": "dm.filament.gallery",
});
let event_content = from_json_value::<RoomMessageEventContent>(json.clone()).unwrap();
assert_let!(MessageType::Gallery(content) = &event_content.msgtype);
assert_eq!(content.body, "My photos from [FOSDEM 2025](https://fosdem.org/2025/)");
assert_eq!(
content.formatted.as_ref().unwrap().body,
"My photos from <a href=\"https://fosdem.org/2025/\">FOSDEM 2025</a>"
);
assert_eq!(content.itemtypes.len(), 1);
let itemtype = content.itemtypes.first().unwrap();
assert_eq!(itemtype.itemtype(), "my_custom_itemtype");
assert_eq!(itemtype.body(), "my message body");
assert_eq!(
JsonValue::Object(itemtype.data().into_owned()),
json!({
"custom_field": "baba",
"another_one": "abab",
})
);
assert_to_canonical_json_eq!(event_content, json);
}
#[test]
fn image_msgtype_serialization() {
let message_event_content =
RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::plain(
"Upload: my_image.jpg".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/file"),
)));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "Upload: my_image.jpg",
"url": "mxc://notareal.hs/file",
"msgtype": "m.image",
})
);
}
#[test]
fn image_msgtype_deserialization() {
let json_data = json!({
"body": "Upload: my_image.jpg",
"url": "mxc://notareal.hs/file",
"msgtype": "m.image",
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Image(content));
assert_eq!(content.body, "Upload: my_image.jpg");
assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
assert!(content.caption().is_none());
}
#[cfg(not(feature = "unstable-msc3488"))]
#[test]
fn location_msgtype_serialization() {
use ruma_events::room::message::LocationMessageEventContent;
let message_event_content =
RoomMessageEventContent::new(MessageType::Location(LocationMessageEventContent::new(
"Alice was at geo:51.5008,0.1247;u=35".to_owned(),
"geo:51.5008,0.1247;u=35".to_owned(),
)));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "Alice was at geo:51.5008,0.1247;u=35",
"geo_uri": "geo:51.5008,0.1247;u=35",
"msgtype": "m.location",
})
);
}
#[cfg(feature = "unstable-msc3488")]
#[test]
fn location_msgtype_serialization() {
use ruma_events::room::message::LocationMessageEventContent;
let message_event_content =
RoomMessageEventContent::new(MessageType::Location(LocationMessageEventContent::new(
"Alice was at geo:51.5008,0.1247;u=35".to_owned(),
"geo:51.5008,0.1247;u=35".to_owned(),
)));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "Alice was at geo:51.5008,0.1247;u=35",
"geo_uri": "geo:51.5008,0.1247;u=35",
"msgtype": "m.location",
"org.matrix.msc1767.text": "Alice was at geo:51.5008,0.1247;u=35",
"org.matrix.msc3488.asset": {
"type": "m.self",
},
"org.matrix.msc3488.location": {
"uri": "geo:51.5008,0.1247;u=35"
},
})
);
}
#[test]
fn location_msgtype_deserialization() {
let json_data = json!({
"body": "Alice was at geo:51.5008,0.1247;u=35",
"geo_uri": "geo:51.5008,0.1247;u=35",
"msgtype": "m.location",
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Location(content));
assert_eq!(content.body, "Alice was at geo:51.5008,0.1247;u=35");
assert_eq!(content.geo_uri, "geo:51.5008,0.1247;u=35");
}
#[test]
fn text_msgtype_body_deserialization() {
let json_data = json!({
"body": "test",
"msgtype": "m.text",
});
assert_matches!(
from_json_value::<RoomMessageEventContent>(json_data),
Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. })
);
assert_eq!(content.body, "test");
}
#[test]
fn text_msgtype_formatted_body_and_body_deserialization() {
let json_data = json!({
"body": "test",
"formatted_body": "<h1>test</h1>",
"format": "org.matrix.custom.html",
"msgtype": "m.text",
});
assert_matches!(
from_json_value::<RoomMessageEventContent>(json_data),
Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. })
);
assert_eq!(content.body, "test");
let formatted = content.formatted.unwrap();
assert_eq!(formatted.body, "<h1>test</h1>");
}
#[test]
fn notice_msgtype_serialization() {
let message_event_content =
RoomMessageEventContent::notice_plain("> <@test:example.com> test\n\ntest reply");
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "> <@test:example.com> test\n\ntest reply",
"msgtype": "m.notice",
})
);
}
#[test]
fn notice_msgtype_deserialization() {
let json_data = json!({
"body": "test",
"msgtype": "m.notice",
});
assert_matches!(
from_json_value::<RoomMessageEventContent>(json_data),
Ok(RoomMessageEventContent { msgtype: MessageType::Notice(content), .. })
);
assert_eq!(content.body, "test");
}
#[test]
fn emote_msgtype_serialization() {
let message_event_content = RoomMessageEventContent::new(MessageType::Emote(
EmoteMessageEventContent::plain("> <@test:example.com> test\n\ntest reply"),
));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "> <@test:example.com> test\n\ntest reply",
"msgtype": "m.emote",
})
);
}
#[test]
fn emote_msgtype_deserialization() {
let json_data = json!({
"body": "test",
"msgtype": "m.emote",
});
assert_matches!(
from_json_value::<RoomMessageEventContent>(json_data),
Ok(RoomMessageEventContent { msgtype: MessageType::Emote(content), .. })
);
assert_eq!(content.body, "test");
}
#[test]
fn video_msgtype_serialization() {
let message_event_content =
RoomMessageEventContent::new(MessageType::Video(VideoMessageEventContent::plain(
"Upload: my_video.mp4".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/file"),
)));
assert_to_canonical_json_eq!(
message_event_content,
json!({
"body": "Upload: my_video.mp4",
"url": "mxc://notareal.hs/file",
"msgtype": "m.video",
})
);
}
#[test]
fn video_msgtype_deserialization() {
let json_data = json!({
"body": "Upload: my_video.mp4",
"url": "mxc://notareal.hs/file",
"msgtype": "m.video",
});
let event_content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(event_content.msgtype, MessageType::Video(content));
assert_eq!(content.body, "Upload: my_video.mp4");
assert_matches!(&content.source, MediaSource::Plain(url));
assert_eq!(url, "mxc://notareal.hs/file");
assert!(content.caption().is_none());
}
#[test]
fn add_mentions_then_make_replacement() {
let alice = owned_user_id!("@alice:localhost");
let bob = owned_user_id!("@bob:localhost");
let original_message_json = json!({
"content": {
"body": "Hello, World!",
"msgtype": "m.text",
"m.mentions": {
"user_ids": [alice],
}
},
"event_id": "$143273582443PhrSn",
"origin_server_ts": 134_829_848,
"room_id": "!roomid:notareal.hs",
"sender": "@user:notareal.hs",
"type": "m.room.message",
});
let original_message: OriginalSyncRoomMessageEvent =
from_json_value(original_message_json).unwrap();
let mut content = RoomMessageEventContent::text_html(
"This is _an edited_ message.",
"This is <em>an edited</em> message.",
);
content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()]));
content = content.make_replacement(&original_message);
let mentions = content.mentions.unwrap();
assert_eq!(mentions.user_ids, [bob.clone()].into());
assert_matches!(content.relates_to, Some(Relation::Replacement(replacement)));
let mentions = replacement.new_content.mentions.unwrap();
assert_eq!(mentions.user_ids, [alice, bob].into());
}
#[test]
fn add_first_mentions_then_make_replacement() {
// Like `add_mentions_then_make_replacement`, but the initial event doesn't have
// mentions.
let alice = owned_user_id!("@alice:localhost");
let bob = owned_user_id!("@bob:localhost");
let original_message_json = json!({
"content": {
"body": "Hello, World!",
"msgtype": "m.text",
},
"event_id": "$143273582443PhrSn",
"origin_server_ts": 134_829_848,
"room_id": "!roomid:notareal.hs",
"sender": "@user:notareal.hs",
"type": "m.room.message",
});
let original_message: OriginalSyncRoomMessageEvent =
from_json_value(original_message_json).unwrap();
let mut content = RoomMessageEventContent::text_html(
"This is _an edited_ message.",
"This is <em>an edited</em> message.",
);
content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()]));
content = content.make_replacement(&original_message);
let mentions = content.mentions.unwrap();
assert_eq!(mentions.user_ids, [alice.clone(), bob.clone()].into());
assert_matches!(content.relates_to, Some(Relation::Replacement(replacement)));
let mentions = replacement.new_content.mentions.unwrap();
assert_eq!(mentions.user_ids, [alice, bob].into());
}
#[test]
fn make_replacement_then_add_mentions() {
let alice = owned_user_id!("@alice:localhost");
let bob = owned_user_id!("@bob:localhost");
let original_message_json = json!({
"content": {
"body": "Hello, World!",
"msgtype": "m.text",
"m.mentions": {
"user_ids": [alice],
}
},
"event_id": "$143273582443PhrSn",
"origin_server_ts": 134_829_848,
"room_id": "!roomid:notareal.hs",
"sender": "@user:notareal.hs",
"type": "m.room.message",
});
let original_message: OriginalSyncRoomMessageEvent =
from_json_value(original_message_json).unwrap();
let mut content = RoomMessageEventContent::text_html(
"This is _an edited_ message.",
"This is <em>an edited</em> message.",
);
content = content.make_replacement(&original_message);
content = content.add_mentions(Mentions::with_user_ids(vec![alice.clone(), bob.clone()]));
let mentions = content.mentions.unwrap();
assert_eq!(mentions.user_ids, [alice, bob].into());
assert_matches!(content.relates_to, Some(Relation::Replacement(replacement)));
assert!(replacement.new_content.mentions.is_none());
}
#[test]
fn mentions_room_deserialization() {
let json_data = json!({
"body": "room!",
"msgtype": "m.text",
"m.mentions": {
"room": true,
},
});
let content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
assert_matches!(content.msgtype, MessageType::Text(text));
assert_eq!(text.body, "room!");
let mentions = content.mentions.unwrap();
assert!(mentions.room);
}
#[test]
fn invalid_replacement() {
// As generated by Element Web: https://github.com/vector-im/element-web/issues/26554
let relation = json!({
"rel_type": "m.replace",
"event_id": "~!kCCQTCfnABLKGGvQjo:matrix.org:m1699715385559.77",
});
let json_data = json!({
"msgtype": "m.text",
"body": " * edited text",
"m.new_content": {
"msgtype": "m.text",
"body": "edited text",
"m.mentions": {},
},
"m.mentions": {},
"m.relates_to": relation
});
let content = from_json_value::<RoomMessageEventContent>(json_data).unwrap();
let relates_to = content.relates_to.unwrap();
let data = relates_to.data();
assert_matches!(&data, Cow::Borrowed(_)); // data is stored in JSON form because it's invalid
assert_eq!(JsonValue::Object(data.into_owned()), relation);
}
#[test]
fn test_audio_filename() {
let mut content = AudioMessageEventContent::plain(
"my_sound.ogg".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/abcdef"),
);
assert_eq!(content.filename(), "my_sound.ogg");
content.body = "This was a great podcast episode".to_owned();
content.filename = Some("sound.ogg".to_owned());
assert_eq!(content.filename(), "sound.ogg");
}
#[test]
fn test_audio_caption() {
let mut content = AudioMessageEventContent::plain(
"my_sound.ogg".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/abcdef"),
);
assert!(content.caption().is_none());
assert!(content.formatted_caption().is_none());
content.filename = Some("my_sound.ogg".to_owned());
assert!(content.caption().is_none());
assert!(content.formatted_caption().is_none());
content.body = "This was a great podcast episode".to_owned();
assert_eq!(content.caption(), Some("This was a great podcast episode"));
assert!(content.formatted_caption().is_none());
content.formatted =
Some(FormattedBody::html("This was a <em>great</em> podcast episode".to_owned()));
assert_eq!(content.caption(), Some("This was a great podcast episode"));
assert_eq!(
content.formatted_caption().map(|f| f.body.clone()),
Some("This was a <em>great</em> podcast episode".to_owned())
);
}
#[test]
fn test_file_filename() {
let mut content = FileMessageEventContent::plain(
"my_file.txt".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/abcdef"),
);
assert_eq!(content.filename(), "my_file.txt");
content.body = "Please check these notes".to_owned();
content.filename = Some("notes.txt".to_owned());
assert_eq!(content.filename(), "notes.txt");
}
#[test]
fn test_file_caption() {
let mut content = FileMessageEventContent::plain(
"my_file.txt".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/abcdef"),
);
assert!(content.caption().is_none());
assert!(content.formatted_caption().is_none());
content.filename = Some("my_file.txt".to_owned());
assert!(content.caption().is_none());
assert!(content.formatted_caption().is_none());
content.body = "Please check these notes".to_owned();
assert_eq!(content.caption(), Some("Please check these notes"));
assert!(content.formatted_caption().is_none());
content.formatted =
Some(FormattedBody::html("<strong>Please check these notes</strong>".to_owned()));
assert_eq!(content.caption(), Some("Please check these notes"));
assert_eq!(
content.formatted_caption().map(|f| f.body.clone()),
Some("<strong>Please check these notes</strong>".to_owned())
);
}
#[test]
fn test_image_filename() {
let mut content = ImageMessageEventContent::plain(
"my_image.jpg".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/abcdef"),
);
assert_eq!(content.filename(), "my_image.jpg");
content.body = "Check it out 😎".to_owned();
content.filename = Some("image.jpg".to_owned());
assert_eq!(content.filename(), "image.jpg");
}
#[test]
fn test_image_caption() {
let mut content = ImageMessageEventContent::plain(
"my_image.jpg".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/abcdef"),
);
assert!(content.caption().is_none());
assert!(content.formatted_caption().is_none());
content.filename = Some("my_image.jpg".to_owned());
assert!(content.caption().is_none());
assert!(content.formatted_caption().is_none());
content.body = "Check it out 😎".to_owned();
assert_eq!(content.caption(), Some("Check it out 😎"));
assert!(content.formatted_caption().is_none());
content.formatted = Some(FormattedBody::html("<h3>Check it out 😎</h3>".to_owned()));
assert_eq!(content.caption(), Some("Check it out 😎"));
assert_eq!(
content.formatted_caption().map(|f| f.body.clone()),
Some("<h3>Check it out 😎</h3>".to_owned())
);
}
#[test]
fn test_video_filename() {
let mut content = VideoMessageEventContent::plain(
"my_video.mp4".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/abcdef"),
);
assert_eq!(content.filename(), "my_video.mp4");
content.body = "You missed a great evening".to_owned();
content.filename = Some("video.mp4".to_owned());
assert_eq!(content.filename(), "video.mp4");
}
#[test]
fn test_video_caption() {
let mut content = VideoMessageEventContent::plain(
"my_video.mp4".to_owned(),
owned_mxc_uri!("mxc://notareal.hs/abcdef"),
);
assert!(content.caption().is_none());
assert!(content.formatted_caption().is_none());
content.filename = Some("my_video.mp4".to_owned());
assert!(content.caption().is_none());
assert!(content.formatted_caption().is_none());
content.body = "You missed a great evening".to_owned();
assert_eq!(content.caption(), Some("You missed a great evening"));
assert!(content.formatted_caption().is_none());
content.formatted =
Some(FormattedBody::html("You missed a <strong>great</strong> evening".to_owned()));
assert_eq!(content.caption(), Some("You missed a great evening"));
assert_eq!(
content.formatted_caption().map(|f| f.body.clone()),
Some("You missed a <strong>great</strong> evening".to_owned())
);
}