Add URL and code to :not_found errors
[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 "it resets instance reachability on successful fetch" do
165 id = "http://mastodon.example.org/@admin/99541947525187367"
166 Instances.set_consistently_unreachable(id)
167 refute Instances.reachable?(id)
168
169 {:ok, _object} =
170 Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
171
172 assert Instances.reachable?(id)
173 end
174 end
175
176 describe "implementation quirks" do
177 test "it can fetch plume articles" do
178 {:ok, object} =
179 Fetcher.fetch_object_from_id(
180 "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
181 )
182
183 assert object
184 end
185
186 test "it can fetch peertube videos" do
187 {:ok, object} =
188 Fetcher.fetch_object_from_id(
189 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
190 )
191
192 assert object
193 end
194
195 test "it can fetch Mobilizon events" do
196 {:ok, object} =
197 Fetcher.fetch_object_from_id(
198 "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
199 )
200
201 assert object
202 end
203
204 test "it can fetch wedistribute articles" do
205 {:ok, object} =
206 Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
207
208 assert object
209 end
210
211 test "all objects with fake directions are rejected by the object fetcher" do
212 assert {:error, _} =
213 Fetcher.fetch_and_contain_remote_object_from_id(
214 "https://info.pleroma.site/activity4.json"
215 )
216 end
217
218 test "handle HTTP 410 Gone response" do
219 assert {:error,
220 {"Object has been deleted", "https://mastodon.example.org/users/userisgone", 410}} ==
221 Fetcher.fetch_and_contain_remote_object_from_id(
222 "https://mastodon.example.org/users/userisgone"
223 )
224 end
225
226 test "handle HTTP 404 response" do
227 assert {:error,
228 {"Object has been deleted", "https://mastodon.example.org/users/userisgone404", 404}} ==
229 Fetcher.fetch_and_contain_remote_object_from_id(
230 "https://mastodon.example.org/users/userisgone404"
231 )
232 end
233
234 test "it can fetch pleroma polls with attachments" do
235 {:ok, object} =
236 Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
237
238 assert object
239 end
240 end
241
242 describe "pruning" do
243 test "it can refetch pruned objects" do
244 object_id = "http://mastodon.example.org/@admin/99541947525187367"
245
246 {:ok, object} = Fetcher.fetch_object_from_id(object_id)
247
248 assert object
249
250 {:ok, _object} = Object.prune(object)
251
252 refute Object.get_by_ap_id(object_id)
253
254 {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
255
256 assert object.data["id"] == object_two.data["id"]
257 assert object.id != object_two.id
258 end
259 end
260
261 describe "signed fetches" do
262 setup do: clear_config([:activitypub, :sign_object_fetches])
263
264 test_with_mock "it signs fetches when configured to do so",
265 Pleroma.Signature,
266 [:passthrough],
267 [] do
268 clear_config([:activitypub, :sign_object_fetches], true)
269
270 Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
271
272 assert called(Pleroma.Signature.sign(:_, :_))
273 end
274
275 test_with_mock "it doesn't sign fetches when not configured to do so",
276 Pleroma.Signature,
277 [:passthrough],
278 [] do
279 clear_config([:activitypub, :sign_object_fetches], false)
280
281 Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
282
283 refute called(Pleroma.Signature.sign(:_, :_))
284 end
285 end
286
287 describe "refetching" do
288 setup do
289 object1 = %{
290 "id" => "https://mastodon.social/1",
291 "actor" => "https://mastodon.social/users/emelie",
292 "attributedTo" => "https://mastodon.social/users/emelie",
293 "type" => "Note",
294 "content" => "test 1",
295 "bcc" => [],
296 "bto" => [],
297 "cc" => [],
298 "to" => [],
299 "summary" => ""
300 }
301
302 object2 = %{
303 "id" => "https://mastodon.social/2",
304 "actor" => "https://mastodon.social/users/emelie",
305 "attributedTo" => "https://mastodon.social/users/emelie",
306 "type" => "Note",
307 "content" => "test 2",
308 "bcc" => [],
309 "bto" => [],
310 "cc" => [],
311 "to" => [],
312 "summary" => "",
313 "formerRepresentations" => %{
314 "type" => "OrderedCollection",
315 "orderedItems" => [
316 %{
317 "type" => "Note",
318 "content" => "orig 2",
319 "actor" => "https://mastodon.social/users/emelie",
320 "attributedTo" => "https://mastodon.social/users/emelie",
321 "bcc" => [],
322 "bto" => [],
323 "cc" => [],
324 "to" => [],
325 "summary" => ""
326 }
327 ],
328 "totalItems" => 1
329 }
330 }
331
332 mock(fn
333 %{
334 method: :get,
335 url: "https://mastodon.social/1"
336 } ->
337 %Tesla.Env{
338 status: 200,
339 headers: [{"content-type", "application/activity+json"}],
340 body: Jason.encode!(object1)
341 }
342
343 %{
344 method: :get,
345 url: "https://mastodon.social/2"
346 } ->
347 %Tesla.Env{
348 status: 200,
349 headers: [{"content-type", "application/activity+json"}],
350 body: Jason.encode!(object2)
351 }
352
353 %{
354 method: :get,
355 url: "https://mastodon.social/users/emelie/collections/featured"
356 } ->
357 %Tesla.Env{
358 status: 200,
359 headers: [{"content-type", "application/activity+json"}],
360 body:
361 Jason.encode!(%{
362 "id" => "https://mastodon.social/users/emelie/collections/featured",
363 "type" => "OrderedCollection",
364 "actor" => "https://mastodon.social/users/emelie",
365 "attributedTo" => "https://mastodon.social/users/emelie",
366 "orderedItems" => [],
367 "totalItems" => 0
368 })
369 }
370
371 env ->
372 apply(HttpRequestMock, :request, [env])
373 end)
374
375 %{object1: object1, object2: object2}
376 end
377
378 test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
379 full_object1 =
380 object1
381 |> Map.merge(%{
382 "formerRepresentations" => %{
383 "type" => "OrderedCollection",
384 "orderedItems" => [
385 %{
386 "type" => "Note",
387 "content" => "orig 2",
388 "actor" => "https://mastodon.social/users/emelie",
389 "attributedTo" => "https://mastodon.social/users/emelie",
390 "bcc" => [],
391 "bto" => [],
392 "cc" => [],
393 "to" => [],
394 "summary" => ""
395 }
396 ],
397 "totalItems" => 1
398 }
399 })
400
401 {:ok, o} = Object.create(full_object1)
402
403 assert {:ok, refetched} = Fetcher.refetch_object(o)
404
405 assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
406 refetched.data
407 end
408
409 test "it uses formerRepresentations from remote if possible", %{object2: object2} do
410 {:ok, o} = Object.create(object2)
411
412 assert {:ok, refetched} = Fetcher.refetch_object(o)
413
414 assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
415 refetched.data
416 end
417
418 test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
419 full_object2 =
420 object2
421 |> Map.merge(%{
422 "content" => "mew mew #def",
423 "formerRepresentations" => %{
424 "type" => "OrderedCollection",
425 "orderedItems" => [
426 %{"type" => "Note", "content" => "mew mew 2"}
427 ],
428 "totalItems" => 1
429 }
430 })
431
432 {:ok, o} = Object.create(full_object2)
433
434 assert {:ok, refetched} = Fetcher.refetch_object(o)
435
436 assert %{
437 "content" => "test 2",
438 "formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
439 } = refetched.data
440 end
441
442 test "it adds to formerRepresentations if the remote does not have one and the object has changed",
443 %{object1: object1} do
444 full_object1 =
445 object1
446 |> Map.merge(%{
447 "content" => "mew mew #def",
448 "formerRepresentations" => %{
449 "type" => "OrderedCollection",
450 "orderedItems" => [
451 %{"type" => "Note", "content" => "mew mew 1"}
452 ],
453 "totalItems" => 1
454 }
455 })
456
457 {:ok, o} = Object.create(full_object1)
458
459 assert {:ok, refetched} = Fetcher.refetch_object(o)
460
461 assert %{
462 "content" => "test 1",
463 "formerRepresentations" => %{
464 "orderedItems" => [
465 %{"content" => "mew mew #def"},
466 %{"content" => "mew mew 1"}
467 ],
468 "totalItems" => 2
469 }
470 } = refetched.data
471 end
472 end
473
474 describe "fetch with history" do
475 setup do
476 object2 = %{
477 "id" => "https://mastodon.social/2",
478 "actor" => "https://mastodon.social/users/emelie",
479 "attributedTo" => "https://mastodon.social/users/emelie",
480 "type" => "Note",
481 "content" => "test 2",
482 "bcc" => [],
483 "bto" => [],
484 "cc" => ["https://mastodon.social/users/emelie/followers"],
485 "to" => [],
486 "summary" => "",
487 "formerRepresentations" => %{
488 "type" => "OrderedCollection",
489 "orderedItems" => [
490 %{
491 "type" => "Note",
492 "content" => "orig 2",
493 "actor" => "https://mastodon.social/users/emelie",
494 "attributedTo" => "https://mastodon.social/users/emelie",
495 "bcc" => [],
496 "bto" => [],
497 "cc" => ["https://mastodon.social/users/emelie/followers"],
498 "to" => [],
499 "summary" => ""
500 }
501 ],
502 "totalItems" => 1
503 }
504 }
505
506 mock(fn
507 %{
508 method: :get,
509 url: "https://mastodon.social/2"
510 } ->
511 %Tesla.Env{
512 status: 200,
513 headers: [{"content-type", "application/activity+json"}],
514 body: Jason.encode!(object2)
515 }
516
517 %{
518 method: :get,
519 url: "https://mastodon.social/users/emelie/collections/featured"
520 } ->
521 %Tesla.Env{
522 status: 200,
523 headers: [{"content-type", "application/activity+json"}],
524 body:
525 Jason.encode!(%{
526 "id" => "https://mastodon.social/users/emelie/collections/featured",
527 "type" => "OrderedCollection",
528 "actor" => "https://mastodon.social/users/emelie",
529 "attributedTo" => "https://mastodon.social/users/emelie",
530 "orderedItems" => [],
531 "totalItems" => 0
532 })
533 }
534
535 env ->
536 apply(HttpRequestMock, :request, [env])
537 end)
538
539 %{object2: object2}
540 end
541
542 test "it gets history", %{object2: object2} do
543 {:ok, object} = Fetcher.fetch_object_from_id(object2["id"])
544
545 assert %{
546 "formerRepresentations" => %{
547 "type" => "OrderedCollection",
548 "orderedItems" => [%{}]
549 }
550 } = object.data
551 end
552 end
553 end