2026-10-01 17:53:59 +02:00
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 ( "<strong>Bold</strong>" , 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" : "<p>Saturday at the library</p>" , "content" : "<p>Bring broken things.</p>" ,
"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" : "<p>look</p>" ,
"attachment" : [{ "type" : "Link" , "href" : "https://news.example/a" ,
"preview" : { "type" : "Article" , "name" : "Headline & more" , "summary" : "<p>What happened</p>" , "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" : "<p>pics</p>" ,
"@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 );
}
2026-10-01 18:35:20 +02:00
[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_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" }] }
""");
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 );
}
2026-10-01 17:53:59 +02:00
[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));
}
}