First
[anni] / test / pleroma / object / fetcher_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.Object.FetcherTest do
6   use Pleroma.DataCase
7
8   alias Pleroma.Activity
9   alias Pleroma.Instances
10   alias Pleroma.Object
11   alias Pleroma.Object.Fetcher
12   alias Pleroma.Web.ActivityPub.ObjectValidator
13
14   require Pleroma.Constants
15
16   import Mock
17   import Pleroma.Factory
18   import Tesla.Mock
19
20   setup do
21     mock(fn
22       %{method: :get, url: "https://mastodon.example.org/users/userisgone"} ->
23         %Tesla.Env{status: 410}
24
25       %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} ->
26         %Tesla.Env{status: 404}
27
28       %{
29         method: :get,
30         url:
31           "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
32       } ->
33         %Tesla.Env{
34           status: 200,
35           headers: [{"content-type", "application/json"}],
36           body: File.read!("test/fixtures/spoofed-object.json")
37         }
38
39       env ->
40         apply(HttpRequestMock, :request, [env])
41     end)
42
43     :ok
44   end
45
46   describe "error cases" do
47     setup do
48       mock(fn
49         %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
50           %Tesla.Env{
51             status: 200,
52             body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"),
53             headers: HttpRequestMock.activitypub_object_headers()
54           }
55
56         %{method: :get, url: "https://social.sakamoto.gq/users/eal"} ->
57           %Tesla.Env{
58             status: 200,
59             body: File.read!("test/fixtures/fetch_mocks/eal.json"),
60             headers: HttpRequestMock.activitypub_object_headers()
61           }
62
63         %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} ->
64           %Tesla.Env{
65             status: 200,
66             body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json"),
67             headers: HttpRequestMock.activitypub_object_headers()
68           }
69
70         %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} ->
71           %Tesla.Env{
72             status: 500
73           }
74
75         %{
76           method: :get,
77           url: "https://stereophonic.space/objects/02997b83-3ea7-4b63-94af-ef3aa2d4ed17"
78         } ->
79           %Tesla.Env{
80             status: 500
81           }
82       end)
83
84       :ok
85     end
86
87     @tag capture_log: true
88     test "it works when fetching the OP actor errors out" do
89       # Here we simulate a case where the author of the OP can't be read
90       assert {:ok, _} =
91                Fetcher.fetch_object_from_id(
92                  "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"
93                )
94     end
95   end
96
97   describe "max thread distance restriction" do
98     @ap_id "http://mastodon.example.org/@admin/99541947525187367"
99     setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
100
101     test "it returns thread depth exceeded error if thread depth is exceeded" do
102       clear_config([:instance, :federation_incoming_replies_max_depth], 0)
103
104       assert {:error, "Max thread distance exceeded."} =
105                Fetcher.fetch_object_from_id(@ap_id, depth: 1)
106     end
107
108     test "it fetches object if max thread depth is restricted to 0 and depth is not specified" do
109       clear_config([:instance, :federation_incoming_replies_max_depth], 0)
110
111       assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id)
112     end
113
114     test "it fetches object if requested depth does not exceed max thread depth" do
115       clear_config([:instance, :federation_incoming_replies_max_depth], 10)
116
117       assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10)
118     end
119   end
120
121   describe "actor origin containment" do
122     test "it rejects objects with a bogus origin" do
123       {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity.json")
124     end
125
126     test "it rejects objects when attributedTo is wrong (variant 1)" do
127       {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity2.json")
128     end
129
130     test "it rejects objects when attributedTo is wrong (variant 2)" do
131       {:error, _} = Fetcher.fetch_object_from_id("https://info.pleroma.site/activity3.json")
132     end
133   end
134
135   describe "fetching an object" do
136     test "it fetches an object" do
137       {:ok, object} =
138         Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
139
140       assert _activity = Activity.get_create_by_object_ap_id(object.data["id"])
141
142       {:ok, object_again} =
143         Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
144
145       assert [attachment] = object.data["attachment"]
146       assert is_list(attachment["url"])
147
148       assert object == object_again
149     end
150
151     test "Return MRF reason when fetched status is rejected by one" do
152       clear_config([:mrf_keyword, :reject], ["yeah"])
153       clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.KeywordPolicy])
154
155       assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
156                Fetcher.fetch_object_from_id(
157                  "http://mastodon.example.org/@admin/99541947525187367"
158                )
159     end
160
161     test "it does not fetch a spoofed object uploaded on an instance as an attachment" do
162       assert {:error, _} =
163                Fetcher.fetch_object_from_id(
164                  "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
165                )
166     end
167
168     test "it resets instance reachability on successful fetch" do
169       id = "http://mastodon.example.org/@admin/99541947525187367"
170       Instances.set_consistently_unreachable(id)
171       refute Instances.reachable?(id)
172
173       {:ok, _object} =
174         Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
175
176       assert Instances.reachable?(id)
177     end
178   end
179
180   describe "implementation quirks" do
181     test "it can fetch plume articles" do
182       {:ok, object} =
183         Fetcher.fetch_object_from_id(
184           "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
185         )
186
187       assert object
188     end
189
190     test "it can fetch peertube videos" do
191       {:ok, object} =
192         Fetcher.fetch_object_from_id(
193           "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
194         )
195
196       assert object
197     end
198
199     test "it can fetch Mobilizon events" do
200       {:ok, object} =
201         Fetcher.fetch_object_from_id(
202           "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
203         )
204
205       assert object
206     end
207
208     test "it can fetch wedistribute articles" do
209       {:ok, object} =
210         Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
211
212       assert object
213     end
214
215     test "all objects with fake directions are rejected by the object fetcher" do
216       assert {:error, _} =
217                Fetcher.fetch_and_contain_remote_object_from_id(
218                  "https://info.pleroma.site/activity4.json"
219                )
220     end
221
222     test "handle HTTP 410 Gone response" do
223       assert {:error, "Object has been deleted"} ==
224                Fetcher.fetch_and_contain_remote_object_from_id(
225                  "https://mastodon.example.org/users/userisgone"
226                )
227     end
228
229     test "handle HTTP 404 response" do
230       assert {:error, "Object has been deleted"} ==
231                Fetcher.fetch_and_contain_remote_object_from_id(
232                  "https://mastodon.example.org/users/userisgone404"
233                )
234     end
235
236     test "it can fetch pleroma polls with attachments" do
237       {:ok, object} =
238         Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
239
240       assert object
241     end
242   end
243
244   describe "pruning" do
245     test "it can refetch pruned objects" do
246       object_id = "http://mastodon.example.org/@admin/99541947525187367"
247
248       {:ok, object} = Fetcher.fetch_object_from_id(object_id)
249
250       assert object
251
252       {:ok, _object} = Object.prune(object)
253
254       refute Object.get_by_ap_id(object_id)
255
256       {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
257
258       assert object.data["id"] == object_two.data["id"]
259       assert object.id != object_two.id
260     end
261   end
262
263   describe "signed fetches" do
264     setup do: clear_config([:activitypub, :sign_object_fetches])
265
266     test_with_mock "it signs fetches when configured to do so",
267                    Pleroma.Signature,
268                    [:passthrough],
269                    [] do
270       clear_config([:activitypub, :sign_object_fetches], true)
271
272       Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
273
274       assert called(Pleroma.Signature.sign(:_, :_))
275     end
276
277     test_with_mock "it doesn't sign fetches when not configured to do so",
278                    Pleroma.Signature,
279                    [:passthrough],
280                    [] do
281       clear_config([:activitypub, :sign_object_fetches], false)
282
283       Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
284
285       refute called(Pleroma.Signature.sign(:_, :_))
286     end
287   end
288
289   describe "refetching" do
290     setup do
291       insert(:user, ap_id: "https://mastodon.social/users/emelie")
292
293       object1 = %{
294         "id" => "https://mastodon.social/1",
295         "actor" => "https://mastodon.social/users/emelie",
296         "attributedTo" => "https://mastodon.social/users/emelie",
297         "type" => "Note",
298         "content" => "test 1",
299         "bcc" => [],
300         "bto" => [],
301         "cc" => [],
302         "to" => [Pleroma.Constants.as_public()],
303         "summary" => "",
304         "published" => "2023-05-08 23:43:20Z",
305         "updated" => "2023-05-09 23:43:20Z"
306       }
307
308       {:ok, local_object1, _} = ObjectValidator.validate(object1, [])
309
310       object2 = %{
311         "id" => "https://mastodon.social/2",
312         "actor" => "https://mastodon.social/users/emelie",
313         "attributedTo" => "https://mastodon.social/users/emelie",
314         "type" => "Note",
315         "content" => "test 2",
316         "bcc" => [],
317         "bto" => [],
318         "cc" => [],
319         "to" => [Pleroma.Constants.as_public()],
320         "summary" => "",
321         "published" => "2023-05-08 23:43:20Z",
322         "updated" => "2023-05-09 23:43:25Z",
323         "formerRepresentations" => %{
324           "type" => "OrderedCollection",
325           "orderedItems" => [
326             %{
327               "type" => "Note",
328               "content" => "orig 2",
329               "actor" => "https://mastodon.social/users/emelie",
330               "attributedTo" => "https://mastodon.social/users/emelie",
331               "bcc" => [],
332               "bto" => [],
333               "cc" => [],
334               "to" => [Pleroma.Constants.as_public()],
335               "summary" => "",
336               "published" => "2023-05-08 23:43:20Z",
337               "updated" => "2023-05-09 23:43:21Z"
338             }
339           ],
340           "totalItems" => 1
341         }
342       }
343
344       {:ok, local_object2, _} = ObjectValidator.validate(object2, [])
345
346       mock(fn
347         %{
348           method: :get,
349           url: "https://mastodon.social/1"
350         } ->
351           %Tesla.Env{
352             status: 200,
353             headers: [{"content-type", "application/activity+json"}],
354             body: Jason.encode!(object1 |> Map.put("updated", "2023-05-09 23:44:20Z"))
355           }
356
357         %{
358           method: :get,
359           url: "https://mastodon.social/2"
360         } ->
361           %Tesla.Env{
362             status: 200,
363             headers: [{"content-type", "application/activity+json"}],
364             body: Jason.encode!(object2 |> Map.put("updated", "2023-05-09 23:44:20Z"))
365           }
366
367         %{
368           method: :get,
369           url: "https://mastodon.social/users/emelie/collections/featured"
370         } ->
371           %Tesla.Env{
372             status: 200,
373             headers: [{"content-type", "application/activity+json"}],
374             body:
375               Jason.encode!(%{
376                 "id" => "https://mastodon.social/users/emelie/collections/featured",
377                 "type" => "OrderedCollection",
378                 "actor" => "https://mastodon.social/users/emelie",
379                 "attributedTo" => "https://mastodon.social/users/emelie",
380                 "orderedItems" => [],
381                 "totalItems" => 0
382               })
383           }
384
385         env ->
386           apply(HttpRequestMock, :request, [env])
387       end)
388
389       %{object1: local_object1, object2: local_object2}
390     end
391
392     test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
393       full_object1 =
394         object1
395         |> Map.merge(%{
396           "formerRepresentations" => %{
397             "type" => "OrderedCollection",
398             "orderedItems" => [
399               %{
400                 "type" => "Note",
401                 "content" => "orig 2",
402                 "actor" => "https://mastodon.social/users/emelie",
403                 "attributedTo" => "https://mastodon.social/users/emelie",
404                 "bcc" => [],
405                 "bto" => [],
406                 "cc" => [],
407                 "to" => [Pleroma.Constants.as_public()],
408                 "summary" => "",
409                 "published" => "2023-05-08 23:43:20Z"
410               }
411             ],
412             "totalItems" => 1
413           }
414         })
415
416       {:ok, o} = Object.create(full_object1)
417
418       assert {:ok, refetched} = Fetcher.refetch_object(o)
419
420       assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
421                refetched.data
422     end
423
424     test "it uses formerRepresentations from remote if possible", %{object2: object2} do
425       {:ok, o} = Object.create(object2)
426
427       assert {:ok, refetched} = Fetcher.refetch_object(o)
428
429       assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
430                refetched.data
431     end
432
433     test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
434       full_object2 =
435         object2
436         |> Map.merge(%{
437           "content" => "mew mew #def",
438           "formerRepresentations" => %{
439             "type" => "OrderedCollection",
440             "orderedItems" => [
441               %{"type" => "Note", "content" => "mew mew 2"}
442             ],
443             "totalItems" => 1
444           }
445         })
446
447       {:ok, o} = Object.create(full_object2)
448
449       assert {:ok, refetched} = Fetcher.refetch_object(o)
450
451       assert %{
452                "content" => "test 2",
453                "formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
454              } = refetched.data
455     end
456
457     test "it adds to formerRepresentations if the remote does not have one and the object has changed",
458          %{object1: object1} do
459       full_object1 =
460         object1
461         |> Map.merge(%{
462           "content" => "mew mew #def",
463           "formerRepresentations" => %{
464             "type" => "OrderedCollection",
465             "orderedItems" => [
466               %{"type" => "Note", "content" => "mew mew 1"}
467             ],
468             "totalItems" => 1
469           }
470         })
471
472       {:ok, o} = Object.create(full_object1)
473
474       assert {:ok, refetched} = Fetcher.refetch_object(o)
475
476       assert %{
477                "content" => "test 1",
478                "formerRepresentations" => %{
479                  "orderedItems" => [
480                    %{"content" => "mew mew #def"},
481                    %{"content" => "mew mew 1"}
482                  ],
483                  "totalItems" => 2
484                }
485              } = refetched.data
486     end
487
488     test "it keeps the history intact if only updated time has changed",
489          %{object1: object1} do
490       full_object1 =
491         object1
492         |> Map.merge(%{
493           "updated" => "2023-05-08 23:43:47Z",
494           "formerRepresentations" => %{
495             "type" => "OrderedCollection",
496             "orderedItems" => [
497               %{"type" => "Note", "content" => "mew mew 1"}
498             ],
499             "totalItems" => 1
500           }
501         })
502
503       {:ok, o} = Object.create(full_object1)
504
505       assert {:ok, refetched} = Fetcher.refetch_object(o)
506
507       assert %{
508                "content" => "test 1",
509                "formerRepresentations" => %{
510                  "orderedItems" => [
511                    %{"content" => "mew mew 1"}
512                  ],
513                  "totalItems" => 1
514                }
515              } = refetched.data
516     end
517
518     test "it goes through ObjectValidator and MRF", %{object2: object2} do
519       with_mock Pleroma.Web.ActivityPub.MRF, [:passthrough],
520         filter: fn
521           %{"type" => "Note"} = object ->
522             {:ok, Map.put(object, "content", "MRFd content")}
523
524           arg ->
525             passthrough([arg])
526         end do
527         {:ok, o} = Object.create(object2)
528
529         assert {:ok, refetched} = Fetcher.refetch_object(o)
530
531         assert %{"content" => "MRFd content"} = refetched.data
532       end
533     end
534   end
535
536   describe "fetch with history" do
537     setup do
538       object2 = %{
539         "id" => "https://mastodon.social/2",
540         "actor" => "https://mastodon.social/users/emelie",
541         "attributedTo" => "https://mastodon.social/users/emelie",
542         "type" => "Note",
543         "content" => "test 2",
544         "bcc" => [],
545         "bto" => [],
546         "cc" => ["https://mastodon.social/users/emelie/followers"],
547         "to" => [],
548         "summary" => "",
549         "formerRepresentations" => %{
550           "type" => "OrderedCollection",
551           "orderedItems" => [
552             %{
553               "type" => "Note",
554               "content" => "orig 2",
555               "actor" => "https://mastodon.social/users/emelie",
556               "attributedTo" => "https://mastodon.social/users/emelie",
557               "bcc" => [],
558               "bto" => [],
559               "cc" => ["https://mastodon.social/users/emelie/followers"],
560               "to" => [],
561               "summary" => ""
562             }
563           ],
564           "totalItems" => 1
565         }
566       }
567
568       mock(fn
569         %{
570           method: :get,
571           url: "https://mastodon.social/2"
572         } ->
573           %Tesla.Env{
574             status: 200,
575             headers: [{"content-type", "application/activity+json"}],
576             body: Jason.encode!(object2)
577           }
578
579         %{
580           method: :get,
581           url: "https://mastodon.social/users/emelie/collections/featured"
582         } ->
583           %Tesla.Env{
584             status: 200,
585             headers: [{"content-type", "application/activity+json"}],
586             body:
587               Jason.encode!(%{
588                 "id" => "https://mastodon.social/users/emelie/collections/featured",
589                 "type" => "OrderedCollection",
590                 "actor" => "https://mastodon.social/users/emelie",
591                 "attributedTo" => "https://mastodon.social/users/emelie",
592                 "orderedItems" => [],
593                 "totalItems" => 0
594               })
595           }
596
597         env ->
598           apply(HttpRequestMock, :request, [env])
599       end)
600
601       %{object2: object2}
602     end
603
604     test "it gets history", %{object2: object2} do
605       {:ok, object} = Fetcher.fetch_object_from_id(object2["id"])
606
607       assert %{
608                "formerRepresentations" => %{
609                  "type" => "OrderedCollection",
610                  "orderedItems" => [%{}]
611                }
612              } = object.data
613     end
614   end
615 end