1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Object.FetcherTest do
9 alias Pleroma.Instances
11 alias Pleroma.Object.Fetcher
12 alias Pleroma.Web.ActivityPub.ObjectValidator
14 require Pleroma.Constants
17 import Pleroma.Factory
22 %{method: :get, url: "https://mastodon.example.org/users/userisgone"} ->
23 %Tesla.Env{status: 410}
25 %{method: :get, url: "https://mastodon.example.org/users/userisgone404"} ->
26 %Tesla.Env{status: 404}
31 "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
35 headers: [{"content-type", "application/json"}],
36 body: File.read!("test/fixtures/spoofed-object.json")
40 apply(HttpRequestMock, :request, [env])
46 describe "error cases" do
49 %{method: :get, url: "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"} ->
52 body: File.read!("test/fixtures/fetch_mocks/9wTkLEnuq47B25EehM.json"),
53 headers: HttpRequestMock.activitypub_object_headers()
56 %{method: :get, url: "https://social.sakamoto.gq/users/eal"} ->
59 body: File.read!("test/fixtures/fetch_mocks/eal.json"),
60 headers: HttpRequestMock.activitypub_object_headers()
63 %{method: :get, url: "https://busshi.moe/users/tuxcrafting/statuses/104410921027210069"} ->
66 body: File.read!("test/fixtures/fetch_mocks/104410921027210069.json"),
67 headers: HttpRequestMock.activitypub_object_headers()
70 %{method: :get, url: "https://busshi.moe/users/tuxcrafting"} ->
77 url: "https://stereophonic.space/objects/02997b83-3ea7-4b63-94af-ef3aa2d4ed17"
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
91 Fetcher.fetch_object_from_id(
92 "https://social.sakamoto.gq/notice/9wTkLEnuq47B25EehM"
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])
101 test "it returns thread depth exceeded error if thread depth is exceeded" do
102 clear_config([:instance, :federation_incoming_replies_max_depth], 0)
104 assert {:error, :allowed_depth} = Fetcher.fetch_object_from_id(@ap_id, depth: 1)
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)
110 assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id)
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)
116 assert {:ok, _} = Fetcher.fetch_object_from_id(@ap_id, depth: 10)
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")
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")
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")
134 describe "fetching an object" do
135 test "it fetches an object" do
137 Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
139 assert _activity = Activity.get_create_by_object_ap_id(object.data["id"])
141 {:ok, object_again} =
142 Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
144 assert [attachment] = object.data["attachment"]
145 assert is_list(attachment["url"])
147 assert object == object_again
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])
154 assert {:reject, "[KeywordPolicy] Matches with rejected keyword"} ==
155 Fetcher.fetch_object_from_id(
156 "http://mastodon.example.org/@admin/99541947525187367"
160 test "it does not fetch a spoofed object uploaded on an instance as an attachment" do
162 Fetcher.fetch_object_from_id(
163 "https://patch.cx/media/03ca3c8b4ac3ddd08bf0f84be7885f2f88de0f709112131a22d83650819e36c2.json"
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)
173 Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
175 assert Instances.reachable?(id)
179 describe "implementation quirks" do
180 test "it can fetch plume articles" do
182 Fetcher.fetch_object_from_id(
183 "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
189 test "it can fetch peertube videos" do
191 Fetcher.fetch_object_from_id(
192 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
198 test "it can fetch Mobilizon events" do
200 Fetcher.fetch_object_from_id(
201 "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
207 test "it can fetch wedistribute articles" do
209 Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
214 test "all objects with fake directions are rejected by the object fetcher" do
216 Fetcher.fetch_and_contain_remote_object_from_id(
217 "https://info.pleroma.site/activity4.json"
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"
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"
235 test "it can fetch pleroma polls with attachments" do
237 Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
243 describe "pruning" do
244 test "it can refetch pruned objects" do
245 object_id = "http://mastodon.example.org/@admin/99541947525187367"
247 {:ok, object} = Fetcher.fetch_object_from_id(object_id)
251 {:ok, _object} = Object.prune(object)
253 refute Object.get_by_ap_id(object_id)
255 {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
257 assert object.data["id"] == object_two.data["id"]
258 assert object.id != object_two.id
262 describe "signed fetches" do
263 setup do: clear_config([:activitypub, :sign_object_fetches])
265 test_with_mock "it signs fetches when configured to do so",
269 clear_config([:activitypub, :sign_object_fetches], true)
271 Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
273 assert called(Pleroma.Signature.sign(:_, :_))
276 test_with_mock "it doesn't sign fetches when not configured to do so",
280 clear_config([:activitypub, :sign_object_fetches], false)
282 Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
284 refute called(Pleroma.Signature.sign(:_, :_))
288 describe "refetching" do
290 insert(:user, ap_id: "https://mastodon.social/users/emelie")
293 "id" => "https://mastodon.social/1",
294 "actor" => "https://mastodon.social/users/emelie",
295 "attributedTo" => "https://mastodon.social/users/emelie",
297 "content" => "test 1",
301 "to" => [Pleroma.Constants.as_public()],
303 "published" => "2023-05-08 23:43:20Z",
304 "updated" => "2023-05-09 23:43:20Z"
307 {:ok, local_object1, _} = ObjectValidator.validate(object1, [])
310 "id" => "https://mastodon.social/2",
311 "actor" => "https://mastodon.social/users/emelie",
312 "attributedTo" => "https://mastodon.social/users/emelie",
314 "content" => "test 2",
318 "to" => [Pleroma.Constants.as_public()],
320 "published" => "2023-05-08 23:43:20Z",
321 "updated" => "2023-05-09 23:43:25Z",
322 "formerRepresentations" => %{
323 "type" => "OrderedCollection",
327 "content" => "orig 2",
328 "actor" => "https://mastodon.social/users/emelie",
329 "attributedTo" => "https://mastodon.social/users/emelie",
333 "to" => [Pleroma.Constants.as_public()],
335 "published" => "2023-05-08 23:43:20Z",
336 "updated" => "2023-05-09 23:43:21Z"
343 {:ok, local_object2, _} = ObjectValidator.validate(object2, [])
348 url: "https://mastodon.social/1"
352 headers: [{"content-type", "application/activity+json"}],
353 body: Jason.encode!(object1 |> Map.put("updated", "2023-05-09 23:44:20Z"))
358 url: "https://mastodon.social/2"
362 headers: [{"content-type", "application/activity+json"}],
363 body: Jason.encode!(object2 |> Map.put("updated", "2023-05-09 23:44:20Z"))
368 url: "https://mastodon.social/users/emelie/collections/featured"
372 headers: [{"content-type", "application/activity+json"}],
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" => [],
385 apply(HttpRequestMock, :request, [env])
388 %{object1: local_object1, object2: local_object2}
391 test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
395 "formerRepresentations" => %{
396 "type" => "OrderedCollection",
400 "content" => "orig 2",
401 "actor" => "https://mastodon.social/users/emelie",
402 "attributedTo" => "https://mastodon.social/users/emelie",
406 "to" => [Pleroma.Constants.as_public()],
408 "published" => "2023-05-08 23:43:20Z"
415 {:ok, o} = Object.create(full_object1)
417 assert {:ok, refetched} = Fetcher.refetch_object(o)
419 assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
423 test "it uses formerRepresentations from remote if possible", %{object2: object2} do
424 {:ok, o} = Object.create(object2)
426 assert {:ok, refetched} = Fetcher.refetch_object(o)
428 assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
432 test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
436 "content" => "mew mew #def",
437 "formerRepresentations" => %{
438 "type" => "OrderedCollection",
440 %{"type" => "Note", "content" => "mew mew 2"}
446 {:ok, o} = Object.create(full_object2)
448 assert {:ok, refetched} = Fetcher.refetch_object(o)
451 "content" => "test 2",
452 "formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
456 test "it adds to formerRepresentations if the remote does not have one and the object has changed",
457 %{object1: object1} do
461 "content" => "mew mew #def",
462 "formerRepresentations" => %{
463 "type" => "OrderedCollection",
465 %{"type" => "Note", "content" => "mew mew 1"}
471 {:ok, o} = Object.create(full_object1)
473 assert {:ok, refetched} = Fetcher.refetch_object(o)
476 "content" => "test 1",
477 "formerRepresentations" => %{
479 %{"content" => "mew mew #def"},
480 %{"content" => "mew mew 1"}
487 test "it keeps the history intact if only updated time has changed",
488 %{object1: object1} do
492 "updated" => "2023-05-08 23:43:47Z",
493 "formerRepresentations" => %{
494 "type" => "OrderedCollection",
496 %{"type" => "Note", "content" => "mew mew 1"}
502 {:ok, o} = Object.create(full_object1)
504 assert {:ok, refetched} = Fetcher.refetch_object(o)
507 "content" => "test 1",
508 "formerRepresentations" => %{
510 %{"content" => "mew mew 1"}
517 test "it goes through ObjectValidator and MRF", %{object2: object2} do
518 with_mock Pleroma.Web.ActivityPub.MRF, [:passthrough],
520 %{"type" => "Note"} = object ->
521 {:ok, Map.put(object, "content", "MRFd content")}
526 {:ok, o} = Object.create(object2)
528 assert {:ok, refetched} = Fetcher.refetch_object(o)
530 assert %{"content" => "MRFd content"} = refetched.data
535 describe "fetch with history" do
538 "id" => "https://mastodon.social/2",
539 "actor" => "https://mastodon.social/users/emelie",
540 "attributedTo" => "https://mastodon.social/users/emelie",
542 "content" => "test 2",
545 "cc" => ["https://mastodon.social/users/emelie/followers"],
548 "formerRepresentations" => %{
549 "type" => "OrderedCollection",
553 "content" => "orig 2",
554 "actor" => "https://mastodon.social/users/emelie",
555 "attributedTo" => "https://mastodon.social/users/emelie",
558 "cc" => ["https://mastodon.social/users/emelie/followers"],
570 url: "https://mastodon.social/2"
574 headers: [{"content-type", "application/activity+json"}],
575 body: Jason.encode!(object2)
580 url: "https://mastodon.social/users/emelie/collections/featured"
584 headers: [{"content-type", "application/activity+json"}],
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" => [],
597 apply(HttpRequestMock, :request, [env])
603 test "it gets history", %{object2: object2} do
604 {:ok, object} = Fetcher.fetch_object_from_id(object2["id"])
607 "formerRepresentations" => %{
608 "type" => "OrderedCollection",
609 "orderedItems" => [%{}]