logo

pleroma

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

fetcher_test.exs (19434B)


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