logo

pleroma

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

fetcher_test.exs (27023B)


  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 does not fetch from local instance" do
  135. local_url = Pleroma.Web.Endpoint.url() <> "/objects/local_resource"
  136. assert {:fetch, {:error, "Trying to fetch local resource"}} =
  137. Fetcher.fetch_object_from_id(local_url)
  138. end
  139. test "it validates content-type headers according to ActivityPub spec" do
  140. # Setup a mock for an object with invalid content-type
  141. mock(fn
  142. %{method: :get, url: "https://example.com/objects/invalid-content-type"} ->
  143. %Tesla.Env{
  144. status: 200,
  145. # Not a valid AP content-type
  146. headers: [{"content-type", "application/json"}],
  147. body:
  148. Jason.encode!(%{
  149. "id" => "https://example.com/objects/invalid-content-type",
  150. "type" => "Note",
  151. "content" => "This has an invalid content type",
  152. "actor" => "https://example.com/users/actor",
  153. "attributedTo" => "https://example.com/users/actor"
  154. })
  155. }
  156. end)
  157. assert {:fetch, {:error, {:content_type, "application/json"}}} =
  158. Fetcher.fetch_object_from_id("https://example.com/objects/invalid-content-type")
  159. end
  160. test "it accepts objects with application/ld+json and ActivityStreams profile" do
  161. # Setup a mock for an object with ld+json content-type and AS profile
  162. mock(fn
  163. %{method: :get, url: "https://example.com/objects/valid-ld-json"} ->
  164. %Tesla.Env{
  165. status: 200,
  166. headers: [
  167. {"content-type",
  168. "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""}
  169. ],
  170. body:
  171. Jason.encode!(%{
  172. "id" => "https://example.com/objects/valid-ld-json",
  173. "type" => "Note",
  174. "content" => "This has a valid ld+json content type",
  175. "actor" => "https://example.com/users/actor",
  176. "attributedTo" => "https://example.com/users/actor"
  177. })
  178. }
  179. end)
  180. # This should pass if content-type validation works correctly
  181. assert {:ok, object} =
  182. Fetcher.fetch_and_contain_remote_object_from_id(
  183. "https://example.com/objects/valid-ld-json"
  184. )
  185. assert object["content"] == "This has a valid ld+json content type"
  186. end
  187. test "it rejects objects with no content-type header" do
  188. # Setup a mock for an object with no content-type header
  189. mock(fn
  190. %{method: :get, url: "https://example.com/objects/no-content-type"} ->
  191. %Tesla.Env{
  192. status: 200,
  193. # No content-type header
  194. headers: [],
  195. body:
  196. Jason.encode!(%{
  197. "id" => "https://example.com/objects/no-content-type",
  198. "type" => "Note",
  199. "content" => "This has no content type header",
  200. "actor" => "https://example.com/users/actor",
  201. "attributedTo" => "https://example.com/users/actor"
  202. })
  203. }
  204. end)
  205. # We want to test that the request fails with a missing content-type error
  206. # but the actual error is {:fetch, {:error, nil}} - we'll check for this format
  207. result = Fetcher.fetch_object_from_id("https://example.com/objects/no-content-type")
  208. assert {:fetch, {:error, nil}} = result
  209. end
  210. test "it resets instance reachability on successful fetch" do
  211. id = "http://mastodon.example.org/@admin/99541947525187367"
  212. Instances.set_consistently_unreachable(id)
  213. refute Instances.reachable?(id)
  214. {:ok, _object} =
  215. Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
  216. assert Instances.reachable?(id)
  217. end
  218. end
  219. describe "implementation quirks" do
  220. test "it can fetch plume articles" do
  221. {:ok, object} =
  222. Fetcher.fetch_object_from_id(
  223. "https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june-2018/"
  224. )
  225. assert object
  226. end
  227. test "it can fetch peertube videos" do
  228. {:ok, object} =
  229. Fetcher.fetch_object_from_id(
  230. "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
  231. )
  232. assert object
  233. end
  234. test "it can fetch Mobilizon events" do
  235. {:ok, object} =
  236. Fetcher.fetch_object_from_id(
  237. "https://mobilizon.org/events/252d5816-00a3-4a89-a66f-15bf65c33e39"
  238. )
  239. assert object
  240. end
  241. test "it can fetch Honk events" do
  242. {:ok, object} =
  243. Fetcher.fetch_object_from_id("https://honk.tedunangst.com/u/tedu/h/8dkPX284T8286Mm9HD")
  244. assert object
  245. end
  246. test "it can fetch wedistribute articles" do
  247. {:ok, object} =
  248. Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810")
  249. assert object
  250. end
  251. test "all objects with fake directions are rejected by the object fetcher" do
  252. assert {:error, _} =
  253. Fetcher.fetch_and_contain_remote_object_from_id(
  254. "https://info.pleroma.site/activity4.json"
  255. )
  256. end
  257. test "handle HTTP 410 Gone response" do
  258. assert {:error, :not_found} ==
  259. Fetcher.fetch_and_contain_remote_object_from_id(
  260. "https://mastodon.example.org/users/userisgone"
  261. )
  262. end
  263. test "handle HTTP 404 response" do
  264. assert {:error, :not_found} ==
  265. Fetcher.fetch_and_contain_remote_object_from_id(
  266. "https://mastodon.example.org/users/userisgone404"
  267. )
  268. end
  269. test "it can fetch pleroma polls with attachments" do
  270. {:ok, object} =
  271. Fetcher.fetch_object_from_id("https://patch.cx/objects/tesla_mock/poll_attachment")
  272. assert object
  273. end
  274. end
  275. describe "pruning" do
  276. test "it can refetch pruned objects" do
  277. object_id = "http://mastodon.example.org/@admin/99541947525187367"
  278. {:ok, object} = Fetcher.fetch_object_from_id(object_id)
  279. assert object
  280. {:ok, _object} = Object.prune(object)
  281. refute Object.get_by_ap_id(object_id)
  282. {:ok, %Object{} = object_two} = Fetcher.fetch_object_from_id(object_id)
  283. assert object.data["id"] == object_two.data["id"]
  284. assert object.id != object_two.id
  285. end
  286. end
  287. describe "signed fetches" do
  288. setup do: clear_config([:activitypub, :sign_object_fetches])
  289. test_with_mock "it signs fetches when configured to do so",
  290. Pleroma.Signature,
  291. [:passthrough],
  292. [] do
  293. clear_config([:activitypub, :sign_object_fetches], true)
  294. Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
  295. assert called(Pleroma.Signature.sign(:_, :_))
  296. end
  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. Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
  303. refute called(Pleroma.Signature.sign(:_, :_))
  304. end
  305. end
  306. describe "refetching" do
  307. setup do
  308. insert(:user, ap_id: "https://mastodon.social/users/emelie")
  309. object1 = %{
  310. "id" => "https://mastodon.social/1",
  311. "actor" => "https://mastodon.social/users/emelie",
  312. "attributedTo" => "https://mastodon.social/users/emelie",
  313. "type" => "Note",
  314. "content" => "test 1",
  315. "bcc" => [],
  316. "bto" => [],
  317. "cc" => [],
  318. "to" => [Pleroma.Constants.as_public()],
  319. "summary" => "",
  320. "published" => "2023-05-08 23:43:20Z",
  321. "updated" => "2023-05-09 23:43:20Z"
  322. }
  323. {:ok, local_object1, _} = ObjectValidator.validate(object1, [])
  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" => [Pleroma.Constants.as_public()],
  334. "summary" => "",
  335. "published" => "2023-05-08 23:43:20Z",
  336. "updated" => "2023-05-09 23:43:25Z",
  337. "formerRepresentations" => %{
  338. "type" => "OrderedCollection",
  339. "orderedItems" => [
  340. %{
  341. "type" => "Note",
  342. "content" => "orig 2",
  343. "actor" => "https://mastodon.social/users/emelie",
  344. "attributedTo" => "https://mastodon.social/users/emelie",
  345. "bcc" => [],
  346. "bto" => [],
  347. "cc" => [],
  348. "to" => [Pleroma.Constants.as_public()],
  349. "summary" => "",
  350. "published" => "2023-05-08 23:43:20Z",
  351. "updated" => "2023-05-09 23:43:21Z"
  352. }
  353. ],
  354. "totalItems" => 1
  355. }
  356. }
  357. {:ok, local_object2, _} = ObjectValidator.validate(object2, [])
  358. mock(fn
  359. %{
  360. method: :get,
  361. url: "https://mastodon.social/1"
  362. } ->
  363. %Tesla.Env{
  364. status: 200,
  365. headers: [{"content-type", "application/activity+json"}],
  366. body: Jason.encode!(object1 |> Map.put("updated", "2023-05-09 23:44:20Z"))
  367. }
  368. %{
  369. method: :get,
  370. url: "https://mastodon.social/2"
  371. } ->
  372. %Tesla.Env{
  373. status: 200,
  374. headers: [{"content-type", "application/activity+json"}],
  375. body: Jason.encode!(object2 |> Map.put("updated", "2023-05-09 23:44:20Z"))
  376. }
  377. %{
  378. method: :get,
  379. url: "https://mastodon.social/users/emelie/collections/featured"
  380. } ->
  381. %Tesla.Env{
  382. status: 200,
  383. headers: [{"content-type", "application/activity+json"}],
  384. body:
  385. Jason.encode!(%{
  386. "id" => "https://mastodon.social/users/emelie/collections/featured",
  387. "type" => "OrderedCollection",
  388. "actor" => "https://mastodon.social/users/emelie",
  389. "attributedTo" => "https://mastodon.social/users/emelie",
  390. "orderedItems" => [],
  391. "totalItems" => 0
  392. })
  393. }
  394. env ->
  395. apply(HttpRequestMock, :request, [env])
  396. end)
  397. %{object1: local_object1, object2: local_object2}
  398. end
  399. test "it keeps formerRepresentations if remote does not have this attr", %{object1: object1} do
  400. full_object1 =
  401. object1
  402. |> Map.merge(%{
  403. "formerRepresentations" => %{
  404. "type" => "OrderedCollection",
  405. "orderedItems" => [
  406. %{
  407. "type" => "Note",
  408. "content" => "orig 2",
  409. "actor" => "https://mastodon.social/users/emelie",
  410. "attributedTo" => "https://mastodon.social/users/emelie",
  411. "bcc" => [],
  412. "bto" => [],
  413. "cc" => [],
  414. "to" => [Pleroma.Constants.as_public()],
  415. "summary" => "",
  416. "published" => "2023-05-08 23:43:20Z"
  417. }
  418. ],
  419. "totalItems" => 1
  420. }
  421. })
  422. {:ok, o} = Object.create(full_object1)
  423. assert {:ok, refetched} = Fetcher.refetch_object(o)
  424. assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
  425. refetched.data
  426. end
  427. test "it uses formerRepresentations from remote if possible", %{object2: object2} do
  428. {:ok, o} = Object.create(object2)
  429. assert {:ok, refetched} = Fetcher.refetch_object(o)
  430. assert %{"formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}} =
  431. refetched.data
  432. end
  433. test "it replaces formerRepresentations with the one from remote", %{object2: object2} do
  434. full_object2 =
  435. object2
  436. |> Map.merge(%{
  437. "content" => "mew mew #def",
  438. "formerRepresentations" => %{
  439. "type" => "OrderedCollection",
  440. "orderedItems" => [
  441. %{"type" => "Note", "content" => "mew mew 2"}
  442. ],
  443. "totalItems" => 1
  444. }
  445. })
  446. {:ok, o} = Object.create(full_object2)
  447. assert {:ok, refetched} = Fetcher.refetch_object(o)
  448. assert %{
  449. "content" => "test 2",
  450. "formerRepresentations" => %{"orderedItems" => [%{"content" => "orig 2"}]}
  451. } = refetched.data
  452. end
  453. test "it adds to formerRepresentations if the remote does not have one and the object has changed",
  454. %{object1: object1} do
  455. full_object1 =
  456. object1
  457. |> Map.merge(%{
  458. "content" => "mew mew #def",
  459. "formerRepresentations" => %{
  460. "type" => "OrderedCollection",
  461. "orderedItems" => [
  462. %{"type" => "Note", "content" => "mew mew 1"}
  463. ],
  464. "totalItems" => 1
  465. }
  466. })
  467. {:ok, o} = Object.create(full_object1)
  468. assert {:ok, refetched} = Fetcher.refetch_object(o)
  469. assert %{
  470. "content" => "test 1",
  471. "formerRepresentations" => %{
  472. "orderedItems" => [
  473. %{"content" => "mew mew #def"},
  474. %{"content" => "mew mew 1"}
  475. ],
  476. "totalItems" => 2
  477. }
  478. } = refetched.data
  479. end
  480. test "it keeps the history intact if only updated time has changed",
  481. %{object1: object1} do
  482. full_object1 =
  483. object1
  484. |> Map.merge(%{
  485. "updated" => "2023-05-08 23:43:47Z",
  486. "formerRepresentations" => %{
  487. "type" => "OrderedCollection",
  488. "orderedItems" => [
  489. %{"type" => "Note", "content" => "mew mew 1"}
  490. ],
  491. "totalItems" => 1
  492. }
  493. })
  494. {:ok, o} = Object.create(full_object1)
  495. assert {:ok, refetched} = Fetcher.refetch_object(o)
  496. assert %{
  497. "content" => "test 1",
  498. "formerRepresentations" => %{
  499. "orderedItems" => [
  500. %{"content" => "mew mew 1"}
  501. ],
  502. "totalItems" => 1
  503. }
  504. } = refetched.data
  505. end
  506. test "it goes through ObjectValidator and MRF", %{object2: object2} do
  507. with_mock Pleroma.Web.ActivityPub.MRF, [:passthrough],
  508. filter: fn
  509. %{"type" => "Note"} = object ->
  510. {:ok, Map.put(object, "content", "MRFd content")}
  511. arg ->
  512. passthrough([arg])
  513. end do
  514. {:ok, o} = Object.create(object2)
  515. assert {:ok, refetched} = Fetcher.refetch_object(o)
  516. assert %{"content" => "MRFd content"} = refetched.data
  517. end
  518. end
  519. end
  520. describe "cross-domain redirect handling" do
  521. setup do
  522. mock(fn
  523. # Cross-domain redirect with original domain in id
  524. %{method: :get, url: "https://original.test/objects/123"} ->
  525. %Tesla.Env{
  526. status: 200,
  527. url: "https://media.test/objects/123",
  528. headers: [{"content-type", "application/activity+json"}],
  529. body:
  530. Jason.encode!(%{
  531. "id" => "https://original.test/objects/123",
  532. "type" => "Note",
  533. "content" => "This is redirected content",
  534. "actor" => "https://original.test/users/actor",
  535. "attributedTo" => "https://original.test/users/actor"
  536. })
  537. }
  538. # Cross-domain redirect with final domain in id
  539. %{method: :get, url: "https://original.test/objects/final-domain-id"} ->
  540. %Tesla.Env{
  541. status: 200,
  542. url: "https://media.test/objects/final-domain-id",
  543. headers: [{"content-type", "application/activity+json"}],
  544. body:
  545. Jason.encode!(%{
  546. "id" => "https://media.test/objects/final-domain-id",
  547. "type" => "Note",
  548. "content" => "This has final domain in id",
  549. "actor" => "https://original.test/users/actor",
  550. "attributedTo" => "https://original.test/users/actor"
  551. })
  552. }
  553. # No redirect - same domain
  554. %{method: :get, url: "https://original.test/objects/same-domain-redirect"} ->
  555. %Tesla.Env{
  556. status: 200,
  557. url: "https://original.test/objects/different-path",
  558. headers: [{"content-type", "application/activity+json"}],
  559. body:
  560. Jason.encode!(%{
  561. "id" => "https://original.test/objects/same-domain-redirect",
  562. "type" => "Note",
  563. "content" => "This has a same-domain redirect",
  564. "actor" => "https://original.test/users/actor",
  565. "attributedTo" => "https://original.test/users/actor"
  566. })
  567. }
  568. # Test case with missing url field in response (common in tests)
  569. %{method: :get, url: "https://original.test/objects/missing-url"} ->
  570. %Tesla.Env{
  571. status: 200,
  572. # No url field
  573. headers: [{"content-type", "application/activity+json"}],
  574. body:
  575. Jason.encode!(%{
  576. "id" => "https://original.test/objects/missing-url",
  577. "type" => "Note",
  578. "content" => "This has no URL field in response",
  579. "actor" => "https://original.test/users/actor",
  580. "attributedTo" => "https://original.test/users/actor"
  581. })
  582. }
  583. end)
  584. :ok
  585. end
  586. test "it rejects objects from cross-domain redirects with original domain in id" do
  587. assert {:error, {:cross_domain_redirect, true}} =
  588. Fetcher.fetch_and_contain_remote_object_from_id(
  589. "https://original.test/objects/123"
  590. )
  591. end
  592. test "it rejects objects from cross-domain redirects with final domain in id" do
  593. assert {:error, {:cross_domain_redirect, true}} =
  594. Fetcher.fetch_and_contain_remote_object_from_id(
  595. "https://original.test/objects/final-domain-id"
  596. )
  597. end
  598. test "it accepts objects with same-domain redirects" do
  599. assert {:ok, data} =
  600. Fetcher.fetch_and_contain_remote_object_from_id(
  601. "https://original.test/objects/same-domain-redirect"
  602. )
  603. assert data["content"] == "This has a same-domain redirect"
  604. end
  605. test "it handles responses without URL field (common in tests)" do
  606. assert {:ok, data} =
  607. Fetcher.fetch_and_contain_remote_object_from_id(
  608. "https://original.test/objects/missing-url"
  609. )
  610. assert data["content"] == "This has no URL field in response"
  611. end
  612. end
  613. describe "fetch with history" do
  614. setup do
  615. object2 = %{
  616. "id" => "https://mastodon.social/2",
  617. "actor" => "https://mastodon.social/users/emelie",
  618. "attributedTo" => "https://mastodon.social/users/emelie",
  619. "type" => "Note",
  620. "content" => "test 2",
  621. "bcc" => [],
  622. "bto" => [],
  623. "cc" => ["https://mastodon.social/users/emelie/followers"],
  624. "to" => [],
  625. "summary" => "",
  626. "formerRepresentations" => %{
  627. "type" => "OrderedCollection",
  628. "orderedItems" => [
  629. %{
  630. "type" => "Note",
  631. "content" => "orig 2",
  632. "actor" => "https://mastodon.social/users/emelie",
  633. "attributedTo" => "https://mastodon.social/users/emelie",
  634. "bcc" => [],
  635. "bto" => [],
  636. "cc" => ["https://mastodon.social/users/emelie/followers"],
  637. "to" => [],
  638. "summary" => ""
  639. }
  640. ],
  641. "totalItems" => 1
  642. }
  643. }
  644. mock(fn
  645. %{
  646. method: :get,
  647. url: "https://mastodon.social/2"
  648. } ->
  649. %Tesla.Env{
  650. status: 200,
  651. headers: [{"content-type", "application/activity+json"}],
  652. body: Jason.encode!(object2)
  653. }
  654. %{
  655. method: :get,
  656. url: "https://mastodon.social/users/emelie/collections/featured"
  657. } ->
  658. %Tesla.Env{
  659. status: 200,
  660. headers: [{"content-type", "application/activity+json"}],
  661. body:
  662. Jason.encode!(%{
  663. "id" => "https://mastodon.social/users/emelie/collections/featured",
  664. "type" => "OrderedCollection",
  665. "actor" => "https://mastodon.social/users/emelie",
  666. "attributedTo" => "https://mastodon.social/users/emelie",
  667. "orderedItems" => [],
  668. "totalItems" => 0
  669. })
  670. }
  671. env ->
  672. apply(HttpRequestMock, :request, [env])
  673. end)
  674. %{object2: object2}
  675. end
  676. test "it gets history", %{object2: object2} do
  677. {:ok, object} = Fetcher.fetch_object_from_id(object2["id"])
  678. assert %{
  679. "formerRepresentations" => %{
  680. "type" => "OrderedCollection",
  681. "orderedItems" => [%{}]
  682. }
  683. } = object.data
  684. end
  685. end
  686. end