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