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