using PrivaPub.Api.Mastodon.Mappers; using PrivaPub.Federation.Objects; using PrivaPub.Models.Post; using System.Text.Json.Nodes; namespace PrivaPub.Tests.Federation { public class ObjectShapesTests { static NoteDocument Parse(string json) => NoteParser.Parse(JsonNode.Parse(json)); [Fact] public void Keeps_a_peertube_video_with_its_files_poster_captions_and_markdown_description() { var note = Parse(""" { "id": "https://tube.example/videos/watch/1", "type": "Video", "name": "A *talk*", "duration": "PT6299S", "mediaType": "text/markdown", "content": "**Bold** words", "sensitive": false, "views": 14771, "state": 1, "isLiveBroadcast": false, "commentsPolicy": 2, "downloadEnabled": true, "licence": { "identifier": "5", "name": "Attribution - Share Alike" }, "category": { "identifier": "2", "name": "Films" }, "attributedTo": [{ "type": "Person", "id": "https://tube.example/accounts/ann" }, { "type": "Group", "id": "https://tube.example/video-channels/films" }], "icon": [ { "type": "Image", "url": "https://tube.example/thumb-280.jpg", "mediaType": "image/jpeg", "width": 280, "height": 157 }, { "type": "Image", "url": "https://tube.example/thumb-1920.jpg", "mediaType": "image/jpeg", "width": 1920, "height": 1080 }, { "type": "Image", "url": "https://tube.example/thumb-850.jpg", "mediaType": "image/jpeg", "width": 850, "height": 480 } ], "subtitleLanguage": [{ "identifier": "en", "url": [{ "mediaType": "text/vtt", "href": "https://tube.example/en.vtt" }] }], "url": [ { "type": "Link", "mediaType": "text/html", "href": "https://tube.example/w/abc" }, { "type": "Link", "mediaType": "video/mp4", "href": "https://tube.example/720.mp4", "height": 720, "width": 1280, "fps": 24, "size": 942586378, "attachment": [{ "type": "PropertyValue", "name": "ffprobe_codec_type", "value": "audio" }, { "type": "PropertyValue", "name": "ffprobe_codec_type", "value": "video" }] }, { "type": "Link", "mediaType": "application/x-mpegURL", "href": "https://tube.example/master.m3u8" }, { "type": "Link", "mediaType": "application/x-bittorrent;x-scheme-handler/magnet", "href": "magnet:?xt=urn:btih:abc" } ] } """); Assert.Equal("https://tube.example/w/abc", note.Url); Assert.Equal("A *talk*", note.Title); Assert.Contains("Bold", note.ContentHtml); Assert.Equal("https://tube.example/thumb-850.jpg", note.CoverURL); Assert.Equal(6299, note.Video.DurationSeconds); var file = Assert.Single(note.Video.Variants); Assert.Equal(new[] { "audio", "video" }, file.Streams); Assert.Equal(942586378, file.Size); Assert.Equal("https://tube.example/master.m3u8", note.Video.PlaylistURL); Assert.Equal(2, note.Video.CommentsPolicy); Assert.Equal("Attribution - Share Alike", note.Video.Licence); Assert.Equal("https://tube.example/video-channels/films", note.Video.ChannelURI); Assert.Equal("https://tube.example/en.vtt", Assert.Single(note.Video.Captions).Url); Assert.Equal("https://tube.example/accounts/ann", note.AttributedTo); } [Fact] public void Keeps_a_mobilizon_event_with_its_time_place_and_online_link() { var note = Parse(""" { "id": "https://events.example/events/9", "type": "Event", "name": "Repair café", "summary": "
Saturday at the library
", "content": "Bring broken things.
", "startTime": "2026-10-03T10:00:00+02:00", "endTime": "2026-10-03T13:00:00+02:00", "timezone": "Europe/Rome", "joinMode": "free", "participantCount": 12, "maximumAttendeeCapacity": 40, "ical:status": "CONFIRMED", "location": { "type": "Place", "name": "Biblioteca", "latitude": 45.07, "longitude": 7.68, "address": { "type": "PostalAddress", "streetAddress": "Via Roma 1", "addressLocality": "Torino", "addressCountry": "IT" } }, "attachment": [{ "type": "Link", "name": "Website", "href": "https://meet.example/repair" }], "attributedTo": "https://events.example/@club" } """); Assert.Null(note.SpoilerText); Assert.False(note.Sensitive); Assert.Equal("Saturday at the library", note.Excerpt); Assert.Equal(new DateTime(2026, 10, 3, 8, 0, 0, DateTimeKind.Utc), note.Event.Start); Assert.Equal("Via Roma 1, Torino, IT", Assert.Single(note.Event.Places).Address); Assert.Equal("https://meet.example/repair", note.Event.OnlineURL); Assert.Equal(40, note.Event.Capacity); Assert.Equal("CONFIRMED", note.Event.Status); Assert.Equal("2026-10-03 10:00 – 13:00 (Europe/Rome) · Biblioteca", MastodonMapper.EventLine(note.Event)); } [Fact] public void Keeps_a_lemmy_link_post_as_a_link_with_its_thumbnail() { var note = Parse(""" { "id": "https://lemmy.example/post/5", "type": "Page", "name": "A good read", "attributedTo": "https://lemmy.example/u/bo", "attachment": [{ "type": "Link", "href": "https://news.example/story", "mediaType": "text/html" }], "image": { "type": "Image", "url": "https://lemmy.example/pictrs/thumb.jpg" }, "language": { "identifier": "en", "name": "English" } } """); Assert.Equal("https://news.example/story", note.Link.Href); Assert.Equal("https://lemmy.example/pictrs/thumb.jpg", note.Link.ImageURL); Assert.False(note.Link.FromPublisher); Assert.Empty(note.Attachments); } [Fact] public void Takes_a_publisher_supplied_preview_from_a_link_attachment() { var note = Parse(""" { "id": "https://m.example/statuses/1", "type": "Note", "attributedTo": "https://m.example/users/a", "content": "look
", "attachment": [{ "type": "Link", "href": "https://news.example/a", "preview": { "type": "Article", "name": "Headline & more", "summary": "What happened
", "image": { "type": "Image", "url": "https://news.example/a.jpg" } } }] } """); Assert.Equal("Headline & more", note.Link.Title); Assert.Equal("What happened", note.Link.Description); Assert.Equal("https://news.example/a.jpg", note.Link.ImageURL); Assert.True(note.Link.FromPublisher); } [Fact] public void Reads_media_in_every_shape_and_infers_what_is_missing() { var note = Parse(""" { "id": "https://bsky.brid.gy/convert/ap/at://did:plc:x/post/1", "type": "Note", "attributedTo": "https://bsky.brid.gy/ap/did:plc:x", "content": "pics
", "@context": ["https://www.w3.org/ns/activitystreams", { "@language": "de" }], "attachment": [ { "type": "Image", "url": "https://cdn.example/a.jpeg", "summary": "alt from summary" }, { "type": "Document", "url": [{ "type": "Link", "href": "https://cdn.example/clip", "mediaType": "video/mp4" }], "icon": { "type": "Image", "url": "https://cdn.example/clip.jpg" }, "duration": "PT12.4S", "sensitive": true }, { "type": "Image", "url": "https://cdn.example/nameless" } ] } """); Assert.Equal("de", note.Language); Assert.Equal("image/jpeg", note.Attachments[0].ContentType); Assert.Equal("alt from summary", note.Attachments[0].Description); Assert.Equal("https://cdn.example/clip", note.Attachments[1].RemoteURL); Assert.Equal("video/mp4", note.Attachments[1].ContentType); Assert.Equal("https://cdn.example/clip.jpg", note.Attachments[1].RemotePreviewURL); Assert.Equal(12.4, note.Attachments[1].DurationSeconds); Assert.True(note.Attachments[1].Sensitive); Assert.Equal("image/*", note.Attachments[2].ContentType); } [Fact] public void Keeps_a_funkwhale_track_and_a_loops_poster() { var track = Parse(""" { "id": "https://music.example/federation/music/uploads/1", "type": "Audio", "name": "Song", "attributedTo": "https://music.example/@band", "summary": "#rock #live", "duration": 241, "position": 3, "album": "https://music.example/albums/2", "license": "https://creativecommons.org/licenses/by/4.0/", "url": [{ "type": "Link", "mediaType": "text/html", "href": "https://music.example/library/tracks/1" }, { "type": "Link", "mediaType": "audio/mpeg", "href": "https://music.example/listen/1.mp3", "bitrate": 320000, "size": 9640000 }] } """); var loop = Parse(""" { "id": "https://loops.example/v/1", "type": "Note", "attributedTo": "https://loops.example/@dancer", "content": "", "preview": { "type": "Image", "url": { "href": "https://loops.example/poster.jpg", "mediaType": "image/jpeg" } }, "attachment": [{ "type": "Document", "mediaType": "video/mp4", "url": "https://loops.example/v/1.mp4", "width": 720, "height": 1280 }] } """); Assert.Equal("#rock #live", track.Excerpt); Assert.Null(track.SpoilerText); Assert.Equal(241, track.Audio.DurationSeconds); Assert.Equal(3, track.Audio.Position); Assert.Equal("https://music.example/listen/1.mp3", Assert.Single(track.Audio.Variants).Url); Assert.Equal("https://loops.example/poster.jpg", loop.CoverURL); } [Fact] public void Keeps_custom_emoji_and_drops_ones_that_could_not_render() { var note = Parse(""" { "id": "https://k.example/notes/1", "type": "Note", "attributedTo": "https://k.example/users/1", "content": ":blobcat: hi :bad:", "tag": [ { "type": "Emoji", "name": ":blobcat:", "updated": "2026-01-01T00:00:00Z", "icon": { "type": "Image", "mediaType": "image/png", "url": "https://k.example/emoji/blobcat.png" } }, { "type": "Emoji", "name": ":blobcat:", "icon": { "type": "Image", "url": "https://k.example/emoji/duplicate.png" } }, { "type": "Emoji", "name": ":bad name:", "icon": { "type": "Image", "url": "https://k.example/emoji/x.png" } }, { "type": "Emoji", "name": ":nourl:", "icon": { "type": "Image", "url": "data:image/png;base64,AAAA" } } ] } """); var emoji = Assert.Single(note.Emojis); Assert.Equal("blobcat", emoji.Shortcode); Assert.Equal("https://k.example/emoji/blobcat.png", emoji.URL); } [Fact] public void Reads_polls_from_mastodon_misskey_akkoma_and_closed_ones() { var mastodon = Parse(""" { "id": "https://m.example/q/1", "type": "Question", "attributedTo": "https://m.example/u", "content": "?", "endTime": "2026-10-02T00:00:00Z", "votersCount": 7, "oneOf": [{ "type": "Note", "name": "a", "replies": { "type": "Collection", "totalItems": 4 } }, { "type": "Note", "name": "b", "replies": { "type": "Collection", "totalItems": 3 } }] } """); var misskey = Parse(""" { "id": "https://k.example/q/1", "type": "Question", "attributedTo": "https://k.example/u", "_misskey_content": "?", "anyOf": [{ "type": "Note", "name": "x", "_misskey_votes": 2 }, { "type": "Note", "name": "y", "_misskey_votes": 5 }] } """); var closed = Parse(""" { "id": "https://g.example/q/1", "type": "Question", "attributedTo": "https://g.example/u", "content": "?", "closed": "2026-09-30T12:00:00Z", "oneOf": [{ "type": "Note", "name": "p" }, { "type": "Note", "name": "q" }] } """); var akkoma = Parse(""" { "id": "https://a.example/objects/1", "type": "Question", "attributedTo": "https://a.example/users/u", "content": "?", "closed": "2099-01-01T00:00:00.000000Z", "oneOf": [{ "type": "Note", "name": "p" }, { "type": "Note", "name": "q" }] } """); Assert.False(mastodon.Poll.Multiple); Assert.Equal(new[] { 4, 3 }, mastodon.Poll.Options.Select(o => o.Votes)); Assert.Equal(7, mastodon.Poll.VotersCount); Assert.True(misskey.Poll.Multiple); Assert.Equal(new[] { 2, 5 }, misskey.Poll.Options.Select(o => o.Votes)); Assert.Null(misskey.Poll.VotersCount); Assert.Equal(new DateTime(2026, 9, 30, 12, 0, 0, DateTimeKind.Utc), closed.Poll.ClosedAt); Assert.Null(akkoma.Poll.ClosedAt); Assert.Equal(new DateTime(2099, 1, 1, 0, 0, 0, DateTimeKind.Utc), akkoma.Poll.ExpiresAt); } [Fact] public void Finds_playable_files_inside_an_hls_only_peertube_video_and_picks_one_with_sound() { var note = Parse(""" { "id": "https://tube.example/videos/watch/2", "type": "Video", "name": "HLS only", "attributedTo": "https://tube.example/accounts/ann", "url": [ { "type": "Link", "mediaType": "text/html", "href": "https://tube.example/w/def" }, { "type": "Link", "mediaType": "application/x-mpegURL", "href": "https://tube.example/master.m3u8", "tag": [ { "type": "Link", "mediaType": "video/mp4", "href": "https://tube.example/1080-fragmented.mp4", "height": 1080, "attachment": [{ "type": "PropertyValue", "name": "ffprobe_codec_type", "value": "video" }, { "type": "PropertyValue", "name": "ffprobe_codec_type", "value": "audio" }] }, { "type": "Link", "mediaType": "video/mp4", "href": "https://tube.example/720-fragmented.mp4", "height": 720, "attachment": [{ "type": "PropertyValue", "name": "ffprobe_codec_type", "value": "video" }] }, { "type": "Link", "mediaType": "video/mp4", "href": "https://tube.example/480-fragmented.mp4", "height": 480, "attachment": [{ "type": "PropertyValue", "name": "ffprobe_codec_type", "value": "video" }, { "type": "PropertyValue", "name": "ffprobe_codec_type", "value": "audio" }] } ] } ] } """); Assert.Equal(3, note.Video.Variants.Count); Assert.Equal("https://tube.example/480-fragmented.mp4", MastodonMapper.BestVideo(note.Video.Variants).Url); } [Fact] public void Normalises_hashtags_like_mastodon() => Assert.Equal("fedi".Normalize(System.Text.NormalizationForm.FormKC).ToLowerInvariant(), Assert.Single(Parse(""" { "id": "https://x.example/1", "type": "Note", "attributedTo": "https://x.example/u", "content": "", "tag": [{ "type": "Hashtag", "name": "#FEDI" }] } """).Tags)); } }