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[:content] == ""
598 assert represented[:reblog][:id] == to_string(activity.id)
599 assert represented[:emojis] == []
600 assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec())
603 test "a peertube video" do
607 Pleroma.Object.Fetcher.fetch_object_from_id(
608 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
611 %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
613 represented = StatusView.render("show.json", %{for: user, activity: activity})
615 assert represented[:id] == to_string(activity.id)
616 assert length(represented[:media_attachments]) == 1
617 assert_schema(represented, "Status", Pleroma.Web.ApiSpec.spec())
620 test "funkwhale audio" do
624 Pleroma.Object.Fetcher.fetch_object_from_id(
625 "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871"
628 %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
630 represented = StatusView.render("show.json", %{for: user, activity: activity})
632 assert represented[:id] == to_string(activity.id)
633 assert length(represented[:media_attachments]) == 1
636 test "a Mobilizon event" do
640 Pleroma.Object.Fetcher.fetch_object_from_id(
641 "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
644 %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
646 represented = StatusView.render("show.json", %{for: user, activity: activity})
648 assert represented[:id] == to_string(activity.id)
650 assert represented[:url] ==
651 "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
653 assert represented[:content] ==
654 "<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>"
657 describe "build_tags/1" do
658 test "it returns a a dictionary tags" do
664 "href" => "https://kawen.space/users/lain",
665 "name" => "@lain@kawen.space",
670 assert StatusView.build_tags(object_tags) == [
671 %{name: "fediverse", url: "http://localhost:4001/tag/fediverse"},
672 %{name: "mastodon", url: "http://localhost:4001/tag/mastodon"},
673 %{name: "nextcloud", url: "http://localhost:4001/tag/nextcloud"}
678 describe "rich media cards" do
679 test "a rich media card without a site name renders correctly" do
680 page_url = "http://example.com"
684 image: page_url <> "/example.jpg",
685 title: "Example website"
688 %{provider_name: "example.com"} =
689 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
692 test "a rich media card without a site name or image renders correctly" do
693 page_url = "http://example.com"
697 title: "Example website"
700 %{provider_name: "example.com"} =
701 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
704 test "a rich media card without an image renders correctly" do
705 page_url = "http://example.com"
709 site_name: "Example site name",
710 title: "Example website"
713 %{provider_name: "example.com"} =
714 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
717 test "a rich media card with all relevant data renders correctly" do
718 page_url = "http://example.com"
722 site_name: "Example site name",
723 title: "Example website",
724 image: page_url <> "/example.jpg",
725 description: "Example description"
728 %{provider_name: "example.com"} =
729 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
733 test "does not embed a relationship in the account" do
735 other_user = insert(:user)
738 CommonAPI.post(user, %{
739 status: "drink more water"
742 result = StatusView.render("show.json", %{activity: activity, for: other_user})
744 assert result[:account][:pleroma][:relationship] == %{}
745 assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
748 test "does not embed a relationship in the account in reposts" do
750 other_user = insert(:user)
753 CommonAPI.post(user, %{
757 {:ok, activity} = CommonAPI.repeat(activity.id, other_user)
759 result = StatusView.render("show.json", %{activity: activity, for: user})
761 assert result[:account][:pleroma][:relationship] == %{}
762 assert result[:reblog][:account][:pleroma][:relationship] == %{}
763 assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
766 test "visibility/list" do
769 {:ok, list} = Pleroma.List.create("foo", user)
771 {:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
773 status = StatusView.render("show.json", activity: activity)
775 assert status.visibility == "list"
778 test "has a field for parent visibility" do
780 poster = insert(:user)
782 {:ok, invisible} = CommonAPI.post(poster, %{status: "hey", visibility: "private"})
785 CommonAPI.post(poster, %{status: "hey", visibility: "private", in_reply_to_id: invisible.id})
787 status = StatusView.render("show.json", activity: visible, for: user)
788 refute status.pleroma.parent_visible
790 status = StatusView.render("show.json", activity: visible, for: poster)
791 assert status.pleroma.parent_visible
794 test "it shows edited_at" do
795 poster = insert(:user)
797 {:ok, post} = CommonAPI.post(poster, %{status: "hey"})
799 status = StatusView.render("show.json", activity: post)
800 refute status.edited_at
802 {:ok, _} = CommonAPI.update(poster, post, %{status: "mew mew"})
803 edited = Pleroma.Activity.normalize(post)
805 status = StatusView.render("show.json", activity: edited)
806 assert status.edited_at
809 test "with a source object" do
812 data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}}
815 activity = insert(:note_activity, note: note)
817 status = StatusView.render("show.json", activity: activity, with_source: true)
818 assert status.text == "object source"
821 describe "source.json" do
822 test "with a source object, renders both source and content type" do
825 data: %{"source" => %{"content" => "object source", "mediaType" => "text/markdown"}}
828 activity = insert(:note_activity, note: note)
830 status = StatusView.render("source.json", activity: activity)
831 assert status.text == "object source"
832 assert status.content_type == "text/markdown"
835 test "with a source string, renders source and put text/plain as the content type" do
836 note = insert(:note, data: %{"source" => "string source"})
837 activity = insert(:note_activity, note: note)
839 status = StatusView.render("source.json", activity: activity)
840 assert status.text == "string source"
841 assert status.content_type == "text/plain"