logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://anongit.hacktivis.me/git/pleroma.git/

fetcher_test.exs (19553B)


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