use std::borrow::Cow; use assert_matches::assert_matches; use js_int::uint; use ruma_common::{ event_id, events::{ key::verification::VerificationMethod, room::{ message::{ AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, ForwardThread, ImageMessageEventContent, KeyVerificationRequestEventContent, LocationMessageEventContent, MessageType, OriginalRoomMessageEvent, RoomMessageEventContent, TextMessageEventContent, VideoMessageEventContent, }, EncryptedFileInit, JsonWebKeyInit, MediaSource, }, MessageLikeUnsigned, }, mxc_uri, room_id, serde::Base64, user_id, MilliSecondsSinceUnixEpoch, OwnedDeviceId, }; use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; macro_rules! json_object { ( $($tt:tt)+ ) => { match serde_json::json!({ $($tt)+ }) { serde_json::value::Value::Object(map) => map, _ => panic!("Not a JSON object"), } } } #[test] fn custom_msgtype_serialization() { let json_data = json_object! { "custom_field": "baba", "another_one": "abab", }; let custom_msgtype = MessageType::new("my_custom_msgtype", "my message body".into(), json_data).unwrap(); assert_eq!( to_json_value(&custom_msgtype).unwrap(), json!({ "msgtype": "my_custom_msgtype", "body": "my message body", "custom_field": "baba", "another_one": "abab", }) ); } #[test] fn custom_msgtype_deserialization() { let json_data = json!({ "msgtype": "my_custom_msgtype", "body": "my custom message", "custom_field": "baba", "another_one": "abab", }); let expected_json_data = json_object! { "custom_field": "baba", "another_one": "abab", }; let custom_event: MessageType = from_json_value(json_data).unwrap(); assert_eq!(custom_event.msgtype(), "my_custom_msgtype"); assert_eq!(custom_event.body(), "my custom message"); assert_eq!(custom_event.data(), Cow::Owned(expected_json_data)); } #[test] fn text_msgtype_formatted_body_serialization() { let message_event_content = RoomMessageEventContent::text_html("Hello, World!", "Hello, World!"); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Hello, World!", "msgtype": "m.text", "format": "org.matrix.custom.html", "formatted_body": "Hello, World!", }) ); } #[test] fn text_msgtype_plain_text_serialization() { let message_event_content = RoomMessageEventContent::text_plain("> <@test:example.com> test\n\ntest reply"); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "> <@test:example.com> test\n\ntest reply", "msgtype": "m.text" }) ); } #[test] #[cfg(feature = "markdown")] fn text_msgtype_markdown_serialization() { use ruma_common::events::room::message::TextMessageEventContent; let formatted_message = RoomMessageEventContent::new(MessageType::Text( TextMessageEventContent::markdown("Testing **bold** and _italic_!"), )); assert_eq!( to_json_value(&formatted_message).unwrap(), json!({ "body": "Testing **bold** and _italic_!", "formatted_body": "

Testing bold and italic!

\n", "format": "org.matrix.custom.html", "msgtype": "m.text" }) ); let plain_message_simple = RoomMessageEventContent::new(MessageType::Text( TextMessageEventContent::markdown("Testing a simple phrase…"), )); assert_eq!( to_json_value(&plain_message_simple).unwrap(), json!({ "body": "Testing a simple phrase…", "msgtype": "m.text" }) ); let plain_message_paragraphs = RoomMessageEventContent::new(MessageType::Text( TextMessageEventContent::markdown("Testing\n\nSeveral\n\nParagraphs."), )); assert_eq!( to_json_value(&plain_message_paragraphs).unwrap(), json!({ "body": "Testing\n\nSeveral\n\nParagraphs.", "formatted_body": "

Testing

\n

Several

\n

Paragraphs.

\n", "format": "org.matrix.custom.html", "msgtype": "m.text" }) ); } #[test] #[cfg(feature = "markdown")] fn markdown_detection() { use ruma_common::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"); formatted_body.unwrap(); // HTML entities don't 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("An HTML message"); formatted_body.unwrap(); } #[test] #[cfg(feature = "markdown")] fn markdown_options() { use ruma_common::events::room::message::FormattedBody; // Tables let formatted_body = FormattedBody::markdown( "|head1|head2|\n\ |---|---|\n\ |body1|body2|\ ", ); assert_eq!( formatted_body.unwrap().body, "\ \ \n\n\
head1head2
body1body2
\n" ); // Strikethrough let formatted_body = FormattedBody::markdown("A message with a ~~strike~~"); assert_eq!(formatted_body.unwrap().body, "

A message with a strike

\n"); } #[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::(json_data).unwrap(); let verification = assert_matches!( content.msgtype, MessageType::VerificationRequest(verification) => 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 = user_id!("@example2:localhost").to_owned(); let device_id: OwnedDeviceId = "XOWLHHFSWM".into(); 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 json_data = json!({ "body": body, "msgtype": "m.key.verification.request", "to": user_id, "from_device": device_id, "methods": methods }); let content = MessageType::VerificationRequest(KeyVerificationRequestEventContent::new( body, methods, device_id, user_id, )); assert_eq!(to_json_value(&content).unwrap(), json_data,); } #[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::(json_data), Err(_)); } #[test] fn escape_tags_in_plain_reply_body() { let first_message = OriginalRoomMessageEvent { content: RoomMessageEventContent::text_plain("Usage: cp "), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), room_id: room_id!("!testroomid:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let second_message = RoomMessageEventContent::text_plain("Usage: rm ") .make_reply_to(&first_message, ForwardThread::Yes); let body = assert_matches!( first_message.content.msgtype, MessageType::Text(TextMessageEventContent { body, formatted: None, .. }) => body ); assert_eq!(body, "Usage: cp "); let (body, formatted) = assert_matches!( second_message.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!( body, "\ > <@user:example.org> Usage: cp \n\ Usage: rm \ " ); let formatted = formatted.unwrap(); assert_eq!( formatted.body, "\ \
\ In reply to \ @user:example.org\
\ Usage: cp <source> <destination>\
\
\ Usage: rm <path>\ " ); } #[test] #[cfg(feature = "unstable-sanitize")] fn reply_sanitize() { use ruma_common::events::room::message::ForwardThread; let first_message = OriginalRoomMessageEvent { content: RoomMessageEventContent::text_html( "# This is the first message", "

This is the first message

", ), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), room_id: room_id!("!testroomid:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let second_message = OriginalRoomMessageEvent { content: RoomMessageEventContent::text_html( "This is the _second_ message", "This is the second message", ) .make_reply_to(&first_message, ForwardThread::Yes), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), room_id: room_id!("!testroomid:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let final_reply = RoomMessageEventContent::text_html( "This is **my** reply", "This is my reply", ) .make_reply_to(&second_message, ForwardThread::Yes); let (body, formatted) = assert_matches!( first_message.content.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!(body, "# This is the first message"); let formatted = formatted.unwrap(); assert_eq!(formatted.body, "

This is the first message

"); let (body, formatted) = assert_matches!( second_message.content.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!( body, "\ > <@user:example.org> # This is the first message\n\ This is the _second_ message\ " ); let formatted = formatted.unwrap(); assert_eq!( formatted.body, "\ \
\ In reply to \ @user:example.org\
\

This is the first message

\
\
\ This is the second message\ " ); let (body, formatted) = assert_matches!( final_reply.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!( body, "\ > <@user:example.org> This is the _second_ message\n\ This is **my** reply\ " ); let formatted = formatted.unwrap(); assert_eq!( formatted.body, "\ \
\ In reply to \ @user:example.org\
\ This is the second message\
\
\ This is my reply\ " ); } #[test] fn make_replacement_no_reply() { let content = RoomMessageEventContent::text_html( "This is _an edited_ message.", "This is an edited message.", ); let event_id = event_id!("$143273582443PhrSn:example.org").to_owned(); let content = content.make_replacement(event_id, None); let (body, formatted) = assert_matches!( content.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!(body, "* This is _an edited_ message."); let formatted = formatted.unwrap(); assert_eq!(formatted.body, "* This is an edited message."); } #[test] fn make_replacement_with_reply() { let replied_to_message = OriginalRoomMessageEvent { content: RoomMessageEventContent::text_html( "# This is the first message", "

This is the first message

", ), event_id: event_id!("$143273582443PhrSn:example.org").to_owned(), origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(10_000)), room_id: room_id!("!testroomid:example.org").to_owned(), sender: user_id!("@user:example.org").to_owned(), unsigned: MessageLikeUnsigned::default(), }; let content = RoomMessageEventContent::text_html( "This is _an edited_ reply.", "This is an edited reply.", ); let event_id = event_id!("$143273582443PhrSn:example.org").to_owned(); let content = content.make_replacement(event_id, Some(&replied_to_message)); let (body, formatted) = assert_matches!( content.msgtype, MessageType::Text(TextMessageEventContent { body, formatted, .. }) => (body, formatted) ); assert_eq!( body, "\ > <@user:example.org> # This is the first message\n\ * This is _an edited_ reply.\ " ); let formatted = formatted.unwrap(); assert_eq!( formatted.body, "\ \
\ In reply to \ @user:example.org\
\

This is the first message

\
\
\ * This is an edited reply.\ " ); } #[test] fn audio_msgtype_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Audio(AudioMessageEventContent::plain( "Upload: my_song.mp3".to_owned(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ))); assert_eq!( to_json_value(&message_event_content).unwrap(), 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::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Audio(content) => content); assert_eq!(content.body, "Upload: my_song.mp3"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); } #[test] fn file_msgtype_plain_content_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::File(FileMessageEventContent::plain( "Upload: my_file.txt".to_owned(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ))); assert_eq!( to_json_value(&message_event_content).unwrap(), 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(), EncryptedFileInit { url: mxc_uri!("mxc://notareal.hs/file").to_owned(), key: JsonWebKeyInit { kty: "oct".to_owned(), key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()], alg: "A256CTR".to_owned(), k: Base64::parse("TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A").unwrap(), ext: true, } .into(), iv: Base64::parse("S22dq3NAX8wAAAAAAAAAAA").unwrap(), hashes: [( "sha256".to_owned(), Base64::parse("aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q").unwrap(), )] .into(), v: "v2".to_owned(), } .into(), ))); assert_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Upload: my_file.txt", "file": { "url": "mxc://notareal.hs/file", "key": { "kty": "oct", "key_ops": ["encrypt", "decrypt"], "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::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::File(content) => content); assert_eq!(content.body, "Upload: my_file.txt"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); } #[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": ["encrypt", "decrypt"], "alg": "A256CTR", "k": "TLlG_OpX807zzQuuwv4QZGJ21_u7weemFGYJFszMn9A", "ext": true }, "iv": "S22dq3NAX8wAAAAAAAAAAA", "hashes": { "sha256": "aWOHudBnDkJ9IwaR1Nd8XKoI7DOrqDTwt6xDPfVGN6Q" }, "v": "v2", }, "msgtype": "m.file", }); let event_content = from_json_value::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::File(content) => content); assert_eq!(content.body, "Upload: my_file.txt"); let encrypted_file = assert_matches!(content.source, MediaSource::Encrypted(f) => f); assert_eq!(encrypted_file.url, "mxc://notareal.hs/file"); } #[test] fn image_msgtype_serialization() { let message_event_content = RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::plain( "Upload: my_image.jpg".to_owned(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ))); assert_eq!( to_json_value(&message_event_content).unwrap(), 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::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Image(content) => content); assert_eq!(content.body, "Upload: my_image.jpg"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); } #[test] fn location_msgtype_serialization() { 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_eq!( to_json_value(&message_event_content).unwrap(), json!({ "body": "Alice was at geo:51.5008,0.1247;u=35", "geo_uri": "geo:51.5008,0.1247;u=35", "msgtype": "m.location", }) ); } #[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::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Location(c) => c); 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", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. }) => content ); assert_eq!(content.body, "test"); } #[test] fn text_msgtype_formatted_body_and_body_deserialization() { let json_data = json!({ "body": "test", "formatted_body": "

test

", "format": "org.matrix.custom.html", "msgtype": "m.text", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Text(content), .. }) => content ); assert_eq!(content.body, "test"); let formatted = content.formatted.unwrap(); assert_eq!(formatted.body, "

test

"); } #[test] fn notice_msgtype_serialization() { let message_event_content = RoomMessageEventContent::notice_plain("> <@test:example.com> test\n\ntest reply"); assert_eq!( to_json_value(&message_event_content).unwrap(), 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", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Notice(content), .. }) => 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_eq!( to_json_value(&message_event_content).unwrap(), 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", }); let content = assert_matches!( from_json_value::(json_data), Ok(RoomMessageEventContent { msgtype: MessageType::Emote(content), .. }) => 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(), mxc_uri!("mxc://notareal.hs/file").to_owned(), None, ))); assert_eq!( to_json_value(&message_event_content).unwrap(), 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::(json_data).unwrap(); let content = assert_matches!(event_content.msgtype, MessageType::Video(content) => content); assert_eq!(content.body, "Upload: my_video.mp4"); let url = assert_matches!(content.source, MediaSource::Plain(url) => url); assert_eq!(url, "mxc://notareal.hs/file"); }