1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
10 alias Pleroma.Conversation.Participation
15 alias Pleroma.UserRelationship
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.MastodonAPI.AccountView
18 alias Pleroma.Web.MastodonAPI.StatusView
20 import Pleroma.Factory
22 import OpenApiSpex.TestAssertions
25 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
29 test "has an emoji reaction list" do
31 other_user = insert(:user)
32 third_user = insert(:user)
33 {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
35 {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
36 {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, ":dinosaur:")
37 {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
38 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
39 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, ":dinosaur:")
41 activity = Repo.get(Activity, activity.id)
42 status = StatusView.render("show.json", activity: activity)
44 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
46 assert status[:pleroma][:emoji_reactions] == [
47 %{name: "☕", count: 2, me: false, url: nil, account_ids: [other_user.id, user.id]},
52 url: "http://localhost:4001/emoji/dino walking.gif",
53 account_ids: [other_user.id, user.id]
55 %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
58 status = StatusView.render("show.json", activity: activity, for: user)
60 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
62 assert status[:pleroma][:emoji_reactions] == [
63 %{name: "☕", count: 2, me: true, url: nil, account_ids: [other_user.id, user.id]},
68 url: "http://localhost:4001/emoji/dino walking.gif",
69 account_ids: [other_user.id, user.id]
71 %{name: "🍵", count: 1, me: false, url: nil, account_ids: [third_user.id]}
75 test "works correctly with badly formatted emojis" do
77 {:ok, activity} = CommonAPI.post(user, %{status: "yo"})
80 |> Object.normalize(fetch: false)
81 |> Object.update_data(%{"reactions" => %{"☕" => [user.ap_id], "x" => 1}})
83 activity = Activity.get_by_id(activity.id)
84 status = StatusView.render("show.json", activity: activity, for: user)
86 assert status[:pleroma][:emoji_reactions] == [
87 %{name: "☕", count: 1, me: true, url: nil, account_ids: [user.id]}
91 test "doesn't show reactions from muted and blocked users" do
93 other_user = insert(:user)
94 third_user = insert(:user)
96 {:ok, activity} = CommonAPI.post(user, %{status: "dae cofe??"})
98 {:ok, _} = User.mute(user, other_user)
99 {:ok, _} = User.block(other_user, third_user)
101 {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
103 activity = Repo.get(Activity, activity.id)
104 status = StatusView.render("show.json", activity: activity)
106 assert status[:pleroma][:emoji_reactions] == [
107 %{name: "☕", count: 1, me: false, url: nil, account_ids: [other_user.id]}
110 status = StatusView.render("show.json", activity: activity, for: user)
112 assert status[:pleroma][:emoji_reactions] == []
114 {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "☕")
116 status = StatusView.render("show.json", activity: activity)
118 assert status[:pleroma][:emoji_reactions] == [
124 account_ids: [third_user.id, other_user.id]
128 status = StatusView.render("show.json", activity: activity, for: user)
130 assert status[:pleroma][:emoji_reactions] == [
131 %{name: "☕", count: 1, me: false, url: nil, account_ids: [third_user.id]}
134 status = StatusView.render("show.json", activity: activity, for: other_user)
136 assert status[:pleroma][:emoji_reactions] == [
137 %{name: "☕", count: 1, me: true, url: nil, account_ids: [other_user.id]}
141 test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do
144 {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"})
145 [participation] = Participation.for_user(user)
148 StatusView.render("show.json",
150 with_direct_conversation_id: true,
154 assert status[:pleroma][:direct_conversation_id] == participation.id
156 status = StatusView.render("show.json", activity: activity, for: user)
157 assert status[:pleroma][:direct_conversation_id] == nil
158 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
161 test "returns the direct conversation id when given the `direct_conversation_id` option" do
164 {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"})
165 [participation] = Participation.for_user(user)
168 StatusView.render("show.json",
170 direct_conversation_id: participation.id,
174 assert status[:pleroma][:direct_conversation_id] == participation.id
175 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
178 test "returns a temporary ap_id based user for activities missing db users" do
181 {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"})
184 User.invalidate_cache(user)
187 "https://localhost/.well-known/webfinger?resource=acct:#{user.nickname}@localhost"
189 Tesla.Mock.mock_global(fn
190 %{method: :get, url: "http://localhost/.well-known/host-meta"} ->
191 %Tesla.Env{status: 404, body: ""}
193 %{method: :get, url: "https://localhost/.well-known/host-meta"} ->
194 %Tesla.Env{status: 404, body: ""}
200 %Tesla.Env{status: 404, body: ""}
203 %{account: ms_user} = StatusView.render("show.json", activity: activity)
205 assert ms_user.acct == "erroruser@example.com"
208 test "tries to get a user by nickname if fetching by ap_id doesn't work" do
211 {:ok, activity} = CommonAPI.post(user, %{status: "Hey @shp!", visibility: "direct"})
215 |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
218 User.invalidate_cache(user)
220 result = StatusView.render("show.json", activity: activity)
222 assert result[:account][:id] == to_string(user.id)
223 assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
226 test "a note with null content" do
227 note = insert(:note_activity)
228 note_object = Object.normalize(note, fetch: false)
232 |> Map.put("content", nil)
234 Object.change(note_object, %{data: data})
235 |> Object.update_and_set_cache()
237 User.get_cached_by_ap_id(note.data["actor"])
239 status = StatusView.render("show.json", %{activity: note})
241 assert status.content == ""
242 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
245 test "a note activity" do
246 note = insert(:note_activity)
247 object_data = Object.normalize(note, fetch: false).data
248 user = User.get_cached_by_ap_id(note.data["actor"])
250 convo_id = :erlang.crc32(object_data["context"]) |> Bitwise.band(Bitwise.bnot(0x8000_0000))
252 status = StatusView.render("show.json", %{activity: note})
255 (object_data["published"] || "")
256 |> String.replace(~r/\.\d+Z/, ".000Z")
259 id: to_string(note.id),
260 uri: object_data["id"],
261 url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note),
262 account: AccountView.render("show.json", %{user: user, skip_visibility_check: true}),
264 in_reply_to_account_id: nil,
267 content: HTML.filter_tags(object_data["content"]),
269 created_at: created_at,
281 spoiler_text: HTML.filter_tags(object_data["summary"]),
282 visibility: "public",
283 media_attachments: [],
288 name: "#{hd(object_data["tag"])}",
289 url: "http://localhost:4001/tag/#{hd(object_data["tag"])}"
298 static_url: "corndog.png",
299 visible_in_picker: false
304 conversation_id: convo_id,
305 context: object_data["context"],
306 in_reply_to_account_acct: nil,
307 content: %{"text/plain" => HTML.strip_tags(object_data["content"])},
308 spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
310 direct_conversation_id: nil,
313 parent_visible: false,
317 source: HTML.filter_tags(object_data["content"])
323 assert status == expected
324 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
327 test "tells if the message is muted for some reason" do
329 other_user = insert(:user)
331 {:ok, _user_relationships} = User.mute(user, other_user)
333 {:ok, activity} = CommonAPI.post(other_user, %{status: "test"})
335 relationships_opt = UserRelationship.view_relationships_option(user, [other_user])
337 opts = %{activity: activity}
338 status = StatusView.render("show.json", opts)
339 assert status.muted == false
340 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
342 status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt))
343 assert status.muted == false
345 for_opts = %{activity: activity, for: user}
346 status = StatusView.render("show.json", for_opts)
347 assert status.muted == true
349 status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt))
350 assert status.muted == true
351 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
354 test "tells if the message is thread muted" do
356 other_user = insert(:user)
358 {:ok, _user_relationships} = User.mute(user, other_user)
360 {:ok, activity} = CommonAPI.post(other_user, %{status: "test"})
361 status = StatusView.render("show.json", %{activity: activity, for: user})
363 assert status.pleroma.thread_muted == false
365 {:ok, activity} = CommonAPI.add_mute(user, activity)
367 status = StatusView.render("show.json", %{activity: activity, for: user})
369 assert status.pleroma.thread_muted == true
372 test "tells if the status is bookmarked" do
375 {:ok, activity} = CommonAPI.post(user, %{status: "Cute girls doing cute things"})
376 status = StatusView.render("show.json", %{activity: activity})
378 assert status.bookmarked == false
380 status = StatusView.render("show.json", %{activity: activity, for: user})
382 assert status.bookmarked == false
384 {:ok, _bookmark} = Bookmark.create(user.id, activity.id)
386 activity = Activity.get_by_id_with_object(activity.id)
388 status = StatusView.render("show.json", %{activity: activity, for: user})
390 assert status.bookmarked == true
394 note = insert(:note_activity)
397 {:ok, activity} = CommonAPI.post(user, %{status: "he", in_reply_to_status_id: note.id})
399 status = StatusView.render("show.json", %{activity: activity})
401 assert status.in_reply_to_id == to_string(note.id)
403 [status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
405 assert status.in_reply_to_id == to_string(note.id)
409 note = insert(:note_activity)
412 {:ok, activity} = CommonAPI.post(user, %{status: "hehe", quote_id: note.id})
414 status = StatusView.render("show.json", %{activity: activity})
416 assert status.quote_id == to_string(note.id)
418 [status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
420 assert status.quote_id == to_string(note.id)
423 test "a quote that we can't resolve" do
424 note = insert(:note_activity, quoteUri: "oopsie")
426 status = StatusView.render("show.json", %{activity: note})
428 assert is_nil(status.quote_id)
429 assert is_nil(status.quote)
432 test "a quote from a user we block" do
434 other_user = insert(:user)
435 blocked_user = insert(:user)
437 {:ok, _relationship} = User.block(user, blocked_user)
439 {:ok, activity} = CommonAPI.post(blocked_user, %{status: ":< i am ANGERY"})
440 {:ok, quote_activity} = CommonAPI.post(other_user, %{status: "hehe", quote_id: activity.id})
442 status = StatusView.render("show.json", %{activity: quote_activity, for: user})
443 assert is_nil(status.quote)
446 test "a quote from a user we mute" do
448 other_user = insert(:user)
449 blocked_user = insert(:user)
451 {:ok, _relationship} = User.mute(user, blocked_user)
453 {:ok, activity} = CommonAPI.post(blocked_user, %{status: ":< i am ANGERY"})
454 {:ok, quote_activity} = CommonAPI.post(other_user, %{status: "hehe", quote_id: activity.id})
456 status = StatusView.render("show.json", %{activity: quote_activity, for: user})
457 assert is_nil(status.quote)
460 test "contains mentions" do
462 mentioned = insert(:user)
464 {:ok, activity} = CommonAPI.post(user, %{status: "hi @#{mentioned.nickname}"})
466 status = StatusView.render("show.json", %{activity: activity})
468 assert status.mentions ==
469 Enum.map([mentioned], fn u -> AccountView.render("mention.json", %{user: u}) end)
471 assert_schema(status, "Status", Pleroma.Web.ApiSpec.spec())
474 test "create mentions from the 'to' field" do
475 %User{ap_id: recipient_ap_id} = insert(:user)
476 cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
481 "to" => [recipient_ap_id],
487 insert(:note_activity, %{
489 recipients: [recipient_ap_id | cc]
492 assert length(activity.recipients) == 3
494 %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
496 assert length(mentions) == 1
497 assert mention.url == recipient_ap_id
500 test "create mentions from the 'tag' field" do
501 recipient = insert(:user)
502 cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
510 "href" => recipient.ap_id,
511 "name" => recipient.nickname,
515 "href" => "https://example.com/search?tag=test",
524 insert(:note_activity, %{
526 recipients: [recipient.ap_id | cc]
529 assert length(activity.recipients) == 3
531 %{mentions: [mention] = mentions} = StatusView.render("show.json", %{activity: activity})
533 assert length(mentions) == 1
534 assert mention.url == recipient.ap_id
537 test "attachments" do
542 "mediaType" => "image/png",
548 "blurhash" => "UJJ8X[xYW,%Jtq%NNFbXB5j]IVM|9GV=WHRn",
556 remote_url: "someurl",
557 preview_url: "someurl",
560 pleroma: %{mime_type: "image/png"},
561 meta: %{original: %{width: 200, height: 100, aspect: 2}},
562 blurhash: "UJJ8X[xYW,%Jtq%NNFbXB5j]IVM|9GV=WHRn"
565 api_spec = Pleroma.Web.ApiSpec.spec()
567 assert expected == StatusView.render("attachment.json", %{attachment: object})
568 assert_schema(expected, "Attachment", api_spec)
570 # If theres a "id", use that instead of the generated one
571 object = Map.put(object, "id", 2)
572 result = StatusView.render("attachment.json", %{attachment: object})
574 assert %{id: "2"} = result
575 assert_schema(result, "Attachment", api_spec)
578 test "put the url advertised in the Activity in to the url attribute" do
579 id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
580 [activity] = Activity.search(nil, id)
582 status = StatusView.render("show.json", %{activity: activity})
584 assert status.uri == id
585 assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/"
590 activity = insert(:note_activity)
592 {:ok, reblog} = CommonAPI.repeat(activity.id, user)
594 represented = StatusView.render("show.json", %{for: user, activity: reblog})
596 assert represented[:id] == to_string(reblog.id)
597 assert represented[:reblog][:id] == to_string(activity.id)
598 assert represented[:emojis] == []
599 assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec())
602 test "a peertube video" do
606 Pleroma.Object.Fetcher.fetch_object_from_id(
607 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
610 %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
612 represented = StatusView.render("show.json", %{for: user, activity: activity})
614 assert represented[:id] == to_string(activity.id)
615 assert length(represented[:media_attachments]) == 1
616 assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec())
619 test "funkwhale audio" do
623 Pleroma.Object.Fetcher.fetch_object_from_id(
624 "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871"
627 %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
629 represented = StatusView.render("show.json", %{for: user, activity: activity})
631 assert represented[:id] == to_string(activity.id)
632 assert length(represented[:media_attachments]) == 1
635 test "a Mobilizon event" do
639 Pleroma.Object.Fetcher.fetch_object_from_id(
640 "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
643 %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
645 represented = StatusView.render("show.json", %{for: user, activity: activity})
647 assert represented[:id] == to_string(activity.id)
649 assert represented[:url] ==
650 "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
652 assert represented[:content] ==
653 "<p><a href=\"https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39\">Mobilizon Launching Party</a></p><p>Mobilizon is now federated! 🎉</p><p></p><p>You can view this event from other instances if they are subscribed to mobilizon.org, and soon directly from Mastodon and Pleroma. It is possible that you may see some comments from other instances, including Mastodon ones, just below.</p><p></p><p>With a Mobilizon account on an instance, you may <strong>participate</strong> at events from other instances and <strong>add comments</strong> on events.</p><p></p><p>Of course, it's still <u>a work in progress</u>: if reports made from an instance on events and comments can be federated, you can't block people right now, and moderators actions are rather limited, but this <strong>will definitely get fixed over time</strong> until first stable version next year.</p><p></p><p>Anyway, if you want to come up with some feedback, head over to our forum or - if you feel you have technical skills and are familiar with it - on our Gitlab repository.</p><p></p><p>Also, to people that want to set Mobilizon themselves even though we really don't advise to do that for now, we have a little documentation but it's quite the early days and you'll probably need some help. No worries, you can chat with us on our Forum or though our Matrix channel.</p><p></p><p>Check our website for more informations and follow us on Twitter or Mastodon.</p>"
656 describe "build_tags/1" do
657 test "it returns a a dictionary tags" do
663 "href" => "https://kawen.space/users/lain",
664 "name" => "@lain@kawen.space",
669 assert StatusView.build_tags(object_tags) == [
670 %{name: "fediverse", url: "http://localhost:4001/tag/fediverse"},
671 %{name: "mastodon", url: "http://localhost:4001/tag/mastodon"},
672 %{name: "nextcloud", url: "http://localhost:4001/tag/nextcloud"}
677 describe "rich media cards" do
678 test "a rich media card without a site name renders correctly" do
679 page_url = "http://example.com"
683 image: page_url <> "/example.jpg",
684 title: "Example website"
687 %{provider_name: "example.com"} =
688 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
691 test "a rich media card without a site name or image renders correctly" do
692 page_url = "http://example.com"
696 title: "Example website"
699 %{provider_name: "example.com"} =
700 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
703 test "a rich media card without an image renders correctly" do
704 page_url = "http://example.com"
708 site_name: "Example site name",
709 title: "Example website"
712 %{provider_name: "example.com"} =
713 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
716 test "a rich media card with all relevant data renders correctly" do
717 page_url = "http://example.com"
721 site_name: "Example site name",
722 title: "Example website",
723 image: page_url <> "/example.jpg",
724 description: "Example description"
727 %{provider_name: "example.com"} =
728 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
732 test "does not embed a relationship in the account" do
734 other_user = insert(:user)
737 CommonAPI.post(user, %{
738 status: "drink more water"
741 result = StatusView.render("show.json", %{activity: activity, for: other_user})
743 assert result[:account][:pleroma][:relationship] == %{}
744 assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
747 test "does not embed a relationship in the account in reposts" do
749 other_user = insert(:user)
752 CommonAPI.post(user, %{
756 {:ok, activity} = CommonAPI.repeat(activity.id, other_user)
758 result = StatusView.render("show.json", %{activity: activity, for: user})
760 assert result[:account][:pleroma][:relationship] == %{}
761 assert result[:reblog][:account][:pleroma][:relationship] == %{}
762 assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
765 test "visibility/list" do
768 {:ok, list} = Pleroma.List.create("foo", user)
770 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
772 status = StatusView.render("show.json", activity: activity)
774 assert status.visibility == "list"
777 test "has a field for parent visibility" do
779 poster = insert(:user)
781 {:ok, invisible} = CommonAPI.post(poster, %{status: "hey", visibility: "private"})
784 CommonAPI.post(poster, %{status: "hey", visibility: "private", in_reply_to_id: invisible.id})
786 status = StatusView.render("show.json", activity: visible, for: user)
787 refute status.pleroma.parent_visible
789 status = StatusView.render("show.json", activity: visible, for: poster)
790 assert status.pleroma.parent_visible
793 test "it shows edited_at" do
794 poster = insert(:user)
796 {:ok, post} = CommonAPI.post(poster, %{status: "hey"})
798 status = StatusView.render("show.json", activity: post)
799 refute status.edited_at
801 {:ok, _} = CommonAPI.update(poster, post, %{status: "mew mew"})
802 edited = Pleroma.Activity.normalize(post)
804 status = StatusView.render("show.json", activity: edited)
805 assert status.edited_at
808 test "with a source object" do
811 data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}}
814 activity = insert(:note_activity, note: note)
816 status = StatusView.render("show.json", activity: activity, with_source: true)
817 assert status.text == "object source"
820 describe "source.json" do
821 test "with a source object, renders both source and content type" do
824 data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}}
827 activity = insert(:note_activity, note: note)
829 status = StatusView.render("source.json", activity: activity)
830 assert status.text == "object source"
831 assert status.content_type == "text/markdown"
834 test "with a source string, renders source and put text/plain as the content type" do
835 note = insert(:note, data: %{"source" => "string source"})
836 activity = insert(:note_activity, note: note)
838 status = StatusView.render("source.json", activity: activity)
839 assert status.text == "string source"
840 assert status.content_type == "text/plain"