using PrivaPub.Federation.Objects; using PrivaPub.Models.Post; using System.Text.Json.Nodes; namespace PrivaPub.Tests.Federation { public class NoteParserTests { const string MastodonNote = """ { "id": "https://m.example/users/alice/statuses/1", "type": "Note", "summary": "spoilers", "inReplyTo": "https://privapub.test/peasants/bob/posts/abc", "published": "2026-09-30T10:00:00Z", "url": "https://m.example/@alice/1", "attributedTo": "https://m.example/users/alice", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://m.example/users/alice/followers", "https://privapub.test/peasants/bob"], "sensitive": true, "conversation": "tag:m.example,2026-09-30:objectId=1:objectType=Conversation", "context": "https://m.example/contexts/1", "content": "
", "contentMap": { "en": "hi
" }, "attachment": [{ "type": "Document", "mediaType": "image/png", "url": "https://files.m.example/a.png", "name": "a cat", "blurhash": "UBL_:rOpGG-oBUNG,qRj2so|=eE1w^n4S5NH", "focalPoint": [0.5, -2.0], "width": 640, "height": 480 }], "tag": [ { "type": "Mention", "href": "https://privapub.test/peasants/bob", "name": "@bob@privapub.test" }, { "type": "Hashtag", "href": "https://m.example/tags/fedi", "name": "#Fedi" }, { "type": "Emoji", "name": ":blob:", "icon": { "type": "Image", "url": "https://files.m.example/blob.png" } } ] } """; // Castopod's episode: its words in description, its sound in audio, the sound's name for a title [Fact] public void A_podcast_episode_is_read_with_its_words_sound_and_cover() { var episode = NoteParser.Parse(JsonNode.Parse(""" {"id":"https://castopod.test/@pod/episodes/one","type":"PodcastEpisode","published":"2026-10-06T09:08:17+00:00", "to":["https://www.w3.org/ns/activitystreams#Public"],"attributedTo":"https://castopod.test/@pod", "description":{"type":"Note","mediaType":"text/markdown","content":"Listen to the **meadow**","contentMap":{"en":"Listen to the meadow
"}}, "image":{"type":"Image","mediaType":"image/png","url":"https://castopod.test/media/cover.png"}, "audio":{"id":"https://castopod.test/audio/one.mp3","type":"Audio","name":"Episode one","size":16791,"duration":4.02, "url":{"href":"https://castopod.test/audio/one.mp3","type":"Link","mediaType":"audio/mpeg"}}} """)); Assert.Equal(("PodcastEpisode", "Episode one", "https://castopod.test/media/cover.png"), (episode.Type, episode.Title, episode.CoverURL)); Assert.Contains("meadow", episode.ContentHtml); var sound = Assert.Single(episode.Attachments); Assert.Equal(("https://castopod.test/audio/one.mp3", "audio/mpeg"), (sound.RemoteURL, sound.ContentType)); Assert.Equal(4.02, episode.Audio.DurationSeconds); } [Fact] public void Reads_a_mastodon_note() { var note = NoteParser.Parse(JsonNode.Parse(MastodonNote)); Assert.Equal("https://m.example/users/alice", note.AttributedTo); Assert.Equal("spoilers", note.SpoilerText); Assert.True(note.Sensitive); Assert.Equal("en", note.Language); Assert.Equal("https://m.example/@alice/1", note.Url); Assert.Equal("https://m.example/contexts/1", note.Context); Assert.DoesNotContain("","summary":"","published":"2026-10-06T08:00:00Z"}]}} """)); Assert.Equal(["first
", "second
"], note.FormerRepresentations.Select(r => r.ContentHtml)); Assert.Equal([new DateTime(2026, 10, 6, 8, 0, 0, DateTimeKind.Utc), new DateTime(2026, 10, 6, 8, 30, 0, DateTimeKind.Utc)], note.FormerRepresentations.Select(r => r.EditedAt)); Assert.Equal([(default(string), false), ("careful", true)], note.FormerRepresentations.Select(r => (r.SpoilerText, r.HasContentWarning))); } [Fact] public void Reads_a_lemmy_page_with_a_title() { var note = NoteParser.Parse(JsonNode.Parse(""" { "id": "https://l.example/post/7", "type": "Page", "name": "A title", "attributedTo": "https://l.example/u/carol", "audience": "https://l.example/c/cats", "content": "body
", "to": ["https://l.example/c/cats", "https://www.w3.org/ns/activitystreams#Public"], "published": "2026-09-30T10:00:00.123456+00:00" } """)); Assert.Equal("A title", note.Title); Assert.Equal("https://l.example/c/cats", note.Audience); Assert.False(note.Sensitive); } [Fact] public void Falls_back_to_misskey_content_and_takes_the_person_from_a_peertube_attribution() { var note = NoteParser.Parse(JsonNode.Parse(""" { "id": "https://k.example/notes/9", "type": "Note", "attributedTo": [{ "type": "Group", "id": "https://k.example/c/chan" }, { "type": "Person", "id": "https://k.example/users/9" }], "_misskey_content": "hello there\nnext", "_misskey_quote": "https://k.example/notes/8" } """)); Assert.Equal("https://k.example/users/9", note.AttributedTo); Assert.Equal("hello <b>there</b>body
" } """)); Assert.Equal(warns, note.Sensitive); Assert.Equal(warns ? "Saturday, 10:00 at the library" : null, note.SpoilerText); Assert.Equal(warns ? null : "Saturday, 10:00 at the library", note.Excerpt); } [Theory] [InlineData("""{ "id": "https://x.example/1", "type": "Person" }""")] [InlineData("""{ "id": "not a uri", "type": "Note" }""")] [InlineData("""[1, 2]""")] public void Refuses_what_is_not_a_note(string json) => Assert.Null(NoteParser.Parse(JsonNode.Parse(json))); } public class AddressingTests { const string Followers = "https://m.example/users/alice/followers"; [Theory] [InlineData(new[] { Addressing.Public }, new[] { Followers }, PostVisibility.Public)] [InlineData(new[] { "as:Public" }, new string[0], PostVisibility.Public)] [InlineData(new[] { Followers }, new[] { Addressing.Public }, PostVisibility.Unlisted)] [InlineData(new[] { Followers }, new[] { "https://privapub.test/peasants/bob" }, PostVisibility.FollowersOnly)] [InlineData(new[] { "https://privapub.test/peasants/bob" }, new string[0], PostVisibility.Direct)] [InlineData(new[] { "https://m.example/users/mallory/followers" }, new string[0], PostVisibility.Direct)] public void Classifies_like_mastodon(string[] to, string[] cc, PostVisibility expected) => Assert.Equal(expected, Addressing.Classify(to, cc, Followers)); } }