total rebase
[anni] / test / pleroma / web / activity_pub / transmogrifier / note_handling_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
6   use Oban.Testing, repo: Pleroma.Repo
7   use Pleroma.DataCase
8
9   alias Pleroma.Activity
10   alias Pleroma.Object
11   alias Pleroma.User
12   alias Pleroma.Web.ActivityPub.Transmogrifier
13   alias Pleroma.Web.ActivityPub.Utils
14   alias Pleroma.Web.CommonAPI
15
16   import Mock
17   import Pleroma.Factory
18
19   setup_all do
20     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
21     :ok
22   end
23
24   setup do: clear_config([:instance, :max_remote_account_fields])
25
26   describe "handle_incoming" do
27     test "it works for incoming notices with tag not being an array (kroeg)" do
28       data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Jason.decode!()
29
30       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
31       object = Object.normalize(data["object"], fetch: false)
32
33       assert object.data["emoji"] == %{
34                "icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
35              }
36
37       data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Jason.decode!()
38
39       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
40       object = Object.normalize(data["object"], fetch: false)
41
42       assert "test" in Object.tags(object)
43       assert Object.hashtags(object) == ["test"]
44     end
45
46     test "it ignores an incoming notice if we already have it" do
47       activity = insert(:note_activity)
48
49       data =
50         File.read!("test/fixtures/mastodon-post-activity.json")
51         |> Jason.decode!()
52         |> Map.put("object", Object.normalize(activity, fetch: false).data)
53
54       {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
55
56       assert activity == returned_activity
57     end
58
59     @tag capture_log: true
60     test "it fetches reply-to activities if we don't have them" do
61       data =
62         File.read!("test/fixtures/mastodon-post-activity.json")
63         |> Jason.decode!()
64
65       object =
66         data["object"]
67         |> Map.put("inReplyTo", "https://mstdn.io/users/mayuutann/statuses/99568293732299394")
68
69       data = Map.put(data, "object", object)
70       {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
71       returned_object = Object.normalize(returned_activity, fetch: false)
72
73       assert %Activity{} =
74                Activity.get_create_by_object_ap_id(
75                  "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
76                )
77
78       assert returned_object.data["inReplyTo"] ==
79                "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
80     end
81
82     test "it does not fetch reply-to activities beyond max replies depth limit" do
83       data =
84         File.read!("test/fixtures/mastodon-post-activity.json")
85         |> Jason.decode!()
86
87       object =
88         data["object"]
89         |> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
90
91       data = Map.put(data, "object", object)
92
93       with_mock Pleroma.Web.Federator,
94         allowed_thread_distance?: fn _ -> false end do
95         {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
96
97         returned_object = Object.normalize(returned_activity, fetch: false)
98
99         refute Activity.get_create_by_object_ap_id(
100                  "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
101                )
102
103         assert returned_object.data["inReplyTo"] == "https://shitposter.club/notice/2827873"
104       end
105     end
106
107     @tag capture_log: true
108     test "it does not crash if the object in inReplyTo can't be fetched" do
109       data =
110         File.read!("test/fixtures/mastodon-post-activity.json")
111         |> Jason.decode!()
112
113       object =
114         data["object"]
115         |> Map.put("inReplyTo", "https://404.site/whatever")
116
117       data =
118         data
119         |> Map.put("object", object)
120
121       assert {:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
122     end
123
124     test "it does not work for deactivated users" do
125       data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
126
127       insert(:user, ap_id: data["actor"], is_active: false)
128
129       assert {:error, _} = Transmogrifier.handle_incoming(data)
130     end
131
132     test "it works for incoming notices" do
133       data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
134
135       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
136
137       assert data["id"] ==
138                "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
139
140       assert data["context"] ==
141                "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
142
143       assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
144
145       assert data["cc"] == [
146                "http://localtesting.pleroma.lol/users/lain",
147                "http://mastodon.example.org/users/admin/followers"
148              ]
149
150       assert data["actor"] == "http://mastodon.example.org/users/admin"
151
152       object_data = Object.normalize(data["object"], fetch: false).data
153
154       assert object_data["id"] ==
155                "http://mastodon.example.org/users/admin/statuses/99512778738411822"
156
157       assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
158
159       assert object_data["cc"] == [
160                "http://localtesting.pleroma.lol/users/lain",
161                "http://mastodon.example.org/users/admin/followers"
162              ]
163
164       assert object_data["actor"] == "http://mastodon.example.org/users/admin"
165       assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin"
166
167       assert object_data["context"] ==
168                "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
169
170       assert object_data["sensitive"] == true
171
172       user = User.get_cached_by_ap_id(object_data["actor"])
173
174       assert user.note_count == 1
175     end
176
177     test "it works for incoming notices without the sensitive property but an nsfw hashtag" do
178       data = File.read!("test/fixtures/mastodon-post-activity-nsfw.json") |> Jason.decode!()
179
180       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
181
182       object_data = Object.normalize(data["object"], fetch: false).data
183
184       assert object_data["sensitive"] == true
185     end
186
187     test "it works for incoming notices with hashtags" do
188       data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!()
189
190       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
191       object = Object.normalize(data["object"], fetch: false)
192
193       assert match?(
194                %{
195                  "href" => "http://localtesting.pleroma.lol/users/lain",
196                  "name" => "@lain@localtesting.pleroma.lol",
197                  "type" => "Mention"
198                },
199                Enum.at(object.data["tag"], 0)
200              )
201
202       assert match?(
203                %{
204                  "href" => "http://mastodon.example.org/tags/moo",
205                  "name" => "#moo",
206                  "type" => "Hashtag"
207                },
208                Enum.at(object.data["tag"], 1)
209              )
210
211       assert "moo" == Enum.at(object.data["tag"], 2)
212     end
213
214     test "it works for incoming notices with contentMap" do
215       data = File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Jason.decode!()
216
217       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
218       object = Object.normalize(data["object"], fetch: false)
219
220       assert object.data["content"] ==
221                "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
222     end
223
224     test "it works for incoming notices with a nil contentMap (firefish)" do
225       data =
226         File.read!("test/fixtures/mastodon-post-activity-contentmap.json")
227         |> Jason.decode!()
228         |> Map.put("contentMap", nil)
229
230       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
231       object = Object.normalize(data["object"], fetch: false)
232
233       assert object.data["content"] ==
234                "<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
235     end
236
237     test "it works for incoming notices with to/cc not being an array (kroeg)" do
238       data = File.read!("test/fixtures/kroeg-post-activity.json") |> Jason.decode!()
239
240       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
241       object = Object.normalize(data["object"], fetch: false)
242
243       assert object.data["content"] ==
244                "<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>"
245     end
246
247     test "it ensures that as:Public activities make it to their followers collection" do
248       user = insert(:user)
249
250       data =
251         File.read!("test/fixtures/mastodon-post-activity.json")
252         |> Jason.decode!()
253         |> Map.put("actor", user.ap_id)
254         |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
255         |> Map.put("cc", [])
256
257       object =
258         data["object"]
259         |> Map.put("attributedTo", user.ap_id)
260         |> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
261         |> Map.put("cc", [])
262         |> Map.put("id", user.ap_id <> "/activities/12345678")
263
264       data = Map.put(data, "object", object)
265
266       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
267
268       assert data["cc"] == [User.ap_followers(user)]
269     end
270
271     test "it ensures that address fields become lists" do
272       user = insert(:user)
273
274       data =
275         File.read!("test/fixtures/mastodon-post-activity.json")
276         |> Jason.decode!()
277         |> Map.put("actor", user.ap_id)
278         |> Map.put("cc", nil)
279
280       object =
281         data["object"]
282         |> Map.put("attributedTo", user.ap_id)
283         |> Map.put("cc", nil)
284         |> Map.put("id", user.ap_id <> "/activities/12345678")
285
286       data = Map.put(data, "object", object)
287
288       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
289
290       refute is_nil(data["cc"])
291     end
292
293     test "it strips internal likes" do
294       data =
295         File.read!("test/fixtures/mastodon-post-activity.json")
296         |> Jason.decode!()
297
298       likes = %{
299         "first" =>
300           "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
301         "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
302         "totalItems" => 3,
303         "type" => "OrderedCollection"
304       }
305
306       object = Map.put(data["object"], "likes", likes)
307       data = Map.put(data, "object", object)
308
309       {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data)
310
311       object = Object.normalize(activity)
312
313       assert object.data["likes"] == []
314     end
315
316     test "it strips internal reactions" do
317       user = insert(:user)
318       {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
319       {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
320
321       %{object: object} = Activity.get_by_id_with_object(activity.id)
322       assert Map.has_key?(object.data, "reactions")
323       assert Map.has_key?(object.data, "reaction_count")
324
325       object_data = Transmogrifier.strip_internal_fields(object.data)
326       refute Map.has_key?(object_data, "reactions")
327       refute Map.has_key?(object_data, "reaction_count")
328     end
329
330     test "it correctly processes messages with non-array to field" do
331       data =
332         File.read!("test/fixtures/mastodon-post-activity.json")
333         |> Poison.decode!()
334         |> Map.put("to", "https://www.w3.org/ns/activitystreams#Public")
335         |> put_in(["object", "to"], "https://www.w3.org/ns/activitystreams#Public")
336
337       assert {:ok, activity} = Transmogrifier.handle_incoming(data)
338
339       assert [
340                "http://localtesting.pleroma.lol/users/lain",
341                "http://mastodon.example.org/users/admin/followers"
342              ] == activity.data["cc"]
343
344       assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
345     end
346
347     test "it correctly processes messages with non-array cc field" do
348       data =
349         File.read!("test/fixtures/mastodon-post-activity.json")
350         |> Poison.decode!()
351         |> Map.put("cc", "http://mastodon.example.org/users/admin/followers")
352         |> put_in(["object", "cc"], "http://mastodon.example.org/users/admin/followers")
353
354       assert {:ok, activity} = Transmogrifier.handle_incoming(data)
355
356       assert ["http://mastodon.example.org/users/admin/followers"] == activity.data["cc"]
357       assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
358     end
359
360     test "it correctly processes messages with weirdness in address fields" do
361       data =
362         File.read!("test/fixtures/mastodon-post-activity.json")
363         |> Poison.decode!()
364         |> Map.put("cc", ["http://mastodon.example.org/users/admin/followers", ["¿"]])
365         |> put_in(["object", "cc"], ["http://mastodon.example.org/users/admin/followers", ["¿"]])
366
367       assert {:ok, activity} = Transmogrifier.handle_incoming(data)
368
369       assert ["http://mastodon.example.org/users/admin/followers"] == activity.data["cc"]
370       assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
371     end
372   end
373
374   describe "`handle_incoming/2`, Mastodon format `replies` handling" do
375     setup do: clear_config([:activitypub, :note_replies_output_limit], 5)
376     setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
377
378     setup do
379       data =
380         "test/fixtures/mastodon-post-activity.json"
381         |> File.read!()
382         |> Jason.decode!()
383
384       items = get_in(data, ["object", "replies", "first", "items"])
385       assert length(items) > 0
386
387       %{data: data, items: items}
388     end
389
390     test "schedules background fetching of `replies` items if max thread depth limit allows", %{
391       data: data,
392       items: items
393     } do
394       clear_config([:instance, :federation_incoming_replies_max_depth], 10)
395
396       {:ok, activity} = Transmogrifier.handle_incoming(data)
397
398       object = Object.normalize(activity.data["object"])
399
400       assert object.data["replies"] == items
401
402       for id <- items do
403         job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
404         assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
405       end
406     end
407
408     test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
409          %{data: data} do
410       clear_config([:instance, :federation_incoming_replies_max_depth], 0)
411
412       {:ok, _activity} = Transmogrifier.handle_incoming(data)
413
414       assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
415     end
416   end
417
418   describe "`handle_incoming/2`, Pleroma format `replies` handling" do
419     setup do: clear_config([:activitypub, :note_replies_output_limit], 5)
420     setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
421
422     setup do
423       replies = %{
424         "type" => "Collection",
425         "items" => [Utils.generate_object_id(), Utils.generate_object_id()]
426       }
427
428       activity =
429         File.read!("test/fixtures/mastodon-post-activity.json")
430         |> Poison.decode!()
431         |> Kernel.put_in(["object", "replies"], replies)
432
433       %{activity: activity}
434     end
435
436     test "schedules background fetching of `replies` items if max thread depth limit allows", %{
437       activity: activity
438     } do
439       clear_config([:instance, :federation_incoming_replies_max_depth], 1)
440
441       assert {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(activity)
442       object = Object.normalize(data["object"])
443
444       for id <- object.data["replies"] do
445         job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
446         assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
447       end
448     end
449
450     test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
451          %{activity: activity} do
452       clear_config([:instance, :federation_incoming_replies_max_depth], 0)
453
454       {:ok, _activity} = Transmogrifier.handle_incoming(activity)
455
456       assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
457     end
458   end
459
460   describe "reserialization" do
461     test "successfully reserializes a message with inReplyTo == nil" do
462       user = insert(:user)
463
464       message = %{
465         "@context" => "https://www.w3.org/ns/activitystreams",
466         "to" => ["https://www.w3.org/ns/activitystreams#Public"],
467         "cc" => [],
468         "type" => "Create",
469         "object" => %{
470           "to" => ["https://www.w3.org/ns/activitystreams#Public"],
471           "cc" => [],
472           "id" => Utils.generate_object_id(),
473           "type" => "Note",
474           "content" => "Hi",
475           "inReplyTo" => nil,
476           "attributedTo" => user.ap_id
477         },
478         "actor" => user.ap_id
479       }
480
481       {:ok, activity} = Transmogrifier.handle_incoming(message)
482
483       {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
484     end
485
486     test "successfully reserializes a message with AS2 objects in IR" do
487       user = insert(:user)
488
489       message = %{
490         "@context" => "https://www.w3.org/ns/activitystreams",
491         "to" => ["https://www.w3.org/ns/activitystreams#Public"],
492         "cc" => [],
493         "type" => "Create",
494         "object" => %{
495           "to" => ["https://www.w3.org/ns/activitystreams#Public"],
496           "cc" => [],
497           "id" => Utils.generate_object_id(),
498           "type" => "Note",
499           "content" => "Hi",
500           "inReplyTo" => nil,
501           "attributedTo" => user.ap_id,
502           "tag" => [
503             %{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"},
504             %{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"}
505           ]
506         },
507         "actor" => user.ap_id
508       }
509
510       {:ok, activity} = Transmogrifier.handle_incoming(message)
511
512       {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
513     end
514   end
515
516   describe "fix_in_reply_to/2" do
517     setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
518
519     setup do
520       data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
521       [data: data]
522     end
523
524     test "returns not modified object when has no inReplyTo field", %{data: data} do
525       assert Transmogrifier.fix_in_reply_to(data) == data
526     end
527
528     test "returns object with inReplyTo when denied incoming reply", %{data: data} do
529       clear_config([:instance, :federation_incoming_replies_max_depth], 0)
530
531       object_with_reply =
532         Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
533
534       modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
535       assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
536
537       object_with_reply =
538         Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
539
540       modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
541       assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
542
543       object_with_reply =
544         Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
545
546       modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
547       assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
548
549       object_with_reply = Map.put(data["object"], "inReplyTo", [])
550       modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
551       assert modified_object["inReplyTo"] == []
552     end
553
554     @tag capture_log: true
555     test "returns modified object when allowed incoming reply", %{data: data} do
556       object_with_reply =
557         Map.put(
558           data["object"],
559           "inReplyTo",
560           "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
561         )
562
563       clear_config([:instance, :federation_incoming_replies_max_depth], 5)
564       modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
565
566       assert modified_object["inReplyTo"] ==
567                "https://mstdn.io/users/mayuutann/statuses/99568293732299394"
568
569       assert modified_object["context"] ==
570                "tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4"
571     end
572   end
573
574   describe "fix_attachments/1" do
575     test "returns not modified object" do
576       data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
577       assert Transmogrifier.fix_attachments(data) == data
578     end
579
580     test "returns modified object when attachment is map" do
581       assert Transmogrifier.fix_attachments(%{
582                "attachment" => %{
583                  "mediaType" => "video/mp4",
584                  "url" => "https://peertube.moe/stat-480.mp4"
585                }
586              }) == %{
587                "attachment" => [
588                  %{
589                    "mediaType" => "video/mp4",
590                    "type" => "Document",
591                    "url" => [
592                      %{
593                        "href" => "https://peertube.moe/stat-480.mp4",
594                        "mediaType" => "video/mp4",
595                        "type" => "Link"
596                      }
597                    ]
598                  }
599                ]
600              }
601     end
602
603     test "returns modified object when attachment is list" do
604       assert Transmogrifier.fix_attachments(%{
605                "attachment" => [
606                  %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
607                  %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
608                ]
609              }) == %{
610                "attachment" => [
611                  %{
612                    "mediaType" => "video/mp4",
613                    "type" => "Document",
614                    "url" => [
615                      %{
616                        "href" => "https://pe.er/stat-480.mp4",
617                        "mediaType" => "video/mp4",
618                        "type" => "Link"
619                      }
620                    ]
621                  },
622                  %{
623                    "mediaType" => "video/mp4",
624                    "type" => "Document",
625                    "url" => [
626                      %{
627                        "href" => "https://pe.er/stat-480.mp4",
628                        "mediaType" => "video/mp4",
629                        "type" => "Link"
630                      }
631                    ]
632                  }
633                ]
634              }
635     end
636   end
637
638   describe "fix_emoji/1" do
639     test "returns not modified object when object not contains tags" do
640       data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
641       assert Transmogrifier.fix_emoji(data) == data
642     end
643
644     test "returns object with emoji when object contains list tags" do
645       assert Transmogrifier.fix_emoji(%{
646                "tag" => [
647                  %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
648                  %{"type" => "Hashtag"}
649                ]
650              }) == %{
651                "emoji" => %{"bib" => "/test"},
652                "tag" => [
653                  %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
654                  %{"type" => "Hashtag"}
655                ]
656              }
657     end
658
659     test "returns object with emoji when object contains map tag" do
660       assert Transmogrifier.fix_emoji(%{
661                "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
662              }) == %{
663                "emoji" => %{"bib" => "/test"},
664                "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
665              }
666     end
667   end
668
669   describe "set_replies/1" do
670     setup do: clear_config([:activitypub, :note_replies_output_limit], 2)
671
672     test "returns unmodified object if activity doesn't have self-replies" do
673       data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
674       assert Transmogrifier.set_replies(data) == data
675     end
676
677     test "sets `replies` collection with a limited number of self-replies" do
678       [user, another_user] = insert_list(2, :user)
679
680       {:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"})
681
682       {:ok, %{id: id2} = self_reply1} =
683         CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: id1})
684
685       {:ok, self_reply2} =
686         CommonAPI.post(user, %{status: "self-reply 2", in_reply_to_status_id: id1})
687
688       # Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2
689       {:ok, _} = CommonAPI.post(user, %{status: "self-reply 3", in_reply_to_status_id: id1})
690
691       {:ok, _} =
692         CommonAPI.post(user, %{
693           status: "self-reply to self-reply",
694           in_reply_to_status_id: id2
695         })
696
697       {:ok, _} =
698         CommonAPI.post(another_user, %{
699           status: "another user's reply",
700           in_reply_to_status_id: id1
701         })
702
703       object = Object.normalize(activity, fetch: false)
704       replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
705
706       assert %{"type" => "Collection", "items" => ^replies_uris} =
707                Transmogrifier.set_replies(object.data)["replies"]
708     end
709   end
710
711   test "take_emoji_tags/1" do
712     user = insert(:user, %{emoji: %{"firefox" => "https://example.org/firefox.png"}})
713
714     assert Transmogrifier.take_emoji_tags(user) == [
715              %{
716                "icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"},
717                "id" => "https://example.org/firefox.png",
718                "name" => ":firefox:",
719                "type" => "Emoji",
720                "updated" => "1970-01-01T00:00:00Z"
721              }
722            ]
723   end
724
725   test "the standalone note uses its own ID when context is missing" do
726     insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
727
728     activity =
729       "test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"
730       |> File.read!()
731       |> Jason.decode!()
732
733     {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity)
734     object = Object.normalize(modified, fetch: false)
735
736     assert object.data["context"] == object.data["id"]
737     assert modified.data["context"] == object.data["id"]
738   end
739
740   @tag capture_log: true
741   test "the reply note uses its parent's ID when context is missing and reply is unreachable" do
742     insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
743
744     activity =
745       "test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"
746       |> File.read!()
747       |> Jason.decode!()
748
749     object =
750       activity["object"]
751       |> Map.put("inReplyTo", "https://404.site/object/went-to-buy-milk")
752
753     activity =
754       activity
755       |> Map.put("object", object)
756
757     {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity)
758     object = Object.normalize(modified, fetch: false)
759
760     assert object.data["context"] == object.data["inReplyTo"]
761     assert modified.data["context"] == object.data["inReplyTo"]
762   end
763 end