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