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